corems.ms_peak.calc.MSPeakCalc
1__author__ = "Yuri E. Corilo" 2__date__ = "Jun 04, 2019" 3 4import warnings 5 6import pyswarm 7from lmfit import models 8from numpy import ( 9 ceil, 10 exp, 11 flip, 12 floor, 13 linspace, 14 log, 15 nan, 16 pi, 17 poly1d, 18 polyfit, 19 rint, 20 sqrt, 21 square, 22) 23try: 24 from numpy import trapezoid 25except ImportError: # numpy < 2.0 26 from numpy import trapz as trapezoid 27 28from corems.encapsulation.constant import Atoms 29from corems.encapsulation.factory.parameters import MSParameters 30 31 32class MSPeakCalculation: 33 """Class to perform calculations on MSPeak objects. 34 35 This class provides methods to perform various calculations on MSPeak objects, such as calculating Kendrick Mass Defect (KMD) and Kendrick Mass (KM), calculating peak area, and fitting peak lineshape using different models. 36 37 Parameters 38 ---------- 39 None 40 41 Attributes 42 ---------- 43 _ms_parent : MSParent 44 The parent MSParent object associated with the MSPeakCalculation object. 45 mz_exp : float 46 The experimental m/z value of the peak. 47 peak_left_index : int 48 The start scan index of the peak. 49 peak_right_index : int 50 The final scan index of the peak. 51 resolving_power : float 52 The resolving power of the peak. 53 54 Methods 55 ------- 56 * _calc_kmd(dict_base). 57 Calculate the Kendrick Mass Defect (KMD) and Kendrick Mass (KM) for a given base formula. 58 * calc_area(). 59 Calculate the peak area using numpy's trapezoidal fit. 60 * fit_peak(mz_extend=6, delta_rp=0, model='Gaussian'). 61 Perform lineshape analysis on a peak using lmfit module. 62 * voigt_pso(w, r, yoff, width, loc, a). 63 Calculate the Voigt function for particle swarm optimization (PSO) fitting. 64 * objective_pso(x, w, u). 65 Calculate the objective function for PSO fitting. 66 * minimize_pso(lower, upper, w, u). 67 Minimize the objective function using the particle swarm optimization algorithm. 68 * fit_peak_pso(mz_extend=6, upsample_multiplier=5). 69 Perform lineshape analysis on a peak using particle swarm optimization (PSO) fitting. 70 * voigt(oversample_multiplier=1, delta_rp=0, mz_overlay=1). 71 [Legacy] Perform voigt lineshape analysis on a peak. 72 * pseudovoigt(oversample_multiplier=1, delta_rp=0, mz_overlay=1, fraction=0.5). 73 [Legacy] Perform pseudovoigt lineshape analysis on a peak. 74 * lorentz(oversample_multiplier=1, delta_rp=0, mz_overlay=1). 75 [Legacy] Perform lorentz lineshape analysis on a peak. 76 * gaussian(oversample_multiplier=1, delta_rp=0, mz_overlay=1). 77 [Legacy] Perform gaussian lineshape analysis on a peak. 78 * get_mz_domain(oversample_multiplier, mz_overlay). 79 [Legacy] Resample/interpolate datapoints for lineshape analysis. 80 * number_possible_assignments(). 81 Return the number of possible molecular formula assignments for the peak. 82 * molecular_formula_lowest_error(). 83 Return the molecular formula with the smallest absolute mz error. 84 * molecular_formula_highest_prob_score(). 85 Return the molecular formula with the highest confidence score. 86 * molecular_formula_earth_filter(lowest_error=True). 87 Filter molecular formula using the 'Earth' filter. 88 * molecular_formula_water_filter(lowest_error=True). 89 Filter molecular formula using the 'Water' filter. 90 * molecular_formula_air_filter(lowest_error=True). 91 Filter molecular formula using the 'Air' filter. 92 * cia_score_S_P_error(). 93 Compound Identification Algorithm SP Error - Assignment Filter. 94 * cia_score_N_S_P_error(). 95 Compound Identification Algorithm NSP Error - Assignment Filter. 96 97 """ 98 99 def _calc_kmd(self, dict_base): 100 """Calculate the Kendrick Mass Defect (KMD) and Kendrick Mass (KM) for a given base formula 101 102 Parameters 103 ---------- 104 dict_base : dict 105 dictionary with the base formula to be used in the calculation 106 Default is CH2, e.g. 107 dict_base = {"C": 1, "H": 2} 108 """ 109 110 if self._ms_parent: 111 # msPeak obj does have a ms object parent 112 kendrick_rounding_method = ( 113 self._ms_parent.mspeaks_settings.kendrick_rounding_method 114 ) # rounding method can be one of floor, ceil or round 115 # msPeak obj does not have a ms object parent 116 else: 117 kendrick_rounding_method = MSParameters.ms_peak.kendrick_rounding_method 118 119 mass = 0 120 for atom in dict_base.keys(): 121 mass += Atoms.atomic_masses.get(atom) * dict_base.get(atom) 122 123 kendrick_mass = (int(mass) / mass) * self.mz_exp 124 125 if kendrick_rounding_method == "ceil": 126 nominal_km = ceil(kendrick_mass) 127 128 elif kendrick_rounding_method == "round": 129 nominal_km = rint(kendrick_mass) 130 131 elif kendrick_rounding_method == "floor": 132 nominal_km = floor(kendrick_mass) 133 134 else: 135 raise Exception( 136 "%s method was not implemented, please refer to corems.ms_peak.calc.MSPeakCalc Class" 137 % kendrick_rounding_method 138 ) 139 140 kmd = nominal_km - kendrick_mass 141 142 # kmd = (nominal_km - km) * 1 143 # kmd = round(kmd,0) 144 145 return kmd, kendrick_mass, nominal_km 146 147 def calc_area(self): 148 """Calculate the peak area using numpy's trapezoidal fit 149 150 uses provided mz_domain to accurately integrate areas independent of digital resolution 151 152 Returns 153 ------- 154 float 155 peak area 156 """ 157 if self.peak_right_index > self.peak_left_index: 158 yy = self._ms_parent.abundance_profile[ 159 self.peak_left_index : self.peak_right_index 160 ] 161 xx = self._ms_parent.mz_exp_profile[ 162 self.peak_left_index : self.peak_right_index 163 ] 164 # check if the axis is high to low m/z or not. if its MSFromFreq its high mz first, if its from Profile, its low mz first 165 if xx[0] > xx[-1]: 166 xx = flip(xx) 167 yy = flip(yy) 168 return float(trapezoid(yy, xx)) 169 170 else: 171 warnings.warn( 172 "Peak Area Calculation for m/z {} has failed".format(self.mz_exp) 173 ) 174 return nan 175 176 def fit_peak(self, mz_extend=6, delta_rp=0, model="Gaussian"): 177 """Lineshape analysis on a peak using lmfit module. 178 179 Model and fit peak lineshape by defined function - using lmfit module 180 Does not oversample/resample/interpolate data points 181 Better to go back to time domain and perform more zero filling - if possible. 182 183 Parameters 184 ---------- 185 mz_extend : int 186 extra points left and right of peak definition to include in fitting 187 delta_rp : float 188 delta resolving power to add to resolving power 189 model : str 190 Type of lineshape model to use. 191 Models allowed: Gaussian, Lorentz, Voigt 192 193 Returns 194 ----- 195 mz_domain : ndarray 196 x-axis domain for fit 197 fit_peak : lmfit object 198 fit results object from lmfit module 199 200 Notes 201 ----- 202 Returns the calculated mz domain, initial defined abundance profile, and the fit peak results object from lmfit module 203 mz_extend here extends the x-axis domain so that we have sufficient points either side of the apex to fit. 204 Takes about 10ms per peak 205 """ 206 start_index = ( 207 self.peak_left_index - mz_extend if not self.peak_left_index == 0 else 0 208 ) 209 final_index = ( 210 self.peak_right_index + mz_extend 211 if not self.peak_right_index == len(self._ms_parent.mz_exp_profile) 212 else self.peak_right_index 213 ) 214 215 # check if MSPeak contains the resolving power info 216 if self.resolving_power: 217 # full width half maximum distance 218 self.fwhm = self.mz_exp / (self.resolving_power + delta_rp) 219 220 mz_domain = self._ms_parent.mz_exp_profile[start_index:final_index] 221 abundance_domain = self._ms_parent.abundance_profile[ 222 start_index:final_index 223 ] 224 225 if model == "Gaussian": 226 # stardard deviation 227 sigma = self.fwhm / (2 * sqrt(2 * log(2))) 228 amplitude = (sqrt(2 * pi) * sigma) * self.abundance 229 model = models.GaussianModel() 230 params = model.make_params( 231 center=self.mz_exp, amplitude=amplitude, sigma=sigma 232 ) 233 234 elif model == "Lorentz": 235 # stardard deviation 236 sigma = self.fwhm / 2 237 amplitude = sigma * pi * self.abundance 238 model = models.LorentzianModel() 239 params = model.make_params( 240 center=self.mz_exp, amplitude=amplitude, sigma=sigma 241 ) 242 243 elif model == "Voigt": 244 # stardard deviation 245 sigma = self.fwhm / 3.6013 246 amplitude = (sqrt(2 * pi) * sigma) * self.abundance 247 model = models.VoigtModel() 248 params = model.make_params( 249 center=self.mz_exp, amplitude=amplitude, sigma=sigma, gamma=sigma 250 ) 251 else: 252 raise LookupError("model lineshape not known or defined") 253 254 # calc_abundance = model.eval(params=params, x=mz_domain) #Same as initial fit, returned in fit_peak object 255 fit_peak = model.fit(abundance_domain, params=params, x=mz_domain) 256 return mz_domain, fit_peak 257 258 else: 259 raise LookupError( 260 "resolving power is not defined, try to use set_max_resolving_power()" 261 ) 262 263 def voigt_pso(self, w, r, yoff, width, loc, a): 264 """Voigt function for particle swarm optimisation (PSO) fitting 265 266 From https://github.com/pnnl/nmrfit/blob/master/nmrfit/equations.py. 267 Calculates a Voigt function over w based on the relevant properties of the distribution. 268 269 Parameters 270 ---------- 271 w : ndarray 272 Array over which the Voigt function will be evaluated. 273 r : float 274 Ratio between the Guassian and Lorentzian functions. 275 yoff : float 276 Y-offset of the Voigt function. 277 width : float 278 The width of the Voigt function. 279 loc : float 280 Center of the Voigt function. 281 a : float 282 Area of the Voigt function. 283 Returns 284 ------- 285 V : ndarray 286 Array defining the Voigt function over w. 287 288 References 289 ---------- 290 1. https://github.com/pnnl/nmrfit 291 292 Notes 293 ----- 294 Particle swarm optimisation (PSO) fitting function can be significantly more computationally expensive than lmfit, with more parameters to optimise. 295 296 """ 297 # Lorentzian component 298 L = (2 / (pi * width)) * 1 / (1 + ((w - loc) / (0.5 * width)) ** 2) 299 300 # Gaussian component 301 G = ( 302 (2 / width) 303 * sqrt(log(2) / pi) 304 * exp(-(((w - loc) / (width / (2 * sqrt(log(2))))) ** 2)) 305 ) 306 307 # Voigt body 308 V = (yoff + a) * (r * L + (1 - r) * G) 309 310 return V 311 312 def objective_pso(self, x, w, u): 313 """Objective function for particle swarm optimisation (PSO) fitting 314 315 The objective function used to fit supplied data. Evaluates sum of squared differences between the fit and the data. 316 317 Parameters 318 ---------- 319 x : list of floats 320 Parameter vector. 321 w : ndarray 322 Array of frequency data. 323 u : ndarray 324 Array of data to be fit. 325 326 Returns 327 ------- 328 rmse : float 329 Root mean square error between the data and fit. 330 331 References 332 ---------- 333 1. https://github.com/pnnl/nmrfit 334 335 """ 336 # global parameters 337 r, width, loc, a = x 338 yoff = 0 339 340 # calculate fit for V 341 V_fit = self.voigt_pso(w, r, yoff, width, loc, a) 342 343 # real component RMSE 344 rmse = sqrt(square((u - V_fit)).mean(axis=None)) 345 346 # return the total RMSE 347 return rmse 348 349 def minimize_pso(self, lower, upper, w, u): 350 """Minimization function for particle swarm optimisation (PSO) fitting 351 352 Minimizes the objective function using the particle swarm optimization algorithm. 353 Minimization function based on defined parameters 354 355 356 Parameters 357 ---------- 358 lower : list of floats 359 Lower bounds for the parameters. 360 upper : list of floats 361 Upper bounds for the parameters. 362 w : ndarray 363 Array of frequency data. 364 u : ndarray 365 Array of data to be fit. 366 367 Notes 368 ----- 369 Particle swarm optimisation (PSO) fitting function can be significantly more computationally expensive than lmfit, with more parameters to optimise. 370 Current parameters take ~2 seconds per peak. 371 372 373 References 374 ---------- 375 1. https://github.com/pnnl/nmrfit 376 377 """ 378 # TODO - allow support to pass swarmsize, maxiter, omega, phip, phig parameters. 379 # TODO - Refactor PSO fitting into its own class? 380 381 xopt, fopt = pyswarm.pso( 382 self.objective_pso, 383 lower, 384 upper, 385 args=(w, u), 386 swarmsize=1000, 387 maxiter=5000, 388 omega=-0.2134, 389 phip=-0.3344, 390 phig=2.3259, 391 ) 392 return xopt, fopt 393 394 def fit_peak_pso(self, mz_extend: int = 6, upsample_multiplier: int = 5): 395 """Lineshape analysis on a peak using particle swarm optimisation (PSO) fitting 396 397 Function to fit a Voigt peakshape using particle swarm optimisation (PSO). 398 Should return better results than lmfit, but much more computationally expensive 399 400 Parameters 401 ---------- 402 mz_extend : int, optional 403 extra points left and right of peak definition to include in fitting. Defaults to 6. 404 upsample_multiplier : int, optional 405 factor to increase x-axis points by for simulation of fitted lineshape function. Defaults to 5. 406 407 Returns 408 ------- 409 xopt : array 410 variables describing the voigt function. 411 G/L ratio, width (fwhm), apex (x-axis), area. 412 y-axis offset is fixed at 0 413 fopt : float 414 objective score (rmse) 415 psfit : array 416 recalculated y values based on function and optimised fit 417 psfit_hdp : tuple of arrays 418 0 - linspace x-axis upsampled grid 419 1 - recalculated y values based on function and upsampled x-axis grid 420 Does not change results, but aids in visualisation of the 'true' voigt lineshape 421 422 Notes 423 ----- 424 Particle swarm optimisation (PSO) fitting function can be significantly more computationally expensive than lmfit, with more parameters to optimise. 425 """ 426 # TODO - Add ability to pass pso args (i.e. swarm size, maxiter, omega, phig, etc) 427 # TODO: fix xopt. Magnitude mode data through CoreMS/Bruker starts at 0 but is noise centered well above 0. 428 # Thermo data is noise reduced by also noise subtracted, so starts at 0 429 # Absorption mode/phased data will have positive and negative components and may not be baseline corrected 430 431 start_index = ( 432 self.peak_left_index - mz_extend if not self.peak_left_index == 0 else 0 433 ) 434 final_index = ( 435 self.peak_right_index + mz_extend 436 if not self.peak_right_index == len(self._ms_parent.mz_exp_profile) 437 else self.peak_right_index 438 ) 439 440 # check if MSPeak contains the resolving power info 441 if self.resolving_power: 442 # full width half maximum distance 443 self.fwhm = self.mz_exp / (self.resolving_power) 444 445 mz_domain = self._ms_parent.mz_exp_profile[start_index:final_index] 446 abundance_domain = self._ms_parent.abundance_profile[ 447 start_index:final_index 448 ] 449 lower = [0, self.fwhm * 0.8, (self.mz_exp - 0.0005), 0] 450 upper = [ 451 1, 452 self.fwhm * 1.2, 453 (self.mz_exp + 0.0005), 454 self.abundance / self.signal_to_noise, 455 ] 456 xopt, fopt = self.minimize_pso(lower, upper, mz_domain, abundance_domain) 457 458 psfit = self.voigt_pso(mz_domain, xopt[0], 0, xopt[1], xopt[2], xopt[3]) 459 psfit_hdp_x = linspace( 460 min(mz_domain), max(mz_domain), num=len(mz_domain) * upsample_multiplier 461 ) 462 psfit_hdp = self.voigt_pso( 463 psfit_hdp_x, xopt[0], 0, xopt[1], xopt[2], xopt[3] 464 ) 465 return xopt, fopt, psfit, (psfit_hdp_x, psfit_hdp) 466 else: 467 raise LookupError( 468 "resolving power is not defined, try to use set_max_resolving_power()" 469 ) 470 471 def voigt(self, oversample_multiplier=1, delta_rp=0, mz_overlay=1): 472 """[Legacy] Voigt lineshape analysis function 473 Legacy function for voigt lineshape analysis 474 475 Parameters 476 ---------- 477 oversample_multiplier : int 478 factor to increase x-axis points by for simulation of fitted lineshape function 479 delta_rp : float 480 delta resolving power to add to resolving power 481 mz_overlay : int 482 extra points left and right of peak definition to include in fitting 483 484 Returns 485 ------- 486 mz_domain : ndarray 487 x-axis domain for fit 488 calc_abundance : ndarray 489 calculated abundance profile based on voigt function 490 """ 491 492 if self.resolving_power: 493 # full width half maximum distance 494 self.fwhm = self.mz_exp / ( 495 self.resolving_power + delta_rp 496 ) # self.resolving_power) 497 498 # stardart deviation 499 sigma = self.fwhm / 3.6013 500 501 # half width baseline distance 502 503 # mz_domain = linspace(self.mz_exp - hw_base_distance, 504 # self.mz_exp + hw_base_distance, datapoint) 505 mz_domain = self.get_mz_domain(oversample_multiplier, mz_overlay) 506 507 # gaussian_pdf = lambda x0, x, s: (1/ math.sqrt(2*math.pi*math.pow(s,2))) * math.exp(-1 * math.pow(x-x0,2) / 2*math.pow(s,2) ) 508 509 # TODO derive amplitude 510 amplitude = (sqrt(2 * pi) * sigma) * self.abundance 511 512 model = models.VoigtModel() 513 514 params = model.make_params( 515 center=self.mz_exp, amplitude=amplitude, sigma=sigma, gamma=sigma 516 ) 517 518 calc_abundance = model.eval(params=params, x=mz_domain) 519 520 return mz_domain, calc_abundance 521 522 else: 523 raise LookupError( 524 "resolving power is not defined, try to use set_max_resolving_power()" 525 ) 526 527 def pseudovoigt( 528 self, oversample_multiplier=1, delta_rp=0, mz_overlay=1, fraction=0.5 529 ): 530 """[Legacy] pseudovoigt lineshape function 531 532 Legacy function for pseudovoigt lineshape analysis. 533 Note - Code may not be functional currently. 534 535 Parameters 536 ---------- 537 oversample_multiplier : int, optional 538 factor to increase x-axis points by for simulation of fitted lineshape function. Defaults to 1. 539 delta_rp : float, optional 540 delta resolving power to add to resolving power. Defaults to 0. 541 mz_overlay : int, optional 542 extra points left and right of peak definition to include in fitting. Defaults to 1. 543 fraction : float, optional 544 fraction of gaussian component in pseudovoigt function. Defaults to 0.5. 545 546 """ 547 if self.resolving_power: 548 # full width half maximum distance 549 self.fwhm = self.mz_exp / ( 550 self.resolving_power + delta_rp 551 ) # self.resolving_power) 552 553 # stardart deviation 554 sigma = self.fwhm / 2 555 556 # half width baseline distance 557 558 # mz_domain = linspace(self.mz_exp - hw_base_distance, 559 # self.mz_exp + hw_base_distance, datapoint) 560 mz_domain = self.get_mz_domain(oversample_multiplier, mz_overlay) 561 562 # gaussian_pdf = lambda x0, x, s: (1/ math.sqrt(2*math.pi*math.pow(s,2))) * math.exp(-1 * math.pow(x-x0,2) / 2*math.pow(s,2) ) 563 model = models.PseudoVoigtModel() 564 565 # TODO derive amplitude 566 gamma = sigma 567 568 amplitude = (sqrt(2 * pi) * sigma) * self.abundance 569 amplitude = (sqrt(pi / log(2)) * (pi * sigma * self.abundance)) / ( 570 (pi * (1 - gamma)) + (sqrt(pi * log(2)) * gamma) 571 ) 572 573 params = model.make_params(center=self.mz_exp, sigma=sigma) 574 575 calc_abundance = model.eval(params=params, x=mz_domain) 576 577 return mz_domain, calc_abundance 578 579 else: 580 raise LookupError( 581 "resolving power is not defined, try to use set_max_resolving_power()" 582 ) 583 584 def lorentz(self, oversample_multiplier=1, delta_rp=0, mz_overlay=1): 585 """[Legacy] Lorentz lineshape analysis function 586 587 Legacy function for lorentz lineshape analysis 588 589 Parameters 590 ---------- 591 oversample_multiplier : int 592 factor to increase x-axis points by for simulation of fitted lineshape function 593 delta_rp : float 594 delta resolving power to add to resolving power 595 mz_overlay : int 596 extra points left and right of peak definition to include in fitting 597 598 Returns 599 ------- 600 mz_domain : ndarray 601 x-axis domain for fit 602 calc_abundance : ndarray 603 calculated abundance profile based on lorentz function 604 605 """ 606 if self.resolving_power: 607 # full width half maximum distance 608 self.fwhm = self.mz_exp / ( 609 self.resolving_power + delta_rp 610 ) # self.resolving_power) 611 612 # stardart deviation 613 sigma = self.fwhm / 2 614 615 # half width baseline distance 616 hw_base_distance = 8 * sigma 617 618 # mz_domain = linspace(self.mz_exp - hw_base_distance, 619 # self.mz_exp + hw_base_distance, datapoint) 620 621 mz_domain = self.get_mz_domain(oversample_multiplier, mz_overlay) 622 # gaussian_pdf = lambda x0, x, s: (1/ math.sqrt(2*math.pi*math.pow(s,2))) * math.exp(-1 * math.pow(x-x0,2) / 2*math.pow(s,2) ) 623 model = models.LorentzianModel() 624 625 amplitude = sigma * pi * self.abundance 626 627 params = model.make_params( 628 center=self.mz_exp, amplitude=amplitude, sigma=sigma 629 ) 630 631 calc_abundance = model.eval(params=params, x=mz_domain) 632 633 return mz_domain, calc_abundance 634 635 else: 636 raise LookupError( 637 "resolving power is not defined, try to use set_max_resolving_power()" 638 ) 639 640 def gaussian(self, oversample_multiplier=1, delta_rp=0, mz_overlay=1): 641 """[Legacy] Gaussian lineshape analysis function 642 Legacy gaussian lineshape analysis function 643 644 Parameters 645 ---------- 646 oversample_multiplier : int 647 factor to increase x-axis points by for simulation of fitted lineshape function 648 delta_rp : float 649 delta resolving power to add to resolving power 650 mz_overlay : int 651 extra points left and right of peak definition to include in fitting 652 653 Returns 654 ------- 655 mz_domain : ndarray 656 x-axis domain for fit 657 calc_abundance : ndarray 658 calculated abundance profile based on gaussian function 659 660 661 """ 662 663 # check if MSPeak contains the resolving power info 664 if self.resolving_power: 665 # full width half maximum distance 666 self.fwhm = self.mz_exp / ( 667 self.resolving_power + delta_rp 668 ) # self.resolving_power) 669 670 # stardart deviation 671 sigma = self.fwhm / (2 * sqrt(2 * log(2))) 672 673 # half width baseline distance 674 # hw_base_distance = (3.2 * s) 675 676 # match_loz_factor = 3 677 678 # n_d = hw_base_distance * match_loz_factor 679 680 # mz_domain = linspace( 681 # self.mz_exp - n_d, self.mz_exp + n_d, datapoint) 682 683 mz_domain = self.get_mz_domain(oversample_multiplier, mz_overlay) 684 685 # gaussian_pdf = lambda x0, x, s: (1/ math.sqrt(2*math.pi*math.pow(s,2))) * math.exp(-1 * math.pow(x-x0,2) / 2*math.pow(s,2) ) 686 687 # calc_abundance = norm.pdf(mz_domain, self.mz_exp, s) 688 689 model = models.GaussianModel() 690 691 amplitude = (sqrt(2 * pi) * sigma) * self.abundance 692 693 params = model.make_params( 694 center=self.mz_exp, amplitude=amplitude, sigma=sigma 695 ) 696 697 calc_abundance = model.eval(params=params, x=mz_domain) 698 699 return mz_domain, calc_abundance 700 701 else: 702 raise LookupError( 703 "resolving power is not defined, try to use set_max_resolving_power()" 704 ) 705 706 def get_mz_domain(self, oversample_multiplier, mz_overlay): 707 """[Legacy] function to resample/interpolate datapoints for lineshape analysis 708 709 This code is used for the legacy line fitting functions and not recommended. 710 Legacy function to support expanding mz domain for legacy lineshape functions 711 712 Parameters 713 ---------- 714 oversample_multiplier : int 715 factor to increase x-axis points by for simulation of fitted lineshape function 716 mz_overlay : int 717 extra points left and right of peak definition to include in fitting 718 719 Returns 720 ------- 721 mz_domain : ndarray 722 x-axis domain for fit 723 724 """ 725 start_index = ( 726 self.peak_left_index - mz_overlay if not self.peak_left_index == 0 else 0 727 ) 728 final_index = ( 729 self.peak_right_index + mz_overlay 730 if not self.peak_right_index == len(self._ms_parent.mz_exp_profile) 731 else self.peak_right_index 732 ) 733 734 if oversample_multiplier == 1: 735 mz_domain = self._ms_parent.mz_exp_profile[start_index:final_index] 736 737 else: 738 # we assume a linear correlation for m/z and datapoits 739 # which is only true if the m/z range in narrow (within 1 m/z unit) 740 # this is not true for a wide m/z range 741 742 indexes = range(start_index, final_index + 1) 743 mz = self._ms_parent.mz_exp_profile[indexes] 744 pol = poly1d(polyfit(indexes, mz, 1)) 745 oversampled_indexes = linspace( 746 start_index, 747 final_index, 748 (final_index - start_index) * oversample_multiplier, 749 ) 750 mz_domain = pol(oversampled_indexes) 751 752 return mz_domain 753 754 @property 755 def number_possible_assignments( 756 self, 757 ): 758 return len(self.molecular_formulas) 759 760 def molecular_formula_lowest_error(self): 761 """Return the molecular formula with the smallest absolute mz error""" 762 763 return min(self.molecular_formulas, key=lambda m: abs(m.mz_error)) 764 765 def molecular_formula_highest_prob_score(self): 766 """Return the molecular formula with the highest confidence score score""" 767 768 return max(self.molecular_formulas, key=lambda m: abs(m.confidence_score)) 769 770 def molecular_formula_earth_filter(self, lowest_error=True): 771 """Filter molecular formula using the 'Earth' filter 772 773 This function applies the Formularity-esque 'Earth' filter to possible molecular formula assignments. 774 Earth Filter: 775 O > 0 AND N <= 3 AND P <= 2 AND 3P <= O 776 777 If the lowest_error method is also used, it will return the single formula annotation with the smallest absolute error which also fits the Earth filter. 778 Otherwise, it will return all Earth-filter compliant formulas. 779 780 Parameters 781 ---------- 782 lowest_error : bool, optional. 783 Return only the lowest error formula which also fits the Earth filter. 784 If False, return all Earth-filter compliant formulas. Default is True. 785 786 Returns 787 ------- 788 list 789 List of molecular formula objects which fit the Earth filter 790 791 References 792 ---------- 793 1. Nikola Tolic et al., "Formularity: Software for Automated Formula Assignment of Natural and Other Organic Matter from Ultrahigh-Resolution Mass Spectra" 794 Anal. Chem. 2017, 89, 23, 12659–12665 795 doi: 10.1021/acs.analchem.7b03318 796 """ 797 798 candidates = list( 799 filter( 800 lambda mf: mf.get("O") > 0 801 and mf.get("N") <= 3 802 and mf.get("P") <= 2 803 and (3 * mf.get("P")) <= mf.get("O"), 804 self.molecular_formulas, 805 ) 806 ) 807 if len(candidates) > 0: 808 if lowest_error: 809 return min(candidates, key=lambda m: abs(m.mz_error)) 810 else: 811 return candidates 812 else: 813 return candidates 814 815 def molecular_formula_water_filter(self, lowest_error=True): 816 """Filter molecular formula using the 'Water' filter 817 818 This function applies the Formularity-esque 'Water' filter to possible molecular formula assignments. 819 Water Filter: 820 O > 0 AND N <= 3 AND S <= 2 AND P <= 2 821 822 If the lowest_error method is also used, it will return the single formula annotation with the smallest absolute error which also fits the Water filter. 823 Otherwise, it will return all Water-filter compliant formulas. 824 825 Parameters 826 ---------- 827 lowest_error : bool, optional 828 Return only the lowest error formula which also fits the Water filter. 829 If False, return all Water-filter compliant formulas. Defaults to 2 830 831 Returns 832 ------- 833 list 834 List of molecular formula objects which fit the Water filter 835 836 References 837 ---------- 838 1. Nikola Tolic et al., "Formularity: Software for Automated Formula Assignment of Natural and Other Organic Matter from Ultrahigh-Resolution Mass Spectra" 839 Anal. Chem. 2017, 89, 23, 12659–12665 840 doi: 10.1021/acs.analchem.7b03318 841 """ 842 843 candidates = list( 844 filter( 845 lambda mf: mf.get("O") > 0 846 and mf.get("N") <= 3 847 and mf.get("S") <= 2 848 and mf.get("P") <= 2, 849 self.molecular_formulas, 850 ) 851 ) 852 if len(candidates) > 0: 853 if lowest_error: 854 return min(candidates, key=lambda m: abs(m.mz_error)) 855 else: 856 return candidates 857 else: 858 return candidates 859 860 def molecular_formula_air_filter(self, lowest_error=True): 861 """Filter molecular formula using the 'Air' filter 862 863 This function applies the Formularity-esque 'Air' filter to possible molecular formula assignments. 864 Air Filter: 865 O > 0 AND N <= 3 AND S <= 1 AND P = 0 AND 3(S+N) <= O 866 867 If the lowest_error method is also used, it will return the single formula annotation with the smallest absolute error which also fits the Air filter. 868 Otherwise, it will return all Air-filter compliant formulas. 869 870 Parameters 871 ---------- 872 lowest_error : bool, optional 873 Return only the lowest error formula which also fits the Air filter. 874 If False, return all Air-filter compliant formulas. Defaults to True. 875 876 Returns 877 ------- 878 list 879 List of molecular formula objects which fit the Air filter 880 881 References 882 ---------- 883 1. Nikola Tolic et al., "Formularity: Software for Automated Formula Assignment of Natural and Other Organic Matter from Ultrahigh-Resolution Mass Spectra" 884 Anal. Chem. 2017, 89, 23, 12659–12665 885 doi: 10.1021/acs.analchem.7b03318 886 """ 887 888 candidates = list( 889 filter( 890 lambda mf: mf.get("O") > 0 891 and mf.get("N") <= 2 892 and mf.get("S") <= 1 893 and mf.get("P") == 0 894 and 3 * (mf.get("S") + mf.get("N")) <= mf.get("O"), 895 self.molecular_formulas, 896 ) 897 ) 898 899 if len(candidates) > 0: 900 if lowest_error: 901 return min(candidates, key=lambda m: abs(m.mz_error)) 902 else: 903 return candidates 904 else: 905 return candidates 906 907 def cia_score_S_P_error(self): 908 """Compound Identification Algorithm SP Error - Assignment Filter 909 910 This function applies the Compound Identification Algorithm (CIA) SP Error filter to possible molecular formula assignments. 911 912 It takes the molecular formula with the lowest S+P count, and returns the formula with the lowest absolute error from this subset. 913 914 Returns 915 ------- 916 MolecularFormula 917 A single molecular formula which fits the rules of the CIA SP Error filter 918 919 920 References 921 ---------- 922 1. Elizabeth B. Kujawinski and Mark D. Behn, "Automated Analysis of Electrospray Ionization Fourier Transform Ion Cyclotron Resonance Mass Spectra of Natural Organic Matter" 923 Anal. Chem. 2006, 78, 13, 4363–4373 924 doi: 10.1021/ac0600306 925 """ 926 # case EFormulaScore.HAcap: 927 928 lowest_S_P_mf = min( 929 self.molecular_formulas, key=lambda mf: mf.get("S") + mf.get("P") 930 ) 931 lowest_S_P_count = lowest_S_P_mf.get("S") + lowest_S_P_mf.get("P") 932 933 list_same_s_p = list( 934 filter( 935 lambda mf: mf.get("S") + mf.get("P") == lowest_S_P_count, 936 self.molecular_formulas, 937 ) 938 ) 939 940 # check if list is not empty 941 if list_same_s_p: 942 return min(list_same_s_p, key=lambda m: abs(m.mz_error)) 943 944 else: 945 return lowest_S_P_mf 946 947 def cia_score_N_S_P_error(self): 948 """Compound Identification Algorithm NSP Error - Assignment Filter 949 950 This function applies the Compound Identification Algorithm (CIA) NSP Error filter to possible molecular formula assignments. 951 952 It takes the molecular formula with the lowest N+S+P count, and returns the formula with the lowest absolute error from this subset. 953 954 Returns 955 ------- 956 MolecularFormula 957 A single molecular formula which fits the rules of the CIA NSP Error filter 958 959 References 960 ---------- 961 1. Elizabeth B. Kujawinski and Mark D. Behn, "Automated Analysis of Electrospray Ionization Fourier Transform Ion Cyclotron Resonance Mass Spectra of Natural Organic Matter" 962 Anal. Chem. 2006, 78, 13, 4363–4373 963 doi: 10.1021/ac0600306 964 965 Raises 966 ------- 967 Exception 968 If no molecular formula are associated with mass spectrum peak. 969 """ 970 # case EFormulaScore.HAcap: 971 if self.molecular_formulas: 972 lowest_N_S_P_mf = min( 973 self.molecular_formulas, 974 key=lambda mf: mf.get("N") + mf.get("S") + mf.get("P"), 975 ) 976 lowest_N_S_P_count = ( 977 lowest_N_S_P_mf.get("N") 978 + lowest_N_S_P_mf.get("S") 979 + lowest_N_S_P_mf.get("P") 980 ) 981 982 list_same_N_S_P = list( 983 filter( 984 lambda mf: mf.get("N") + mf.get("S") + mf.get("P") 985 == lowest_N_S_P_count, 986 self.molecular_formulas, 987 ) 988 ) 989 990 if list_same_N_S_P: 991 SP_filtered_list = list( 992 filter( 993 lambda mf: (mf.get("S") <= 3) and (mf.get("P") <= 1), 994 list_same_N_S_P, 995 ) 996 ) 997 998 if SP_filtered_list: 999 return min(SP_filtered_list, key=lambda m: abs(m.mz_error)) 1000 1001 else: 1002 return min(list_same_N_S_P, key=lambda m: abs(m.mz_error)) 1003 1004 else: 1005 return lowest_N_S_P_mf 1006 else: 1007 raise Exception( 1008 "No molecular formula associated with the mass spectrum peak at m/z: %.6f" 1009 % self.mz_exp 1010 )
33class MSPeakCalculation: 34 """Class to perform calculations on MSPeak objects. 35 36 This class provides methods to perform various calculations on MSPeak objects, such as calculating Kendrick Mass Defect (KMD) and Kendrick Mass (KM), calculating peak area, and fitting peak lineshape using different models. 37 38 Parameters 39 ---------- 40 None 41 42 Attributes 43 ---------- 44 _ms_parent : MSParent 45 The parent MSParent object associated with the MSPeakCalculation object. 46 mz_exp : float 47 The experimental m/z value of the peak. 48 peak_left_index : int 49 The start scan index of the peak. 50 peak_right_index : int 51 The final scan index of the peak. 52 resolving_power : float 53 The resolving power of the peak. 54 55 Methods 56 ------- 57 * _calc_kmd(dict_base). 58 Calculate the Kendrick Mass Defect (KMD) and Kendrick Mass (KM) for a given base formula. 59 * calc_area(). 60 Calculate the peak area using numpy's trapezoidal fit. 61 * fit_peak(mz_extend=6, delta_rp=0, model='Gaussian'). 62 Perform lineshape analysis on a peak using lmfit module. 63 * voigt_pso(w, r, yoff, width, loc, a). 64 Calculate the Voigt function for particle swarm optimization (PSO) fitting. 65 * objective_pso(x, w, u). 66 Calculate the objective function for PSO fitting. 67 * minimize_pso(lower, upper, w, u). 68 Minimize the objective function using the particle swarm optimization algorithm. 69 * fit_peak_pso(mz_extend=6, upsample_multiplier=5). 70 Perform lineshape analysis on a peak using particle swarm optimization (PSO) fitting. 71 * voigt(oversample_multiplier=1, delta_rp=0, mz_overlay=1). 72 [Legacy] Perform voigt lineshape analysis on a peak. 73 * pseudovoigt(oversample_multiplier=1, delta_rp=0, mz_overlay=1, fraction=0.5). 74 [Legacy] Perform pseudovoigt lineshape analysis on a peak. 75 * lorentz(oversample_multiplier=1, delta_rp=0, mz_overlay=1). 76 [Legacy] Perform lorentz lineshape analysis on a peak. 77 * gaussian(oversample_multiplier=1, delta_rp=0, mz_overlay=1). 78 [Legacy] Perform gaussian lineshape analysis on a peak. 79 * get_mz_domain(oversample_multiplier, mz_overlay). 80 [Legacy] Resample/interpolate datapoints for lineshape analysis. 81 * number_possible_assignments(). 82 Return the number of possible molecular formula assignments for the peak. 83 * molecular_formula_lowest_error(). 84 Return the molecular formula with the smallest absolute mz error. 85 * molecular_formula_highest_prob_score(). 86 Return the molecular formula with the highest confidence score. 87 * molecular_formula_earth_filter(lowest_error=True). 88 Filter molecular formula using the 'Earth' filter. 89 * molecular_formula_water_filter(lowest_error=True). 90 Filter molecular formula using the 'Water' filter. 91 * molecular_formula_air_filter(lowest_error=True). 92 Filter molecular formula using the 'Air' filter. 93 * cia_score_S_P_error(). 94 Compound Identification Algorithm SP Error - Assignment Filter. 95 * cia_score_N_S_P_error(). 96 Compound Identification Algorithm NSP Error - Assignment Filter. 97 98 """ 99 100 def _calc_kmd(self, dict_base): 101 """Calculate the Kendrick Mass Defect (KMD) and Kendrick Mass (KM) for a given base formula 102 103 Parameters 104 ---------- 105 dict_base : dict 106 dictionary with the base formula to be used in the calculation 107 Default is CH2, e.g. 108 dict_base = {"C": 1, "H": 2} 109 """ 110 111 if self._ms_parent: 112 # msPeak obj does have a ms object parent 113 kendrick_rounding_method = ( 114 self._ms_parent.mspeaks_settings.kendrick_rounding_method 115 ) # rounding method can be one of floor, ceil or round 116 # msPeak obj does not have a ms object parent 117 else: 118 kendrick_rounding_method = MSParameters.ms_peak.kendrick_rounding_method 119 120 mass = 0 121 for atom in dict_base.keys(): 122 mass += Atoms.atomic_masses.get(atom) * dict_base.get(atom) 123 124 kendrick_mass = (int(mass) / mass) * self.mz_exp 125 126 if kendrick_rounding_method == "ceil": 127 nominal_km = ceil(kendrick_mass) 128 129 elif kendrick_rounding_method == "round": 130 nominal_km = rint(kendrick_mass) 131 132 elif kendrick_rounding_method == "floor": 133 nominal_km = floor(kendrick_mass) 134 135 else: 136 raise Exception( 137 "%s method was not implemented, please refer to corems.ms_peak.calc.MSPeakCalc Class" 138 % kendrick_rounding_method 139 ) 140 141 kmd = nominal_km - kendrick_mass 142 143 # kmd = (nominal_km - km) * 1 144 # kmd = round(kmd,0) 145 146 return kmd, kendrick_mass, nominal_km 147 148 def calc_area(self): 149 """Calculate the peak area using numpy's trapezoidal fit 150 151 uses provided mz_domain to accurately integrate areas independent of digital resolution 152 153 Returns 154 ------- 155 float 156 peak area 157 """ 158 if self.peak_right_index > self.peak_left_index: 159 yy = self._ms_parent.abundance_profile[ 160 self.peak_left_index : self.peak_right_index 161 ] 162 xx = self._ms_parent.mz_exp_profile[ 163 self.peak_left_index : self.peak_right_index 164 ] 165 # check if the axis is high to low m/z or not. if its MSFromFreq its high mz first, if its from Profile, its low mz first 166 if xx[0] > xx[-1]: 167 xx = flip(xx) 168 yy = flip(yy) 169 return float(trapezoid(yy, xx)) 170 171 else: 172 warnings.warn( 173 "Peak Area Calculation for m/z {} has failed".format(self.mz_exp) 174 ) 175 return nan 176 177 def fit_peak(self, mz_extend=6, delta_rp=0, model="Gaussian"): 178 """Lineshape analysis on a peak using lmfit module. 179 180 Model and fit peak lineshape by defined function - using lmfit module 181 Does not oversample/resample/interpolate data points 182 Better to go back to time domain and perform more zero filling - if possible. 183 184 Parameters 185 ---------- 186 mz_extend : int 187 extra points left and right of peak definition to include in fitting 188 delta_rp : float 189 delta resolving power to add to resolving power 190 model : str 191 Type of lineshape model to use. 192 Models allowed: Gaussian, Lorentz, Voigt 193 194 Returns 195 ----- 196 mz_domain : ndarray 197 x-axis domain for fit 198 fit_peak : lmfit object 199 fit results object from lmfit module 200 201 Notes 202 ----- 203 Returns the calculated mz domain, initial defined abundance profile, and the fit peak results object from lmfit module 204 mz_extend here extends the x-axis domain so that we have sufficient points either side of the apex to fit. 205 Takes about 10ms per peak 206 """ 207 start_index = ( 208 self.peak_left_index - mz_extend if not self.peak_left_index == 0 else 0 209 ) 210 final_index = ( 211 self.peak_right_index + mz_extend 212 if not self.peak_right_index == len(self._ms_parent.mz_exp_profile) 213 else self.peak_right_index 214 ) 215 216 # check if MSPeak contains the resolving power info 217 if self.resolving_power: 218 # full width half maximum distance 219 self.fwhm = self.mz_exp / (self.resolving_power + delta_rp) 220 221 mz_domain = self._ms_parent.mz_exp_profile[start_index:final_index] 222 abundance_domain = self._ms_parent.abundance_profile[ 223 start_index:final_index 224 ] 225 226 if model == "Gaussian": 227 # stardard deviation 228 sigma = self.fwhm / (2 * sqrt(2 * log(2))) 229 amplitude = (sqrt(2 * pi) * sigma) * self.abundance 230 model = models.GaussianModel() 231 params = model.make_params( 232 center=self.mz_exp, amplitude=amplitude, sigma=sigma 233 ) 234 235 elif model == "Lorentz": 236 # stardard deviation 237 sigma = self.fwhm / 2 238 amplitude = sigma * pi * self.abundance 239 model = models.LorentzianModel() 240 params = model.make_params( 241 center=self.mz_exp, amplitude=amplitude, sigma=sigma 242 ) 243 244 elif model == "Voigt": 245 # stardard deviation 246 sigma = self.fwhm / 3.6013 247 amplitude = (sqrt(2 * pi) * sigma) * self.abundance 248 model = models.VoigtModel() 249 params = model.make_params( 250 center=self.mz_exp, amplitude=amplitude, sigma=sigma, gamma=sigma 251 ) 252 else: 253 raise LookupError("model lineshape not known or defined") 254 255 # calc_abundance = model.eval(params=params, x=mz_domain) #Same as initial fit, returned in fit_peak object 256 fit_peak = model.fit(abundance_domain, params=params, x=mz_domain) 257 return mz_domain, fit_peak 258 259 else: 260 raise LookupError( 261 "resolving power is not defined, try to use set_max_resolving_power()" 262 ) 263 264 def voigt_pso(self, w, r, yoff, width, loc, a): 265 """Voigt function for particle swarm optimisation (PSO) fitting 266 267 From https://github.com/pnnl/nmrfit/blob/master/nmrfit/equations.py. 268 Calculates a Voigt function over w based on the relevant properties of the distribution. 269 270 Parameters 271 ---------- 272 w : ndarray 273 Array over which the Voigt function will be evaluated. 274 r : float 275 Ratio between the Guassian and Lorentzian functions. 276 yoff : float 277 Y-offset of the Voigt function. 278 width : float 279 The width of the Voigt function. 280 loc : float 281 Center of the Voigt function. 282 a : float 283 Area of the Voigt function. 284 Returns 285 ------- 286 V : ndarray 287 Array defining the Voigt function over w. 288 289 References 290 ---------- 291 1. https://github.com/pnnl/nmrfit 292 293 Notes 294 ----- 295 Particle swarm optimisation (PSO) fitting function can be significantly more computationally expensive than lmfit, with more parameters to optimise. 296 297 """ 298 # Lorentzian component 299 L = (2 / (pi * width)) * 1 / (1 + ((w - loc) / (0.5 * width)) ** 2) 300 301 # Gaussian component 302 G = ( 303 (2 / width) 304 * sqrt(log(2) / pi) 305 * exp(-(((w - loc) / (width / (2 * sqrt(log(2))))) ** 2)) 306 ) 307 308 # Voigt body 309 V = (yoff + a) * (r * L + (1 - r) * G) 310 311 return V 312 313 def objective_pso(self, x, w, u): 314 """Objective function for particle swarm optimisation (PSO) fitting 315 316 The objective function used to fit supplied data. Evaluates sum of squared differences between the fit and the data. 317 318 Parameters 319 ---------- 320 x : list of floats 321 Parameter vector. 322 w : ndarray 323 Array of frequency data. 324 u : ndarray 325 Array of data to be fit. 326 327 Returns 328 ------- 329 rmse : float 330 Root mean square error between the data and fit. 331 332 References 333 ---------- 334 1. https://github.com/pnnl/nmrfit 335 336 """ 337 # global parameters 338 r, width, loc, a = x 339 yoff = 0 340 341 # calculate fit for V 342 V_fit = self.voigt_pso(w, r, yoff, width, loc, a) 343 344 # real component RMSE 345 rmse = sqrt(square((u - V_fit)).mean(axis=None)) 346 347 # return the total RMSE 348 return rmse 349 350 def minimize_pso(self, lower, upper, w, u): 351 """Minimization function for particle swarm optimisation (PSO) fitting 352 353 Minimizes the objective function using the particle swarm optimization algorithm. 354 Minimization function based on defined parameters 355 356 357 Parameters 358 ---------- 359 lower : list of floats 360 Lower bounds for the parameters. 361 upper : list of floats 362 Upper bounds for the parameters. 363 w : ndarray 364 Array of frequency data. 365 u : ndarray 366 Array of data to be fit. 367 368 Notes 369 ----- 370 Particle swarm optimisation (PSO) fitting function can be significantly more computationally expensive than lmfit, with more parameters to optimise. 371 Current parameters take ~2 seconds per peak. 372 373 374 References 375 ---------- 376 1. https://github.com/pnnl/nmrfit 377 378 """ 379 # TODO - allow support to pass swarmsize, maxiter, omega, phip, phig parameters. 380 # TODO - Refactor PSO fitting into its own class? 381 382 xopt, fopt = pyswarm.pso( 383 self.objective_pso, 384 lower, 385 upper, 386 args=(w, u), 387 swarmsize=1000, 388 maxiter=5000, 389 omega=-0.2134, 390 phip=-0.3344, 391 phig=2.3259, 392 ) 393 return xopt, fopt 394 395 def fit_peak_pso(self, mz_extend: int = 6, upsample_multiplier: int = 5): 396 """Lineshape analysis on a peak using particle swarm optimisation (PSO) fitting 397 398 Function to fit a Voigt peakshape using particle swarm optimisation (PSO). 399 Should return better results than lmfit, but much more computationally expensive 400 401 Parameters 402 ---------- 403 mz_extend : int, optional 404 extra points left and right of peak definition to include in fitting. Defaults to 6. 405 upsample_multiplier : int, optional 406 factor to increase x-axis points by for simulation of fitted lineshape function. Defaults to 5. 407 408 Returns 409 ------- 410 xopt : array 411 variables describing the voigt function. 412 G/L ratio, width (fwhm), apex (x-axis), area. 413 y-axis offset is fixed at 0 414 fopt : float 415 objective score (rmse) 416 psfit : array 417 recalculated y values based on function and optimised fit 418 psfit_hdp : tuple of arrays 419 0 - linspace x-axis upsampled grid 420 1 - recalculated y values based on function and upsampled x-axis grid 421 Does not change results, but aids in visualisation of the 'true' voigt lineshape 422 423 Notes 424 ----- 425 Particle swarm optimisation (PSO) fitting function can be significantly more computationally expensive than lmfit, with more parameters to optimise. 426 """ 427 # TODO - Add ability to pass pso args (i.e. swarm size, maxiter, omega, phig, etc) 428 # TODO: fix xopt. Magnitude mode data through CoreMS/Bruker starts at 0 but is noise centered well above 0. 429 # Thermo data is noise reduced by also noise subtracted, so starts at 0 430 # Absorption mode/phased data will have positive and negative components and may not be baseline corrected 431 432 start_index = ( 433 self.peak_left_index - mz_extend if not self.peak_left_index == 0 else 0 434 ) 435 final_index = ( 436 self.peak_right_index + mz_extend 437 if not self.peak_right_index == len(self._ms_parent.mz_exp_profile) 438 else self.peak_right_index 439 ) 440 441 # check if MSPeak contains the resolving power info 442 if self.resolving_power: 443 # full width half maximum distance 444 self.fwhm = self.mz_exp / (self.resolving_power) 445 446 mz_domain = self._ms_parent.mz_exp_profile[start_index:final_index] 447 abundance_domain = self._ms_parent.abundance_profile[ 448 start_index:final_index 449 ] 450 lower = [0, self.fwhm * 0.8, (self.mz_exp - 0.0005), 0] 451 upper = [ 452 1, 453 self.fwhm * 1.2, 454 (self.mz_exp + 0.0005), 455 self.abundance / self.signal_to_noise, 456 ] 457 xopt, fopt = self.minimize_pso(lower, upper, mz_domain, abundance_domain) 458 459 psfit = self.voigt_pso(mz_domain, xopt[0], 0, xopt[1], xopt[2], xopt[3]) 460 psfit_hdp_x = linspace( 461 min(mz_domain), max(mz_domain), num=len(mz_domain) * upsample_multiplier 462 ) 463 psfit_hdp = self.voigt_pso( 464 psfit_hdp_x, xopt[0], 0, xopt[1], xopt[2], xopt[3] 465 ) 466 return xopt, fopt, psfit, (psfit_hdp_x, psfit_hdp) 467 else: 468 raise LookupError( 469 "resolving power is not defined, try to use set_max_resolving_power()" 470 ) 471 472 def voigt(self, oversample_multiplier=1, delta_rp=0, mz_overlay=1): 473 """[Legacy] Voigt lineshape analysis function 474 Legacy function for voigt lineshape analysis 475 476 Parameters 477 ---------- 478 oversample_multiplier : int 479 factor to increase x-axis points by for simulation of fitted lineshape function 480 delta_rp : float 481 delta resolving power to add to resolving power 482 mz_overlay : int 483 extra points left and right of peak definition to include in fitting 484 485 Returns 486 ------- 487 mz_domain : ndarray 488 x-axis domain for fit 489 calc_abundance : ndarray 490 calculated abundance profile based on voigt function 491 """ 492 493 if self.resolving_power: 494 # full width half maximum distance 495 self.fwhm = self.mz_exp / ( 496 self.resolving_power + delta_rp 497 ) # self.resolving_power) 498 499 # stardart deviation 500 sigma = self.fwhm / 3.6013 501 502 # half width baseline distance 503 504 # mz_domain = linspace(self.mz_exp - hw_base_distance, 505 # self.mz_exp + hw_base_distance, datapoint) 506 mz_domain = self.get_mz_domain(oversample_multiplier, mz_overlay) 507 508 # gaussian_pdf = lambda x0, x, s: (1/ math.sqrt(2*math.pi*math.pow(s,2))) * math.exp(-1 * math.pow(x-x0,2) / 2*math.pow(s,2) ) 509 510 # TODO derive amplitude 511 amplitude = (sqrt(2 * pi) * sigma) * self.abundance 512 513 model = models.VoigtModel() 514 515 params = model.make_params( 516 center=self.mz_exp, amplitude=amplitude, sigma=sigma, gamma=sigma 517 ) 518 519 calc_abundance = model.eval(params=params, x=mz_domain) 520 521 return mz_domain, calc_abundance 522 523 else: 524 raise LookupError( 525 "resolving power is not defined, try to use set_max_resolving_power()" 526 ) 527 528 def pseudovoigt( 529 self, oversample_multiplier=1, delta_rp=0, mz_overlay=1, fraction=0.5 530 ): 531 """[Legacy] pseudovoigt lineshape function 532 533 Legacy function for pseudovoigt lineshape analysis. 534 Note - Code may not be functional currently. 535 536 Parameters 537 ---------- 538 oversample_multiplier : int, optional 539 factor to increase x-axis points by for simulation of fitted lineshape function. Defaults to 1. 540 delta_rp : float, optional 541 delta resolving power to add to resolving power. Defaults to 0. 542 mz_overlay : int, optional 543 extra points left and right of peak definition to include in fitting. Defaults to 1. 544 fraction : float, optional 545 fraction of gaussian component in pseudovoigt function. Defaults to 0.5. 546 547 """ 548 if self.resolving_power: 549 # full width half maximum distance 550 self.fwhm = self.mz_exp / ( 551 self.resolving_power + delta_rp 552 ) # self.resolving_power) 553 554 # stardart deviation 555 sigma = self.fwhm / 2 556 557 # half width baseline distance 558 559 # mz_domain = linspace(self.mz_exp - hw_base_distance, 560 # self.mz_exp + hw_base_distance, datapoint) 561 mz_domain = self.get_mz_domain(oversample_multiplier, mz_overlay) 562 563 # gaussian_pdf = lambda x0, x, s: (1/ math.sqrt(2*math.pi*math.pow(s,2))) * math.exp(-1 * math.pow(x-x0,2) / 2*math.pow(s,2) ) 564 model = models.PseudoVoigtModel() 565 566 # TODO derive amplitude 567 gamma = sigma 568 569 amplitude = (sqrt(2 * pi) * sigma) * self.abundance 570 amplitude = (sqrt(pi / log(2)) * (pi * sigma * self.abundance)) / ( 571 (pi * (1 - gamma)) + (sqrt(pi * log(2)) * gamma) 572 ) 573 574 params = model.make_params(center=self.mz_exp, sigma=sigma) 575 576 calc_abundance = model.eval(params=params, x=mz_domain) 577 578 return mz_domain, calc_abundance 579 580 else: 581 raise LookupError( 582 "resolving power is not defined, try to use set_max_resolving_power()" 583 ) 584 585 def lorentz(self, oversample_multiplier=1, delta_rp=0, mz_overlay=1): 586 """[Legacy] Lorentz lineshape analysis function 587 588 Legacy function for lorentz lineshape analysis 589 590 Parameters 591 ---------- 592 oversample_multiplier : int 593 factor to increase x-axis points by for simulation of fitted lineshape function 594 delta_rp : float 595 delta resolving power to add to resolving power 596 mz_overlay : int 597 extra points left and right of peak definition to include in fitting 598 599 Returns 600 ------- 601 mz_domain : ndarray 602 x-axis domain for fit 603 calc_abundance : ndarray 604 calculated abundance profile based on lorentz function 605 606 """ 607 if self.resolving_power: 608 # full width half maximum distance 609 self.fwhm = self.mz_exp / ( 610 self.resolving_power + delta_rp 611 ) # self.resolving_power) 612 613 # stardart deviation 614 sigma = self.fwhm / 2 615 616 # half width baseline distance 617 hw_base_distance = 8 * sigma 618 619 # mz_domain = linspace(self.mz_exp - hw_base_distance, 620 # self.mz_exp + hw_base_distance, datapoint) 621 622 mz_domain = self.get_mz_domain(oversample_multiplier, mz_overlay) 623 # gaussian_pdf = lambda x0, x, s: (1/ math.sqrt(2*math.pi*math.pow(s,2))) * math.exp(-1 * math.pow(x-x0,2) / 2*math.pow(s,2) ) 624 model = models.LorentzianModel() 625 626 amplitude = sigma * pi * self.abundance 627 628 params = model.make_params( 629 center=self.mz_exp, amplitude=amplitude, sigma=sigma 630 ) 631 632 calc_abundance = model.eval(params=params, x=mz_domain) 633 634 return mz_domain, calc_abundance 635 636 else: 637 raise LookupError( 638 "resolving power is not defined, try to use set_max_resolving_power()" 639 ) 640 641 def gaussian(self, oversample_multiplier=1, delta_rp=0, mz_overlay=1): 642 """[Legacy] Gaussian lineshape analysis function 643 Legacy gaussian lineshape analysis function 644 645 Parameters 646 ---------- 647 oversample_multiplier : int 648 factor to increase x-axis points by for simulation of fitted lineshape function 649 delta_rp : float 650 delta resolving power to add to resolving power 651 mz_overlay : int 652 extra points left and right of peak definition to include in fitting 653 654 Returns 655 ------- 656 mz_domain : ndarray 657 x-axis domain for fit 658 calc_abundance : ndarray 659 calculated abundance profile based on gaussian function 660 661 662 """ 663 664 # check if MSPeak contains the resolving power info 665 if self.resolving_power: 666 # full width half maximum distance 667 self.fwhm = self.mz_exp / ( 668 self.resolving_power + delta_rp 669 ) # self.resolving_power) 670 671 # stardart deviation 672 sigma = self.fwhm / (2 * sqrt(2 * log(2))) 673 674 # half width baseline distance 675 # hw_base_distance = (3.2 * s) 676 677 # match_loz_factor = 3 678 679 # n_d = hw_base_distance * match_loz_factor 680 681 # mz_domain = linspace( 682 # self.mz_exp - n_d, self.mz_exp + n_d, datapoint) 683 684 mz_domain = self.get_mz_domain(oversample_multiplier, mz_overlay) 685 686 # gaussian_pdf = lambda x0, x, s: (1/ math.sqrt(2*math.pi*math.pow(s,2))) * math.exp(-1 * math.pow(x-x0,2) / 2*math.pow(s,2) ) 687 688 # calc_abundance = norm.pdf(mz_domain, self.mz_exp, s) 689 690 model = models.GaussianModel() 691 692 amplitude = (sqrt(2 * pi) * sigma) * self.abundance 693 694 params = model.make_params( 695 center=self.mz_exp, amplitude=amplitude, sigma=sigma 696 ) 697 698 calc_abundance = model.eval(params=params, x=mz_domain) 699 700 return mz_domain, calc_abundance 701 702 else: 703 raise LookupError( 704 "resolving power is not defined, try to use set_max_resolving_power()" 705 ) 706 707 def get_mz_domain(self, oversample_multiplier, mz_overlay): 708 """[Legacy] function to resample/interpolate datapoints for lineshape analysis 709 710 This code is used for the legacy line fitting functions and not recommended. 711 Legacy function to support expanding mz domain for legacy lineshape functions 712 713 Parameters 714 ---------- 715 oversample_multiplier : int 716 factor to increase x-axis points by for simulation of fitted lineshape function 717 mz_overlay : int 718 extra points left and right of peak definition to include in fitting 719 720 Returns 721 ------- 722 mz_domain : ndarray 723 x-axis domain for fit 724 725 """ 726 start_index = ( 727 self.peak_left_index - mz_overlay if not self.peak_left_index == 0 else 0 728 ) 729 final_index = ( 730 self.peak_right_index + mz_overlay 731 if not self.peak_right_index == len(self._ms_parent.mz_exp_profile) 732 else self.peak_right_index 733 ) 734 735 if oversample_multiplier == 1: 736 mz_domain = self._ms_parent.mz_exp_profile[start_index:final_index] 737 738 else: 739 # we assume a linear correlation for m/z and datapoits 740 # which is only true if the m/z range in narrow (within 1 m/z unit) 741 # this is not true for a wide m/z range 742 743 indexes = range(start_index, final_index + 1) 744 mz = self._ms_parent.mz_exp_profile[indexes] 745 pol = poly1d(polyfit(indexes, mz, 1)) 746 oversampled_indexes = linspace( 747 start_index, 748 final_index, 749 (final_index - start_index) * oversample_multiplier, 750 ) 751 mz_domain = pol(oversampled_indexes) 752 753 return mz_domain 754 755 @property 756 def number_possible_assignments( 757 self, 758 ): 759 return len(self.molecular_formulas) 760 761 def molecular_formula_lowest_error(self): 762 """Return the molecular formula with the smallest absolute mz error""" 763 764 return min(self.molecular_formulas, key=lambda m: abs(m.mz_error)) 765 766 def molecular_formula_highest_prob_score(self): 767 """Return the molecular formula with the highest confidence score score""" 768 769 return max(self.molecular_formulas, key=lambda m: abs(m.confidence_score)) 770 771 def molecular_formula_earth_filter(self, lowest_error=True): 772 """Filter molecular formula using the 'Earth' filter 773 774 This function applies the Formularity-esque 'Earth' filter to possible molecular formula assignments. 775 Earth Filter: 776 O > 0 AND N <= 3 AND P <= 2 AND 3P <= O 777 778 If the lowest_error method is also used, it will return the single formula annotation with the smallest absolute error which also fits the Earth filter. 779 Otherwise, it will return all Earth-filter compliant formulas. 780 781 Parameters 782 ---------- 783 lowest_error : bool, optional. 784 Return only the lowest error formula which also fits the Earth filter. 785 If False, return all Earth-filter compliant formulas. Default is True. 786 787 Returns 788 ------- 789 list 790 List of molecular formula objects which fit the Earth filter 791 792 References 793 ---------- 794 1. Nikola Tolic et al., "Formularity: Software for Automated Formula Assignment of Natural and Other Organic Matter from Ultrahigh-Resolution Mass Spectra" 795 Anal. Chem. 2017, 89, 23, 12659–12665 796 doi: 10.1021/acs.analchem.7b03318 797 """ 798 799 candidates = list( 800 filter( 801 lambda mf: mf.get("O") > 0 802 and mf.get("N") <= 3 803 and mf.get("P") <= 2 804 and (3 * mf.get("P")) <= mf.get("O"), 805 self.molecular_formulas, 806 ) 807 ) 808 if len(candidates) > 0: 809 if lowest_error: 810 return min(candidates, key=lambda m: abs(m.mz_error)) 811 else: 812 return candidates 813 else: 814 return candidates 815 816 def molecular_formula_water_filter(self, lowest_error=True): 817 """Filter molecular formula using the 'Water' filter 818 819 This function applies the Formularity-esque 'Water' filter to possible molecular formula assignments. 820 Water Filter: 821 O > 0 AND N <= 3 AND S <= 2 AND P <= 2 822 823 If the lowest_error method is also used, it will return the single formula annotation with the smallest absolute error which also fits the Water filter. 824 Otherwise, it will return all Water-filter compliant formulas. 825 826 Parameters 827 ---------- 828 lowest_error : bool, optional 829 Return only the lowest error formula which also fits the Water filter. 830 If False, return all Water-filter compliant formulas. Defaults to 2 831 832 Returns 833 ------- 834 list 835 List of molecular formula objects which fit the Water filter 836 837 References 838 ---------- 839 1. Nikola Tolic et al., "Formularity: Software for Automated Formula Assignment of Natural and Other Organic Matter from Ultrahigh-Resolution Mass Spectra" 840 Anal. Chem. 2017, 89, 23, 12659–12665 841 doi: 10.1021/acs.analchem.7b03318 842 """ 843 844 candidates = list( 845 filter( 846 lambda mf: mf.get("O") > 0 847 and mf.get("N") <= 3 848 and mf.get("S") <= 2 849 and mf.get("P") <= 2, 850 self.molecular_formulas, 851 ) 852 ) 853 if len(candidates) > 0: 854 if lowest_error: 855 return min(candidates, key=lambda m: abs(m.mz_error)) 856 else: 857 return candidates 858 else: 859 return candidates 860 861 def molecular_formula_air_filter(self, lowest_error=True): 862 """Filter molecular formula using the 'Air' filter 863 864 This function applies the Formularity-esque 'Air' filter to possible molecular formula assignments. 865 Air Filter: 866 O > 0 AND N <= 3 AND S <= 1 AND P = 0 AND 3(S+N) <= O 867 868 If the lowest_error method is also used, it will return the single formula annotation with the smallest absolute error which also fits the Air filter. 869 Otherwise, it will return all Air-filter compliant formulas. 870 871 Parameters 872 ---------- 873 lowest_error : bool, optional 874 Return only the lowest error formula which also fits the Air filter. 875 If False, return all Air-filter compliant formulas. Defaults to True. 876 877 Returns 878 ------- 879 list 880 List of molecular formula objects which fit the Air filter 881 882 References 883 ---------- 884 1. Nikola Tolic et al., "Formularity: Software for Automated Formula Assignment of Natural and Other Organic Matter from Ultrahigh-Resolution Mass Spectra" 885 Anal. Chem. 2017, 89, 23, 12659–12665 886 doi: 10.1021/acs.analchem.7b03318 887 """ 888 889 candidates = list( 890 filter( 891 lambda mf: mf.get("O") > 0 892 and mf.get("N") <= 2 893 and mf.get("S") <= 1 894 and mf.get("P") == 0 895 and 3 * (mf.get("S") + mf.get("N")) <= mf.get("O"), 896 self.molecular_formulas, 897 ) 898 ) 899 900 if len(candidates) > 0: 901 if lowest_error: 902 return min(candidates, key=lambda m: abs(m.mz_error)) 903 else: 904 return candidates 905 else: 906 return candidates 907 908 def cia_score_S_P_error(self): 909 """Compound Identification Algorithm SP Error - Assignment Filter 910 911 This function applies the Compound Identification Algorithm (CIA) SP Error filter to possible molecular formula assignments. 912 913 It takes the molecular formula with the lowest S+P count, and returns the formula with the lowest absolute error from this subset. 914 915 Returns 916 ------- 917 MolecularFormula 918 A single molecular formula which fits the rules of the CIA SP Error filter 919 920 921 References 922 ---------- 923 1. Elizabeth B. Kujawinski and Mark D. Behn, "Automated Analysis of Electrospray Ionization Fourier Transform Ion Cyclotron Resonance Mass Spectra of Natural Organic Matter" 924 Anal. Chem. 2006, 78, 13, 4363–4373 925 doi: 10.1021/ac0600306 926 """ 927 # case EFormulaScore.HAcap: 928 929 lowest_S_P_mf = min( 930 self.molecular_formulas, key=lambda mf: mf.get("S") + mf.get("P") 931 ) 932 lowest_S_P_count = lowest_S_P_mf.get("S") + lowest_S_P_mf.get("P") 933 934 list_same_s_p = list( 935 filter( 936 lambda mf: mf.get("S") + mf.get("P") == lowest_S_P_count, 937 self.molecular_formulas, 938 ) 939 ) 940 941 # check if list is not empty 942 if list_same_s_p: 943 return min(list_same_s_p, key=lambda m: abs(m.mz_error)) 944 945 else: 946 return lowest_S_P_mf 947 948 def cia_score_N_S_P_error(self): 949 """Compound Identification Algorithm NSP Error - Assignment Filter 950 951 This function applies the Compound Identification Algorithm (CIA) NSP Error filter to possible molecular formula assignments. 952 953 It takes the molecular formula with the lowest N+S+P count, and returns the formula with the lowest absolute error from this subset. 954 955 Returns 956 ------- 957 MolecularFormula 958 A single molecular formula which fits the rules of the CIA NSP Error filter 959 960 References 961 ---------- 962 1. Elizabeth B. Kujawinski and Mark D. Behn, "Automated Analysis of Electrospray Ionization Fourier Transform Ion Cyclotron Resonance Mass Spectra of Natural Organic Matter" 963 Anal. Chem. 2006, 78, 13, 4363–4373 964 doi: 10.1021/ac0600306 965 966 Raises 967 ------- 968 Exception 969 If no molecular formula are associated with mass spectrum peak. 970 """ 971 # case EFormulaScore.HAcap: 972 if self.molecular_formulas: 973 lowest_N_S_P_mf = min( 974 self.molecular_formulas, 975 key=lambda mf: mf.get("N") + mf.get("S") + mf.get("P"), 976 ) 977 lowest_N_S_P_count = ( 978 lowest_N_S_P_mf.get("N") 979 + lowest_N_S_P_mf.get("S") 980 + lowest_N_S_P_mf.get("P") 981 ) 982 983 list_same_N_S_P = list( 984 filter( 985 lambda mf: mf.get("N") + mf.get("S") + mf.get("P") 986 == lowest_N_S_P_count, 987 self.molecular_formulas, 988 ) 989 ) 990 991 if list_same_N_S_P: 992 SP_filtered_list = list( 993 filter( 994 lambda mf: (mf.get("S") <= 3) and (mf.get("P") <= 1), 995 list_same_N_S_P, 996 ) 997 ) 998 999 if SP_filtered_list: 1000 return min(SP_filtered_list, key=lambda m: abs(m.mz_error)) 1001 1002 else: 1003 return min(list_same_N_S_P, key=lambda m: abs(m.mz_error)) 1004 1005 else: 1006 return lowest_N_S_P_mf 1007 else: 1008 raise Exception( 1009 "No molecular formula associated with the mass spectrum peak at m/z: %.6f" 1010 % self.mz_exp 1011 )
Class to perform calculations on MSPeak objects.
This class provides methods to perform various calculations on MSPeak objects, such as calculating Kendrick Mass Defect (KMD) and Kendrick Mass (KM), calculating peak area, and fitting peak lineshape using different models.
Parameters
- None
Attributes
- _ms_parent (MSParent): The parent MSParent object associated with the MSPeakCalculation object.
- mz_exp (float): The experimental m/z value of the peak.
- peak_left_index (int): The start scan index of the peak.
- peak_right_index (int): The final scan index of the peak.
- resolving_power (float): The resolving power of the peak.
Methods
- _calc_kmd(dict_base). Calculate the Kendrick Mass Defect (KMD) and Kendrick Mass (KM) for a given base formula.
- calc_area(). Calculate the peak area using numpy's trapezoidal fit.
- fit_peak(mz_extend=6, delta_rp=0, model='Gaussian'). Perform lineshape analysis on a peak using lmfit module.
- voigt_pso(w, r, yoff, width, loc, a). Calculate the Voigt function for particle swarm optimization (PSO) fitting.
- objective_pso(x, w, u). Calculate the objective function for PSO fitting.
- minimize_pso(lower, upper, w, u). Minimize the objective function using the particle swarm optimization algorithm.
- fit_peak_pso(mz_extend=6, upsample_multiplier=5). Perform lineshape analysis on a peak using particle swarm optimization (PSO) fitting.
- voigt(oversample_multiplier=1, delta_rp=0, mz_overlay=1). [Legacy] Perform voigt lineshape analysis on a peak.
- pseudovoigt(oversample_multiplier=1, delta_rp=0, mz_overlay=1, fraction=0.5). [Legacy] Perform pseudovoigt lineshape analysis on a peak.
- lorentz(oversample_multiplier=1, delta_rp=0, mz_overlay=1). [Legacy] Perform lorentz lineshape analysis on a peak.
- gaussian(oversample_multiplier=1, delta_rp=0, mz_overlay=1). [Legacy] Perform gaussian lineshape analysis on a peak.
- get_mz_domain(oversample_multiplier, mz_overlay). [Legacy] Resample/interpolate datapoints for lineshape analysis.
- number_possible_assignments(). Return the number of possible molecular formula assignments for the peak.
- molecular_formula_lowest_error(). Return the molecular formula with the smallest absolute mz error.
- molecular_formula_highest_prob_score(). Return the molecular formula with the highest confidence score.
- molecular_formula_earth_filter(lowest_error=True). Filter molecular formula using the 'Earth' filter.
- molecular_formula_water_filter(lowest_error=True). Filter molecular formula using the 'Water' filter.
- molecular_formula_air_filter(lowest_error=True). Filter molecular formula using the 'Air' filter.
- cia_score_S_P_error(). Compound Identification Algorithm SP Error - Assignment Filter.
- cia_score_N_S_P_error(). Compound Identification Algorithm NSP Error - Assignment Filter.
148 def calc_area(self): 149 """Calculate the peak area using numpy's trapezoidal fit 150 151 uses provided mz_domain to accurately integrate areas independent of digital resolution 152 153 Returns 154 ------- 155 float 156 peak area 157 """ 158 if self.peak_right_index > self.peak_left_index: 159 yy = self._ms_parent.abundance_profile[ 160 self.peak_left_index : self.peak_right_index 161 ] 162 xx = self._ms_parent.mz_exp_profile[ 163 self.peak_left_index : self.peak_right_index 164 ] 165 # check if the axis is high to low m/z or not. if its MSFromFreq its high mz first, if its from Profile, its low mz first 166 if xx[0] > xx[-1]: 167 xx = flip(xx) 168 yy = flip(yy) 169 return float(trapezoid(yy, xx)) 170 171 else: 172 warnings.warn( 173 "Peak Area Calculation for m/z {} has failed".format(self.mz_exp) 174 ) 175 return nan
Calculate the peak area using numpy's trapezoidal fit
uses provided mz_domain to accurately integrate areas independent of digital resolution
Returns
- float: peak area
177 def fit_peak(self, mz_extend=6, delta_rp=0, model="Gaussian"): 178 """Lineshape analysis on a peak using lmfit module. 179 180 Model and fit peak lineshape by defined function - using lmfit module 181 Does not oversample/resample/interpolate data points 182 Better to go back to time domain and perform more zero filling - if possible. 183 184 Parameters 185 ---------- 186 mz_extend : int 187 extra points left and right of peak definition to include in fitting 188 delta_rp : float 189 delta resolving power to add to resolving power 190 model : str 191 Type of lineshape model to use. 192 Models allowed: Gaussian, Lorentz, Voigt 193 194 Returns 195 ----- 196 mz_domain : ndarray 197 x-axis domain for fit 198 fit_peak : lmfit object 199 fit results object from lmfit module 200 201 Notes 202 ----- 203 Returns the calculated mz domain, initial defined abundance profile, and the fit peak results object from lmfit module 204 mz_extend here extends the x-axis domain so that we have sufficient points either side of the apex to fit. 205 Takes about 10ms per peak 206 """ 207 start_index = ( 208 self.peak_left_index - mz_extend if not self.peak_left_index == 0 else 0 209 ) 210 final_index = ( 211 self.peak_right_index + mz_extend 212 if not self.peak_right_index == len(self._ms_parent.mz_exp_profile) 213 else self.peak_right_index 214 ) 215 216 # check if MSPeak contains the resolving power info 217 if self.resolving_power: 218 # full width half maximum distance 219 self.fwhm = self.mz_exp / (self.resolving_power + delta_rp) 220 221 mz_domain = self._ms_parent.mz_exp_profile[start_index:final_index] 222 abundance_domain = self._ms_parent.abundance_profile[ 223 start_index:final_index 224 ] 225 226 if model == "Gaussian": 227 # stardard deviation 228 sigma = self.fwhm / (2 * sqrt(2 * log(2))) 229 amplitude = (sqrt(2 * pi) * sigma) * self.abundance 230 model = models.GaussianModel() 231 params = model.make_params( 232 center=self.mz_exp, amplitude=amplitude, sigma=sigma 233 ) 234 235 elif model == "Lorentz": 236 # stardard deviation 237 sigma = self.fwhm / 2 238 amplitude = sigma * pi * self.abundance 239 model = models.LorentzianModel() 240 params = model.make_params( 241 center=self.mz_exp, amplitude=amplitude, sigma=sigma 242 ) 243 244 elif model == "Voigt": 245 # stardard deviation 246 sigma = self.fwhm / 3.6013 247 amplitude = (sqrt(2 * pi) * sigma) * self.abundance 248 model = models.VoigtModel() 249 params = model.make_params( 250 center=self.mz_exp, amplitude=amplitude, sigma=sigma, gamma=sigma 251 ) 252 else: 253 raise LookupError("model lineshape not known or defined") 254 255 # calc_abundance = model.eval(params=params, x=mz_domain) #Same as initial fit, returned in fit_peak object 256 fit_peak = model.fit(abundance_domain, params=params, x=mz_domain) 257 return mz_domain, fit_peak 258 259 else: 260 raise LookupError( 261 "resolving power is not defined, try to use set_max_resolving_power()" 262 )
Lineshape analysis on a peak using lmfit module.
Model and fit peak lineshape by defined function - using lmfit module Does not oversample/resample/interpolate data points Better to go back to time domain and perform more zero filling - if possible.
Parameters
- mz_extend (int): extra points left and right of peak definition to include in fitting
- delta_rp (float): delta resolving power to add to resolving power
- model (str): Type of lineshape model to use. Models allowed: Gaussian, Lorentz, Voigt
Returns
- mz_domain (ndarray): x-axis domain for fit
- fit_peak (lmfit object): fit results object from lmfit module
Notes
Returns the calculated mz domain, initial defined abundance profile, and the fit peak results object from lmfit module mz_extend here extends the x-axis domain so that we have sufficient points either side of the apex to fit. Takes about 10ms per peak
264 def voigt_pso(self, w, r, yoff, width, loc, a): 265 """Voigt function for particle swarm optimisation (PSO) fitting 266 267 From https://github.com/pnnl/nmrfit/blob/master/nmrfit/equations.py. 268 Calculates a Voigt function over w based on the relevant properties of the distribution. 269 270 Parameters 271 ---------- 272 w : ndarray 273 Array over which the Voigt function will be evaluated. 274 r : float 275 Ratio between the Guassian and Lorentzian functions. 276 yoff : float 277 Y-offset of the Voigt function. 278 width : float 279 The width of the Voigt function. 280 loc : float 281 Center of the Voigt function. 282 a : float 283 Area of the Voigt function. 284 Returns 285 ------- 286 V : ndarray 287 Array defining the Voigt function over w. 288 289 References 290 ---------- 291 1. https://github.com/pnnl/nmrfit 292 293 Notes 294 ----- 295 Particle swarm optimisation (PSO) fitting function can be significantly more computationally expensive than lmfit, with more parameters to optimise. 296 297 """ 298 # Lorentzian component 299 L = (2 / (pi * width)) * 1 / (1 + ((w - loc) / (0.5 * width)) ** 2) 300 301 # Gaussian component 302 G = ( 303 (2 / width) 304 * sqrt(log(2) / pi) 305 * exp(-(((w - loc) / (width / (2 * sqrt(log(2))))) ** 2)) 306 ) 307 308 # Voigt body 309 V = (yoff + a) * (r * L + (1 - r) * G) 310 311 return V
Voigt function for particle swarm optimisation (PSO) fitting
From https://github.com/pnnl/nmrfit/blob/master/nmrfit/equations.py. Calculates a Voigt function over w based on the relevant properties of the distribution.
Parameters
- w (ndarray): Array over which the Voigt function will be evaluated.
- r (float): Ratio between the Guassian and Lorentzian functions.
- yoff (float): Y-offset of the Voigt function.
- width (float): The width of the Voigt function.
- loc (float): Center of the Voigt function.
- a (float): Area of the Voigt function.
Returns
- V (ndarray): Array defining the Voigt function over w.
References
Notes
Particle swarm optimisation (PSO) fitting function can be significantly more computationally expensive than lmfit, with more parameters to optimise.
313 def objective_pso(self, x, w, u): 314 """Objective function for particle swarm optimisation (PSO) fitting 315 316 The objective function used to fit supplied data. Evaluates sum of squared differences between the fit and the data. 317 318 Parameters 319 ---------- 320 x : list of floats 321 Parameter vector. 322 w : ndarray 323 Array of frequency data. 324 u : ndarray 325 Array of data to be fit. 326 327 Returns 328 ------- 329 rmse : float 330 Root mean square error between the data and fit. 331 332 References 333 ---------- 334 1. https://github.com/pnnl/nmrfit 335 336 """ 337 # global parameters 338 r, width, loc, a = x 339 yoff = 0 340 341 # calculate fit for V 342 V_fit = self.voigt_pso(w, r, yoff, width, loc, a) 343 344 # real component RMSE 345 rmse = sqrt(square((u - V_fit)).mean(axis=None)) 346 347 # return the total RMSE 348 return rmse
Objective function for particle swarm optimisation (PSO) fitting
The objective function used to fit supplied data. Evaluates sum of squared differences between the fit and the data.
Parameters
- x (list of floats): Parameter vector.
- w (ndarray): Array of frequency data.
- u (ndarray): Array of data to be fit.
Returns
- rmse (float): Root mean square error between the data and fit.
References
350 def minimize_pso(self, lower, upper, w, u): 351 """Minimization function for particle swarm optimisation (PSO) fitting 352 353 Minimizes the objective function using the particle swarm optimization algorithm. 354 Minimization function based on defined parameters 355 356 357 Parameters 358 ---------- 359 lower : list of floats 360 Lower bounds for the parameters. 361 upper : list of floats 362 Upper bounds for the parameters. 363 w : ndarray 364 Array of frequency data. 365 u : ndarray 366 Array of data to be fit. 367 368 Notes 369 ----- 370 Particle swarm optimisation (PSO) fitting function can be significantly more computationally expensive than lmfit, with more parameters to optimise. 371 Current parameters take ~2 seconds per peak. 372 373 374 References 375 ---------- 376 1. https://github.com/pnnl/nmrfit 377 378 """ 379 # TODO - allow support to pass swarmsize, maxiter, omega, phip, phig parameters. 380 # TODO - Refactor PSO fitting into its own class? 381 382 xopt, fopt = pyswarm.pso( 383 self.objective_pso, 384 lower, 385 upper, 386 args=(w, u), 387 swarmsize=1000, 388 maxiter=5000, 389 omega=-0.2134, 390 phip=-0.3344, 391 phig=2.3259, 392 ) 393 return xopt, fopt
Minimization function for particle swarm optimisation (PSO) fitting
Minimizes the objective function using the particle swarm optimization algorithm. Minimization function based on defined parameters
Parameters
- lower (list of floats): Lower bounds for the parameters.
- upper (list of floats): Upper bounds for the parameters.
- w (ndarray): Array of frequency data.
- u (ndarray): Array of data to be fit.
Notes
Particle swarm optimisation (PSO) fitting function can be significantly more computationally expensive than lmfit, with more parameters to optimise. Current parameters take ~2 seconds per peak.
References
395 def fit_peak_pso(self, mz_extend: int = 6, upsample_multiplier: int = 5): 396 """Lineshape analysis on a peak using particle swarm optimisation (PSO) fitting 397 398 Function to fit a Voigt peakshape using particle swarm optimisation (PSO). 399 Should return better results than lmfit, but much more computationally expensive 400 401 Parameters 402 ---------- 403 mz_extend : int, optional 404 extra points left and right of peak definition to include in fitting. Defaults to 6. 405 upsample_multiplier : int, optional 406 factor to increase x-axis points by for simulation of fitted lineshape function. Defaults to 5. 407 408 Returns 409 ------- 410 xopt : array 411 variables describing the voigt function. 412 G/L ratio, width (fwhm), apex (x-axis), area. 413 y-axis offset is fixed at 0 414 fopt : float 415 objective score (rmse) 416 psfit : array 417 recalculated y values based on function and optimised fit 418 psfit_hdp : tuple of arrays 419 0 - linspace x-axis upsampled grid 420 1 - recalculated y values based on function and upsampled x-axis grid 421 Does not change results, but aids in visualisation of the 'true' voigt lineshape 422 423 Notes 424 ----- 425 Particle swarm optimisation (PSO) fitting function can be significantly more computationally expensive than lmfit, with more parameters to optimise. 426 """ 427 # TODO - Add ability to pass pso args (i.e. swarm size, maxiter, omega, phig, etc) 428 # TODO: fix xopt. Magnitude mode data through CoreMS/Bruker starts at 0 but is noise centered well above 0. 429 # Thermo data is noise reduced by also noise subtracted, so starts at 0 430 # Absorption mode/phased data will have positive and negative components and may not be baseline corrected 431 432 start_index = ( 433 self.peak_left_index - mz_extend if not self.peak_left_index == 0 else 0 434 ) 435 final_index = ( 436 self.peak_right_index + mz_extend 437 if not self.peak_right_index == len(self._ms_parent.mz_exp_profile) 438 else self.peak_right_index 439 ) 440 441 # check if MSPeak contains the resolving power info 442 if self.resolving_power: 443 # full width half maximum distance 444 self.fwhm = self.mz_exp / (self.resolving_power) 445 446 mz_domain = self._ms_parent.mz_exp_profile[start_index:final_index] 447 abundance_domain = self._ms_parent.abundance_profile[ 448 start_index:final_index 449 ] 450 lower = [0, self.fwhm * 0.8, (self.mz_exp - 0.0005), 0] 451 upper = [ 452 1, 453 self.fwhm * 1.2, 454 (self.mz_exp + 0.0005), 455 self.abundance / self.signal_to_noise, 456 ] 457 xopt, fopt = self.minimize_pso(lower, upper, mz_domain, abundance_domain) 458 459 psfit = self.voigt_pso(mz_domain, xopt[0], 0, xopt[1], xopt[2], xopt[3]) 460 psfit_hdp_x = linspace( 461 min(mz_domain), max(mz_domain), num=len(mz_domain) * upsample_multiplier 462 ) 463 psfit_hdp = self.voigt_pso( 464 psfit_hdp_x, xopt[0], 0, xopt[1], xopt[2], xopt[3] 465 ) 466 return xopt, fopt, psfit, (psfit_hdp_x, psfit_hdp) 467 else: 468 raise LookupError( 469 "resolving power is not defined, try to use set_max_resolving_power()" 470 )
Lineshape analysis on a peak using particle swarm optimisation (PSO) fitting
Function to fit a Voigt peakshape using particle swarm optimisation (PSO). Should return better results than lmfit, but much more computationally expensive
Parameters
- mz_extend (int, optional): extra points left and right of peak definition to include in fitting. Defaults to 6.
- upsample_multiplier (int, optional): factor to increase x-axis points by for simulation of fitted lineshape function. Defaults to 5.
Returns
- xopt (array): variables describing the voigt function. G/L ratio, width (fwhm), apex (x-axis), area. y-axis offset is fixed at 0
- fopt (float): objective score (rmse)
- psfit (array): recalculated y values based on function and optimised fit
- psfit_hdp (tuple of arrays): 0 - linspace x-axis upsampled grid 1 - recalculated y values based on function and upsampled x-axis grid Does not change results, but aids in visualisation of the 'true' voigt lineshape
Notes
Particle swarm optimisation (PSO) fitting function can be significantly more computationally expensive than lmfit, with more parameters to optimise.
472 def voigt(self, oversample_multiplier=1, delta_rp=0, mz_overlay=1): 473 """[Legacy] Voigt lineshape analysis function 474 Legacy function for voigt lineshape analysis 475 476 Parameters 477 ---------- 478 oversample_multiplier : int 479 factor to increase x-axis points by for simulation of fitted lineshape function 480 delta_rp : float 481 delta resolving power to add to resolving power 482 mz_overlay : int 483 extra points left and right of peak definition to include in fitting 484 485 Returns 486 ------- 487 mz_domain : ndarray 488 x-axis domain for fit 489 calc_abundance : ndarray 490 calculated abundance profile based on voigt function 491 """ 492 493 if self.resolving_power: 494 # full width half maximum distance 495 self.fwhm = self.mz_exp / ( 496 self.resolving_power + delta_rp 497 ) # self.resolving_power) 498 499 # stardart deviation 500 sigma = self.fwhm / 3.6013 501 502 # half width baseline distance 503 504 # mz_domain = linspace(self.mz_exp - hw_base_distance, 505 # self.mz_exp + hw_base_distance, datapoint) 506 mz_domain = self.get_mz_domain(oversample_multiplier, mz_overlay) 507 508 # gaussian_pdf = lambda x0, x, s: (1/ math.sqrt(2*math.pi*math.pow(s,2))) * math.exp(-1 * math.pow(x-x0,2) / 2*math.pow(s,2) ) 509 510 # TODO derive amplitude 511 amplitude = (sqrt(2 * pi) * sigma) * self.abundance 512 513 model = models.VoigtModel() 514 515 params = model.make_params( 516 center=self.mz_exp, amplitude=amplitude, sigma=sigma, gamma=sigma 517 ) 518 519 calc_abundance = model.eval(params=params, x=mz_domain) 520 521 return mz_domain, calc_abundance 522 523 else: 524 raise LookupError( 525 "resolving power is not defined, try to use set_max_resolving_power()" 526 )
[Legacy] Voigt lineshape analysis function Legacy function for voigt lineshape analysis
Parameters
- oversample_multiplier (int): factor to increase x-axis points by for simulation of fitted lineshape function
- delta_rp (float): delta resolving power to add to resolving power
- mz_overlay (int): extra points left and right of peak definition to include in fitting
Returns
- mz_domain (ndarray): x-axis domain for fit
- calc_abundance (ndarray): calculated abundance profile based on voigt function
528 def pseudovoigt( 529 self, oversample_multiplier=1, delta_rp=0, mz_overlay=1, fraction=0.5 530 ): 531 """[Legacy] pseudovoigt lineshape function 532 533 Legacy function for pseudovoigt lineshape analysis. 534 Note - Code may not be functional currently. 535 536 Parameters 537 ---------- 538 oversample_multiplier : int, optional 539 factor to increase x-axis points by for simulation of fitted lineshape function. Defaults to 1. 540 delta_rp : float, optional 541 delta resolving power to add to resolving power. Defaults to 0. 542 mz_overlay : int, optional 543 extra points left and right of peak definition to include in fitting. Defaults to 1. 544 fraction : float, optional 545 fraction of gaussian component in pseudovoigt function. Defaults to 0.5. 546 547 """ 548 if self.resolving_power: 549 # full width half maximum distance 550 self.fwhm = self.mz_exp / ( 551 self.resolving_power + delta_rp 552 ) # self.resolving_power) 553 554 # stardart deviation 555 sigma = self.fwhm / 2 556 557 # half width baseline distance 558 559 # mz_domain = linspace(self.mz_exp - hw_base_distance, 560 # self.mz_exp + hw_base_distance, datapoint) 561 mz_domain = self.get_mz_domain(oversample_multiplier, mz_overlay) 562 563 # gaussian_pdf = lambda x0, x, s: (1/ math.sqrt(2*math.pi*math.pow(s,2))) * math.exp(-1 * math.pow(x-x0,2) / 2*math.pow(s,2) ) 564 model = models.PseudoVoigtModel() 565 566 # TODO derive amplitude 567 gamma = sigma 568 569 amplitude = (sqrt(2 * pi) * sigma) * self.abundance 570 amplitude = (sqrt(pi / log(2)) * (pi * sigma * self.abundance)) / ( 571 (pi * (1 - gamma)) + (sqrt(pi * log(2)) * gamma) 572 ) 573 574 params = model.make_params(center=self.mz_exp, sigma=sigma) 575 576 calc_abundance = model.eval(params=params, x=mz_domain) 577 578 return mz_domain, calc_abundance 579 580 else: 581 raise LookupError( 582 "resolving power is not defined, try to use set_max_resolving_power()" 583 )
[Legacy] pseudovoigt lineshape function
Legacy function for pseudovoigt lineshape analysis. Note - Code may not be functional currently.
Parameters
- oversample_multiplier (int, optional): factor to increase x-axis points by for simulation of fitted lineshape function. Defaults to 1.
- delta_rp (float, optional): delta resolving power to add to resolving power. Defaults to 0.
- mz_overlay (int, optional): extra points left and right of peak definition to include in fitting. Defaults to 1.
- fraction (float, optional): fraction of gaussian component in pseudovoigt function. Defaults to 0.5.
585 def lorentz(self, oversample_multiplier=1, delta_rp=0, mz_overlay=1): 586 """[Legacy] Lorentz lineshape analysis function 587 588 Legacy function for lorentz lineshape analysis 589 590 Parameters 591 ---------- 592 oversample_multiplier : int 593 factor to increase x-axis points by for simulation of fitted lineshape function 594 delta_rp : float 595 delta resolving power to add to resolving power 596 mz_overlay : int 597 extra points left and right of peak definition to include in fitting 598 599 Returns 600 ------- 601 mz_domain : ndarray 602 x-axis domain for fit 603 calc_abundance : ndarray 604 calculated abundance profile based on lorentz function 605 606 """ 607 if self.resolving_power: 608 # full width half maximum distance 609 self.fwhm = self.mz_exp / ( 610 self.resolving_power + delta_rp 611 ) # self.resolving_power) 612 613 # stardart deviation 614 sigma = self.fwhm / 2 615 616 # half width baseline distance 617 hw_base_distance = 8 * sigma 618 619 # mz_domain = linspace(self.mz_exp - hw_base_distance, 620 # self.mz_exp + hw_base_distance, datapoint) 621 622 mz_domain = self.get_mz_domain(oversample_multiplier, mz_overlay) 623 # gaussian_pdf = lambda x0, x, s: (1/ math.sqrt(2*math.pi*math.pow(s,2))) * math.exp(-1 * math.pow(x-x0,2) / 2*math.pow(s,2) ) 624 model = models.LorentzianModel() 625 626 amplitude = sigma * pi * self.abundance 627 628 params = model.make_params( 629 center=self.mz_exp, amplitude=amplitude, sigma=sigma 630 ) 631 632 calc_abundance = model.eval(params=params, x=mz_domain) 633 634 return mz_domain, calc_abundance 635 636 else: 637 raise LookupError( 638 "resolving power is not defined, try to use set_max_resolving_power()" 639 )
[Legacy] Lorentz lineshape analysis function
Legacy function for lorentz lineshape analysis
Parameters
- oversample_multiplier (int): factor to increase x-axis points by for simulation of fitted lineshape function
- delta_rp (float): delta resolving power to add to resolving power
- mz_overlay (int): extra points left and right of peak definition to include in fitting
Returns
- mz_domain (ndarray): x-axis domain for fit
- calc_abundance (ndarray): calculated abundance profile based on lorentz function
641 def gaussian(self, oversample_multiplier=1, delta_rp=0, mz_overlay=1): 642 """[Legacy] Gaussian lineshape analysis function 643 Legacy gaussian lineshape analysis function 644 645 Parameters 646 ---------- 647 oversample_multiplier : int 648 factor to increase x-axis points by for simulation of fitted lineshape function 649 delta_rp : float 650 delta resolving power to add to resolving power 651 mz_overlay : int 652 extra points left and right of peak definition to include in fitting 653 654 Returns 655 ------- 656 mz_domain : ndarray 657 x-axis domain for fit 658 calc_abundance : ndarray 659 calculated abundance profile based on gaussian function 660 661 662 """ 663 664 # check if MSPeak contains the resolving power info 665 if self.resolving_power: 666 # full width half maximum distance 667 self.fwhm = self.mz_exp / ( 668 self.resolving_power + delta_rp 669 ) # self.resolving_power) 670 671 # stardart deviation 672 sigma = self.fwhm / (2 * sqrt(2 * log(2))) 673 674 # half width baseline distance 675 # hw_base_distance = (3.2 * s) 676 677 # match_loz_factor = 3 678 679 # n_d = hw_base_distance * match_loz_factor 680 681 # mz_domain = linspace( 682 # self.mz_exp - n_d, self.mz_exp + n_d, datapoint) 683 684 mz_domain = self.get_mz_domain(oversample_multiplier, mz_overlay) 685 686 # gaussian_pdf = lambda x0, x, s: (1/ math.sqrt(2*math.pi*math.pow(s,2))) * math.exp(-1 * math.pow(x-x0,2) / 2*math.pow(s,2) ) 687 688 # calc_abundance = norm.pdf(mz_domain, self.mz_exp, s) 689 690 model = models.GaussianModel() 691 692 amplitude = (sqrt(2 * pi) * sigma) * self.abundance 693 694 params = model.make_params( 695 center=self.mz_exp, amplitude=amplitude, sigma=sigma 696 ) 697 698 calc_abundance = model.eval(params=params, x=mz_domain) 699 700 return mz_domain, calc_abundance 701 702 else: 703 raise LookupError( 704 "resolving power is not defined, try to use set_max_resolving_power()" 705 )
[Legacy] Gaussian lineshape analysis function Legacy gaussian lineshape analysis function
Parameters
- oversample_multiplier (int): factor to increase x-axis points by for simulation of fitted lineshape function
- delta_rp (float): delta resolving power to add to resolving power
- mz_overlay (int): extra points left and right of peak definition to include in fitting
Returns
- mz_domain (ndarray): x-axis domain for fit
- calc_abundance (ndarray): calculated abundance profile based on gaussian function
707 def get_mz_domain(self, oversample_multiplier, mz_overlay): 708 """[Legacy] function to resample/interpolate datapoints for lineshape analysis 709 710 This code is used for the legacy line fitting functions and not recommended. 711 Legacy function to support expanding mz domain for legacy lineshape functions 712 713 Parameters 714 ---------- 715 oversample_multiplier : int 716 factor to increase x-axis points by for simulation of fitted lineshape function 717 mz_overlay : int 718 extra points left and right of peak definition to include in fitting 719 720 Returns 721 ------- 722 mz_domain : ndarray 723 x-axis domain for fit 724 725 """ 726 start_index = ( 727 self.peak_left_index - mz_overlay if not self.peak_left_index == 0 else 0 728 ) 729 final_index = ( 730 self.peak_right_index + mz_overlay 731 if not self.peak_right_index == len(self._ms_parent.mz_exp_profile) 732 else self.peak_right_index 733 ) 734 735 if oversample_multiplier == 1: 736 mz_domain = self._ms_parent.mz_exp_profile[start_index:final_index] 737 738 else: 739 # we assume a linear correlation for m/z and datapoits 740 # which is only true if the m/z range in narrow (within 1 m/z unit) 741 # this is not true for a wide m/z range 742 743 indexes = range(start_index, final_index + 1) 744 mz = self._ms_parent.mz_exp_profile[indexes] 745 pol = poly1d(polyfit(indexes, mz, 1)) 746 oversampled_indexes = linspace( 747 start_index, 748 final_index, 749 (final_index - start_index) * oversample_multiplier, 750 ) 751 mz_domain = pol(oversampled_indexes) 752 753 return mz_domain
[Legacy] function to resample/interpolate datapoints for lineshape analysis
This code is used for the legacy line fitting functions and not recommended. Legacy function to support expanding mz domain for legacy lineshape functions
Parameters
- oversample_multiplier (int): factor to increase x-axis points by for simulation of fitted lineshape function
- mz_overlay (int): extra points left and right of peak definition to include in fitting
Returns
- mz_domain (ndarray): x-axis domain for fit
761 def molecular_formula_lowest_error(self): 762 """Return the molecular formula with the smallest absolute mz error""" 763 764 return min(self.molecular_formulas, key=lambda m: abs(m.mz_error))
Return the molecular formula with the smallest absolute mz error
766 def molecular_formula_highest_prob_score(self): 767 """Return the molecular formula with the highest confidence score score""" 768 769 return max(self.molecular_formulas, key=lambda m: abs(m.confidence_score))
Return the molecular formula with the highest confidence score score
771 def molecular_formula_earth_filter(self, lowest_error=True): 772 """Filter molecular formula using the 'Earth' filter 773 774 This function applies the Formularity-esque 'Earth' filter to possible molecular formula assignments. 775 Earth Filter: 776 O > 0 AND N <= 3 AND P <= 2 AND 3P <= O 777 778 If the lowest_error method is also used, it will return the single formula annotation with the smallest absolute error which also fits the Earth filter. 779 Otherwise, it will return all Earth-filter compliant formulas. 780 781 Parameters 782 ---------- 783 lowest_error : bool, optional. 784 Return only the lowest error formula which also fits the Earth filter. 785 If False, return all Earth-filter compliant formulas. Default is True. 786 787 Returns 788 ------- 789 list 790 List of molecular formula objects which fit the Earth filter 791 792 References 793 ---------- 794 1. Nikola Tolic et al., "Formularity: Software for Automated Formula Assignment of Natural and Other Organic Matter from Ultrahigh-Resolution Mass Spectra" 795 Anal. Chem. 2017, 89, 23, 12659–12665 796 doi: 10.1021/acs.analchem.7b03318 797 """ 798 799 candidates = list( 800 filter( 801 lambda mf: mf.get("O") > 0 802 and mf.get("N") <= 3 803 and mf.get("P") <= 2 804 and (3 * mf.get("P")) <= mf.get("O"), 805 self.molecular_formulas, 806 ) 807 ) 808 if len(candidates) > 0: 809 if lowest_error: 810 return min(candidates, key=lambda m: abs(m.mz_error)) 811 else: 812 return candidates 813 else: 814 return candidates
Filter molecular formula using the 'Earth' filter
This function applies the Formularity-esque 'Earth' filter to possible molecular formula assignments. Earth Filter: O > 0 AND N <= 3 AND P <= 2 AND 3P <= O
If the lowest_error method is also used, it will return the single formula annotation with the smallest absolute error which also fits the Earth filter. Otherwise, it will return all Earth-filter compliant formulas.
Parameters
- lowest_error (bool, optional.): Return only the lowest error formula which also fits the Earth filter. If False, return all Earth-filter compliant formulas. Default is True.
Returns
- list: List of molecular formula objects which fit the Earth filter
References
- Nikola Tolic et al., "Formularity: Software for Automated Formula Assignment of Natural and Other Organic Matter from Ultrahigh-Resolution Mass Spectra" Anal. Chem. 2017, 89, 23, 12659–12665 doi: 10.1021/acs.analchem.7b03318
816 def molecular_formula_water_filter(self, lowest_error=True): 817 """Filter molecular formula using the 'Water' filter 818 819 This function applies the Formularity-esque 'Water' filter to possible molecular formula assignments. 820 Water Filter: 821 O > 0 AND N <= 3 AND S <= 2 AND P <= 2 822 823 If the lowest_error method is also used, it will return the single formula annotation with the smallest absolute error which also fits the Water filter. 824 Otherwise, it will return all Water-filter compliant formulas. 825 826 Parameters 827 ---------- 828 lowest_error : bool, optional 829 Return only the lowest error formula which also fits the Water filter. 830 If False, return all Water-filter compliant formulas. Defaults to 2 831 832 Returns 833 ------- 834 list 835 List of molecular formula objects which fit the Water filter 836 837 References 838 ---------- 839 1. Nikola Tolic et al., "Formularity: Software for Automated Formula Assignment of Natural and Other Organic Matter from Ultrahigh-Resolution Mass Spectra" 840 Anal. Chem. 2017, 89, 23, 12659–12665 841 doi: 10.1021/acs.analchem.7b03318 842 """ 843 844 candidates = list( 845 filter( 846 lambda mf: mf.get("O") > 0 847 and mf.get("N") <= 3 848 and mf.get("S") <= 2 849 and mf.get("P") <= 2, 850 self.molecular_formulas, 851 ) 852 ) 853 if len(candidates) > 0: 854 if lowest_error: 855 return min(candidates, key=lambda m: abs(m.mz_error)) 856 else: 857 return candidates 858 else: 859 return candidates
Filter molecular formula using the 'Water' filter
This function applies the Formularity-esque 'Water' filter to possible molecular formula assignments. Water Filter: O > 0 AND N <= 3 AND S <= 2 AND P <= 2
If the lowest_error method is also used, it will return the single formula annotation with the smallest absolute error which also fits the Water filter. Otherwise, it will return all Water-filter compliant formulas.
Parameters
- lowest_error (bool, optional): Return only the lowest error formula which also fits the Water filter. If False, return all Water-filter compliant formulas. Defaults to 2
Returns
- list: List of molecular formula objects which fit the Water filter
References
- Nikola Tolic et al., "Formularity: Software for Automated Formula Assignment of Natural and Other Organic Matter from Ultrahigh-Resolution Mass Spectra" Anal. Chem. 2017, 89, 23, 12659–12665 doi: 10.1021/acs.analchem.7b03318
861 def molecular_formula_air_filter(self, lowest_error=True): 862 """Filter molecular formula using the 'Air' filter 863 864 This function applies the Formularity-esque 'Air' filter to possible molecular formula assignments. 865 Air Filter: 866 O > 0 AND N <= 3 AND S <= 1 AND P = 0 AND 3(S+N) <= O 867 868 If the lowest_error method is also used, it will return the single formula annotation with the smallest absolute error which also fits the Air filter. 869 Otherwise, it will return all Air-filter compliant formulas. 870 871 Parameters 872 ---------- 873 lowest_error : bool, optional 874 Return only the lowest error formula which also fits the Air filter. 875 If False, return all Air-filter compliant formulas. Defaults to True. 876 877 Returns 878 ------- 879 list 880 List of molecular formula objects which fit the Air filter 881 882 References 883 ---------- 884 1. Nikola Tolic et al., "Formularity: Software for Automated Formula Assignment of Natural and Other Organic Matter from Ultrahigh-Resolution Mass Spectra" 885 Anal. Chem. 2017, 89, 23, 12659–12665 886 doi: 10.1021/acs.analchem.7b03318 887 """ 888 889 candidates = list( 890 filter( 891 lambda mf: mf.get("O") > 0 892 and mf.get("N") <= 2 893 and mf.get("S") <= 1 894 and mf.get("P") == 0 895 and 3 * (mf.get("S") + mf.get("N")) <= mf.get("O"), 896 self.molecular_formulas, 897 ) 898 ) 899 900 if len(candidates) > 0: 901 if lowest_error: 902 return min(candidates, key=lambda m: abs(m.mz_error)) 903 else: 904 return candidates 905 else: 906 return candidates
Filter molecular formula using the 'Air' filter
This function applies the Formularity-esque 'Air' filter to possible molecular formula assignments. Air Filter: O > 0 AND N <= 3 AND S <= 1 AND P = 0 AND 3(S+N) <= O
If the lowest_error method is also used, it will return the single formula annotation with the smallest absolute error which also fits the Air filter. Otherwise, it will return all Air-filter compliant formulas.
Parameters
- lowest_error (bool, optional): Return only the lowest error formula which also fits the Air filter. If False, return all Air-filter compliant formulas. Defaults to True.
Returns
- list: List of molecular formula objects which fit the Air filter
References
- Nikola Tolic et al., "Formularity: Software for Automated Formula Assignment of Natural and Other Organic Matter from Ultrahigh-Resolution Mass Spectra" Anal. Chem. 2017, 89, 23, 12659–12665 doi: 10.1021/acs.analchem.7b03318
908 def cia_score_S_P_error(self): 909 """Compound Identification Algorithm SP Error - Assignment Filter 910 911 This function applies the Compound Identification Algorithm (CIA) SP Error filter to possible molecular formula assignments. 912 913 It takes the molecular formula with the lowest S+P count, and returns the formula with the lowest absolute error from this subset. 914 915 Returns 916 ------- 917 MolecularFormula 918 A single molecular formula which fits the rules of the CIA SP Error filter 919 920 921 References 922 ---------- 923 1. Elizabeth B. Kujawinski and Mark D. Behn, "Automated Analysis of Electrospray Ionization Fourier Transform Ion Cyclotron Resonance Mass Spectra of Natural Organic Matter" 924 Anal. Chem. 2006, 78, 13, 4363–4373 925 doi: 10.1021/ac0600306 926 """ 927 # case EFormulaScore.HAcap: 928 929 lowest_S_P_mf = min( 930 self.molecular_formulas, key=lambda mf: mf.get("S") + mf.get("P") 931 ) 932 lowest_S_P_count = lowest_S_P_mf.get("S") + lowest_S_P_mf.get("P") 933 934 list_same_s_p = list( 935 filter( 936 lambda mf: mf.get("S") + mf.get("P") == lowest_S_P_count, 937 self.molecular_formulas, 938 ) 939 ) 940 941 # check if list is not empty 942 if list_same_s_p: 943 return min(list_same_s_p, key=lambda m: abs(m.mz_error)) 944 945 else: 946 return lowest_S_P_mf
Compound Identification Algorithm SP Error - Assignment Filter
This function applies the Compound Identification Algorithm (CIA) SP Error filter to possible molecular formula assignments.
It takes the molecular formula with the lowest S+P count, and returns the formula with the lowest absolute error from this subset.
Returns
- MolecularFormula: A single molecular formula which fits the rules of the CIA SP Error filter
References
- Elizabeth B. Kujawinski and Mark D. Behn, "Automated Analysis of Electrospray Ionization Fourier Transform Ion Cyclotron Resonance Mass Spectra of Natural Organic Matter" Anal. Chem. 2006, 78, 13, 4363–4373 doi: 10.1021/ac0600306
948 def cia_score_N_S_P_error(self): 949 """Compound Identification Algorithm NSP Error - Assignment Filter 950 951 This function applies the Compound Identification Algorithm (CIA) NSP Error filter to possible molecular formula assignments. 952 953 It takes the molecular formula with the lowest N+S+P count, and returns the formula with the lowest absolute error from this subset. 954 955 Returns 956 ------- 957 MolecularFormula 958 A single molecular formula which fits the rules of the CIA NSP Error filter 959 960 References 961 ---------- 962 1. Elizabeth B. Kujawinski and Mark D. Behn, "Automated Analysis of Electrospray Ionization Fourier Transform Ion Cyclotron Resonance Mass Spectra of Natural Organic Matter" 963 Anal. Chem. 2006, 78, 13, 4363–4373 964 doi: 10.1021/ac0600306 965 966 Raises 967 ------- 968 Exception 969 If no molecular formula are associated with mass spectrum peak. 970 """ 971 # case EFormulaScore.HAcap: 972 if self.molecular_formulas: 973 lowest_N_S_P_mf = min( 974 self.molecular_formulas, 975 key=lambda mf: mf.get("N") + mf.get("S") + mf.get("P"), 976 ) 977 lowest_N_S_P_count = ( 978 lowest_N_S_P_mf.get("N") 979 + lowest_N_S_P_mf.get("S") 980 + lowest_N_S_P_mf.get("P") 981 ) 982 983 list_same_N_S_P = list( 984 filter( 985 lambda mf: mf.get("N") + mf.get("S") + mf.get("P") 986 == lowest_N_S_P_count, 987 self.molecular_formulas, 988 ) 989 ) 990 991 if list_same_N_S_P: 992 SP_filtered_list = list( 993 filter( 994 lambda mf: (mf.get("S") <= 3) and (mf.get("P") <= 1), 995 list_same_N_S_P, 996 ) 997 ) 998 999 if SP_filtered_list: 1000 return min(SP_filtered_list, key=lambda m: abs(m.mz_error)) 1001 1002 else: 1003 return min(list_same_N_S_P, key=lambda m: abs(m.mz_error)) 1004 1005 else: 1006 return lowest_N_S_P_mf 1007 else: 1008 raise Exception( 1009 "No molecular formula associated with the mass spectrum peak at m/z: %.6f" 1010 % self.mz_exp 1011 )
Compound Identification Algorithm NSP Error - Assignment Filter
This function applies the Compound Identification Algorithm (CIA) NSP Error filter to possible molecular formula assignments.
It takes the molecular formula with the lowest N+S+P count, and returns the formula with the lowest absolute error from this subset.
Returns
- MolecularFormula: A single molecular formula which fits the rules of the CIA NSP Error filter
References
- Elizabeth B. Kujawinski and Mark D. Behn, "Automated Analysis of Electrospray Ionization Fourier Transform Ion Cyclotron Resonance Mass Spectra of Natural Organic Matter" Anal. Chem. 2006, 78, 13, 4363–4373 doi: 10.1021/ac0600306
Raises
- Exception: If no molecular formula are associated with mass spectrum peak.