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            )
class MSPeakCalculation:
  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.
def calc_area(self):
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
def fit_peak(self, mz_extend=6, delta_rp=0, model='Gaussian'):
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

def voigt_pso(self, w, r, yoff, width, loc, a):
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
  1. https://github.com/pnnl/nmrfit
Notes

Particle swarm optimisation (PSO) fitting function can be significantly more computationally expensive than lmfit, with more parameters to optimise.

def objective_pso(self, x, w, u):
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
  1. https://github.com/pnnl/nmrfit
def minimize_pso(self, lower, upper, w, u):
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
  1. https://github.com/pnnl/nmrfit
def fit_peak_pso(self, mz_extend: int = 6, upsample_multiplier: int = 5):
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.

def voigt(self, oversample_multiplier=1, delta_rp=0, mz_overlay=1):
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
def pseudovoigt( self, oversample_multiplier=1, delta_rp=0, mz_overlay=1, fraction=0.5):
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.
def lorentz(self, oversample_multiplier=1, delta_rp=0, mz_overlay=1):
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
def gaussian(self, oversample_multiplier=1, delta_rp=0, mz_overlay=1):
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
def get_mz_domain(self, oversample_multiplier, mz_overlay):
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
number_possible_assignments
755    @property
756    def number_possible_assignments(
757        self,
758    ):
759        return len(self.molecular_formulas)
def molecular_formula_lowest_error(self):
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

def molecular_formula_highest_prob_score(self):
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

def molecular_formula_earth_filter(self, lowest_error=True):
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
  1. 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
def molecular_formula_water_filter(self, lowest_error=True):
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
  1. 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
def molecular_formula_air_filter(self, lowest_error=True):
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
  1. 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
def cia_score_S_P_error(self):
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
  1. 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
def cia_score_N_S_P_error(self):
 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
  1. 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.