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

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):
149    def calc_area(self):
150        """Calculate the peak area using numpy's trapezoidal fit
151
152        uses provided mz_domain to accurately integrate areas independent of digital resolution
153
154        Returns
155        -------
156        float
157            peak area
158        """
159        if self.peak_right_index > self.peak_left_index:
160            yy = self._ms_parent.abundance_profile[
161                self.peak_left_index : self.peak_right_index
162            ]
163            xx = self._ms_parent.mz_exp_profile[
164                self.peak_left_index : self.peak_right_index
165            ]
166            # 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
167            if xx[0] > xx[-1]:
168                xx = flip(xx)
169                yy = flip(yy)
170            return float(trapezoid(yy, xx))
171
172        else:
173            warnings.warn(
174                "Peak Area Calculation for m/z {} has failed".format(self.mz_exp)
175            )
176            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'):
178    def fit_peak(self, mz_extend=6, delta_rp=0, model="Gaussian"):
179        """Lineshape analysis on a peak using lmfit module.
180
181        Model and fit peak lineshape by defined function - using lmfit module
182        Does not oversample/resample/interpolate data points
183        Better to go back to time domain and perform more zero filling - if possible.
184
185        Parameters
186        ----------
187        mz_extend : int
188            extra points left and right of peak definition to include in fitting
189        delta_rp : float
190            delta resolving power to add to resolving power
191        model : str
192            Type of lineshape model to use.
193            Models allowed: Gaussian, Lorentz, Voigt
194
195        Returns
196        -----
197        mz_domain : ndarray
198            x-axis domain for fit
199        fit_peak : lmfit object
200            fit results object from lmfit module
201
202        Notes
203        -----
204        Returns the calculated mz domain, initial defined abundance profile, and the fit peak results object from lmfit module
205        mz_extend here extends the x-axis domain so that we have sufficient points either side of the apex to fit.
206        Takes about 10ms per peak
207        """
208        start_index = (
209            self.peak_left_index - mz_extend if not self.peak_left_index == 0 else 0
210        )
211        final_index = (
212            self.peak_right_index + mz_extend
213            if not self.peak_right_index == len(self._ms_parent.mz_exp_profile)
214            else self.peak_right_index
215        )
216
217        # check if MSPeak contains the resolving power info
218        if self.resolving_power:
219            # full width half maximum distance
220            self.fwhm = self.mz_exp / (self.resolving_power + delta_rp)
221
222            mz_domain = self._ms_parent.mz_exp_profile[start_index:final_index]
223            abundance_domain = self._ms_parent.abundance_profile[
224                start_index:final_index
225            ]
226
227            if model == "Gaussian":
228                # stardard deviation
229                sigma = self.fwhm / (2 * sqrt(2 * log(2)))
230                amplitude = (sqrt(2 * pi) * sigma) * self.abundance
231                model = models.GaussianModel()
232                params = model.make_params(
233                    center=self.mz_exp, amplitude=amplitude, sigma=sigma
234                )
235
236            elif model == "Lorentz":
237                # stardard deviation
238                sigma = self.fwhm / 2
239                amplitude = sigma * pi * self.abundance
240                model = models.LorentzianModel()
241                params = model.make_params(
242                    center=self.mz_exp, amplitude=amplitude, sigma=sigma
243                )
244
245            elif model == "Voigt":
246                # stardard deviation
247                sigma = self.fwhm / 3.6013
248                amplitude = (sqrt(2 * pi) * sigma) * self.abundance
249                model = models.VoigtModel()
250                params = model.make_params(
251                    center=self.mz_exp, amplitude=amplitude, sigma=sigma, gamma=sigma
252                )
253            else:
254                raise LookupError("model lineshape not known or defined")
255
256            # calc_abundance = model.eval(params=params, x=mz_domain) #Same as initial fit, returned in fit_peak object
257            fit_peak = model.fit(abundance_domain, params=params, x=mz_domain)
258            return mz_domain, fit_peak
259
260        else:
261            raise LookupError(
262                "resolving power is not defined, try to use set_max_resolving_power()"
263            )

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):
265    def voigt_pso(self, w, r, yoff, width, loc, a):
266        """Voigt function for particle swarm optimisation (PSO) fitting
267
268        From https://github.com/pnnl/nmrfit/blob/master/nmrfit/equations.py.
269        Calculates a Voigt function over w based on the relevant properties of the distribution.
270
271        Parameters
272        ----------
273        w : ndarray
274            Array over which the Voigt function will be evaluated.
275        r : float
276            Ratio between the Guassian and Lorentzian functions.
277        yoff : float
278            Y-offset of the Voigt function.
279        width : float
280            The width of the Voigt function.
281        loc : float
282            Center of the Voigt function.
283        a : float
284            Area of the Voigt function.
285        Returns
286        -------
287        V : ndarray
288            Array defining the Voigt function over w.
289
290        References
291        ----------
292        1. https://github.com/pnnl/nmrfit
293
294        Notes
295        -----
296        Particle swarm optimisation (PSO) fitting function can be significantly more computationally expensive than lmfit, with more parameters to optimise.
297
298        """
299        # Lorentzian component
300        L = (2 / (pi * width)) * 1 / (1 + ((w - loc) / (0.5 * width)) ** 2)
301
302        # Gaussian component
303        G = (
304            (2 / width)
305            * sqrt(log(2) / pi)
306            * exp(-(((w - loc) / (width / (2 * sqrt(log(2))))) ** 2))
307        )
308
309        # Voigt body
310        V = (yoff + a) * (r * L + (1 - r) * G)
311
312        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):
314    def objective_pso(self, x, w, u):
315        """Objective function for particle swarm optimisation (PSO) fitting
316
317        The objective function used to fit supplied data.  Evaluates sum of squared differences between the fit and the data.
318
319        Parameters
320        ----------
321        x : list of floats
322            Parameter vector.
323        w : ndarray
324            Array of frequency data.
325        u : ndarray
326            Array of data to be fit.
327
328        Returns
329        -------
330        rmse : float
331            Root mean square error between the data and fit.
332
333        References
334        ----------
335        1. https://github.com/pnnl/nmrfit
336
337        """
338        # global parameters
339        r, width, loc, a = x
340        yoff = 0
341
342        # calculate fit for V
343        V_fit = self.voigt_pso(w, r, yoff, width, loc, a)
344
345        # real component RMSE
346        rmse = sqrt(square((u - V_fit)).mean(axis=None))
347
348        # return the total RMSE
349        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):
351    def minimize_pso(self, lower, upper, w, u):
352        """Minimization function for particle swarm optimisation (PSO) fitting
353
354        Minimizes the objective function using the particle swarm optimization algorithm.
355        Minimization function based on defined parameters
356
357
358        Parameters
359        ----------
360        lower : list of floats
361            Lower bounds for the parameters.
362        upper : list of floats
363            Upper bounds for the parameters.
364        w : ndarray
365            Array of frequency data.
366        u : ndarray
367            Array of data to be fit.
368
369        Notes
370        -----
371        Particle swarm optimisation (PSO) fitting function can be significantly more computationally expensive than lmfit, with more parameters to optimise.
372        Current parameters take ~2 seconds per peak.
373
374
375        References
376        ----------
377        1. https://github.com/pnnl/nmrfit
378
379        """
380        # TODO - allow support to pass swarmsize, maxiter, omega, phip, phig parameters.
381        # TODO - Refactor PSO fitting into its own class?
382
383        xopt, fopt = pyswarm.pso(
384            self.objective_pso,
385            lower,
386            upper,
387            args=(w, u),
388            swarmsize=1000,
389            maxiter=5000,
390            omega=-0.2134,
391            phip=-0.3344,
392            phig=2.3259,
393        )
394        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):
396    def fit_peak_pso(self, mz_extend: int = 6, upsample_multiplier: int = 5):
397        """Lineshape analysis on a peak using particle swarm optimisation (PSO) fitting
398
399        Function to fit a Voigt peakshape using particle swarm optimisation (PSO).
400        Should return better results than lmfit, but much more computationally expensive
401
402        Parameters
403        ----------
404        mz_extend : int, optional
405            extra points left and right of peak definition to include in fitting. Defaults to 6.
406        upsample_multiplier : int, optional
407            factor to increase x-axis points by for simulation of fitted lineshape function. Defaults to 5.
408
409        Returns
410        -------
411        xopt : array
412            variables describing the voigt function.
413            G/L ratio, width (fwhm), apex (x-axis), area.
414            y-axis offset is fixed at 0
415        fopt : float
416            objective score (rmse)
417        psfit : array
418            recalculated y values based on function and optimised fit
419        psfit_hdp : tuple of arrays
420            0 - linspace x-axis upsampled grid
421            1 - recalculated y values based on function and upsampled x-axis grid
422            Does not change results, but aids in visualisation of the 'true' voigt lineshape
423
424        Notes
425        -----
426        Particle swarm optimisation (PSO) fitting function can be significantly more computationally expensive than lmfit, with more parameters to optimise.
427        """
428        # TODO - Add ability to pass pso args (i.e. swarm size, maxiter, omega, phig, etc)
429        # TODO: fix xopt. Magnitude mode data through CoreMS/Bruker starts at 0 but is noise centered well above 0.
430        # Thermo data is noise reduced by also noise subtracted, so starts at 0
431        # Absorption mode/phased data will have positive and negative components and may not be baseline corrected
432
433        start_index = (
434            self.peak_left_index - mz_extend if not self.peak_left_index == 0 else 0
435        )
436        final_index = (
437            self.peak_right_index + mz_extend
438            if not self.peak_right_index == len(self._ms_parent.mz_exp_profile)
439            else self.peak_right_index
440        )
441
442        # check if MSPeak contains the resolving power info
443        if self.resolving_power:
444            # full width half maximum distance
445            self.fwhm = self.mz_exp / (self.resolving_power)
446
447            mz_domain = self._ms_parent.mz_exp_profile[start_index:final_index]
448            abundance_domain = self._ms_parent.abundance_profile[
449                start_index:final_index
450            ]
451            lower = [0, self.fwhm * 0.8, (self.mz_exp - 0.0005), 0]
452            upper = [
453                1,
454                self.fwhm * 1.2,
455                (self.mz_exp + 0.0005),
456                self.abundance / self.signal_to_noise,
457            ]
458            xopt, fopt = self.minimize_pso(lower, upper, mz_domain, abundance_domain)
459
460            psfit = self.voigt_pso(mz_domain, xopt[0], 0, xopt[1], xopt[2], xopt[3])
461            psfit_hdp_x = linspace(
462                min(mz_domain), max(mz_domain), num=len(mz_domain) * upsample_multiplier
463            )
464            psfit_hdp = self.voigt_pso(
465                psfit_hdp_x, xopt[0], 0, xopt[1], xopt[2], xopt[3]
466            )
467            return xopt, fopt, psfit, (psfit_hdp_x, psfit_hdp)
468        else:
469            raise LookupError(
470                "resolving power is not defined, try to use set_max_resolving_power()"
471            )

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):
473    def voigt(self, oversample_multiplier=1, delta_rp=0, mz_overlay=1):
474        """[Legacy] Voigt lineshape analysis function
475        Legacy function for voigt lineshape analysis
476
477        Parameters
478        ----------
479        oversample_multiplier : int
480            factor to increase x-axis points by for simulation of fitted lineshape function
481        delta_rp : float
482            delta resolving power to add to resolving power
483        mz_overlay : int
484            extra points left and right of peak definition to include in fitting
485
486        Returns
487        -------
488        mz_domain : ndarray
489            x-axis domain for fit
490        calc_abundance : ndarray
491            calculated abundance profile based on voigt function
492        """
493
494        if self.resolving_power:
495            # full width half maximum distance
496            self.fwhm = self.mz_exp / (
497                self.resolving_power + delta_rp
498            )  # self.resolving_power)
499
500            # stardart deviation
501            sigma = self.fwhm / 3.6013
502
503            # half width baseline distance
504
505            # mz_domain = linspace(self.mz_exp - hw_base_distance,
506            #                     self.mz_exp + hw_base_distance, datapoint)
507            mz_domain = self.get_mz_domain(oversample_multiplier, mz_overlay)
508
509            # 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) )
510
511            # TODO derive amplitude
512            amplitude = (sqrt(2 * pi) * sigma) * self.abundance
513
514            model = models.VoigtModel()
515
516            params = model.make_params(
517                center=self.mz_exp, amplitude=amplitude, sigma=sigma, gamma=sigma
518            )
519
520            calc_abundance = model.eval(params=params, x=mz_domain)
521
522            return mz_domain, calc_abundance
523
524        else:
525            raise LookupError(
526                "resolving power is not defined, try to use set_max_resolving_power()"
527            )

[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):
529    def pseudovoigt(
530        self, oversample_multiplier=1, delta_rp=0, mz_overlay=1, fraction=0.5
531    ):
532        """[Legacy] pseudovoigt lineshape function
533
534        Legacy function for pseudovoigt lineshape analysis.
535        Note - Code may not be functional currently.
536
537        Parameters
538        ----------
539        oversample_multiplier : int, optional
540            factor to increase x-axis points by for simulation of fitted lineshape function. Defaults to 1.
541        delta_rp : float, optional
542            delta resolving power to add to resolving power. Defaults to 0.
543        mz_overlay : int, optional
544            extra points left and right of peak definition to include in fitting. Defaults to 1.
545        fraction : float, optional
546            fraction of gaussian component in pseudovoigt function. Defaults to 0.5.
547
548        """
549        if self.resolving_power:
550            # full width half maximum distance
551            self.fwhm = self.mz_exp / (
552                self.resolving_power + delta_rp
553            )  # self.resolving_power)
554
555            # stardart deviation
556            sigma = self.fwhm / 2
557
558            # half width baseline distance
559
560            # mz_domain = linspace(self.mz_exp - hw_base_distance,
561            #                     self.mz_exp + hw_base_distance, datapoint)
562            mz_domain = self.get_mz_domain(oversample_multiplier, mz_overlay)
563
564            # 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) )
565            model = models.PseudoVoigtModel()
566
567            # TODO derive amplitude
568            gamma = sigma
569
570            amplitude = (sqrt(2 * pi) * sigma) * self.abundance
571            amplitude = (sqrt(pi / log(2)) * (pi * sigma * self.abundance)) / (
572                (pi * (1 - gamma)) + (sqrt(pi * log(2)) * gamma)
573            )
574
575            params = model.make_params(center=self.mz_exp, sigma=sigma)
576
577            calc_abundance = model.eval(params=params, x=mz_domain)
578
579            return mz_domain, calc_abundance
580
581        else:
582            raise LookupError(
583                "resolving power is not defined, try to use set_max_resolving_power()"
584            )

[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):
586    def lorentz(self, oversample_multiplier=1, delta_rp=0, mz_overlay=1):
587        """[Legacy] Lorentz lineshape analysis function
588
589        Legacy function for lorentz lineshape analysis
590
591        Parameters
592        ----------
593        oversample_multiplier : int
594            factor to increase x-axis points by for simulation of fitted lineshape function
595        delta_rp : float
596            delta resolving power to add to resolving power
597        mz_overlay : int
598            extra points left and right of peak definition to include in fitting
599
600        Returns
601        -------
602        mz_domain : ndarray
603            x-axis domain for fit
604        calc_abundance : ndarray
605            calculated abundance profile based on lorentz function
606
607        """
608        if self.resolving_power:
609            # full width half maximum distance
610            self.fwhm = self.mz_exp / (
611                self.resolving_power + delta_rp
612            )  # self.resolving_power)
613
614            # stardart deviation
615            sigma = self.fwhm / 2
616
617            # half width baseline distance
618            hw_base_distance = 8 * sigma
619
620            # mz_domain = linspace(self.mz_exp - hw_base_distance,
621            #                     self.mz_exp + hw_base_distance, datapoint)
622
623            mz_domain = self.get_mz_domain(oversample_multiplier, mz_overlay)
624            # 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) )
625            model = models.LorentzianModel()
626
627            amplitude = sigma * pi * self.abundance
628
629            params = model.make_params(
630                center=self.mz_exp, amplitude=amplitude, sigma=sigma
631            )
632
633            calc_abundance = model.eval(params=params, x=mz_domain)
634
635            return mz_domain, calc_abundance
636
637        else:
638            raise LookupError(
639                "resolving power is not defined, try to use set_max_resolving_power()"
640            )

[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):
642    def gaussian(self, oversample_multiplier=1, delta_rp=0, mz_overlay=1):
643        """[Legacy] Gaussian lineshape analysis function
644        Legacy gaussian lineshape analysis function
645
646        Parameters
647        ----------
648        oversample_multiplier : int
649            factor to increase x-axis points by for simulation of fitted lineshape function
650        delta_rp : float
651            delta resolving power to add to resolving power
652        mz_overlay : int
653            extra points left and right of peak definition to include in fitting
654
655        Returns
656        -------
657        mz_domain : ndarray
658            x-axis domain for fit
659        calc_abundance : ndarray
660            calculated abundance profile based on gaussian function
661
662
663        """
664
665        # check if MSPeak contains the resolving power info
666        if self.resolving_power:
667            # full width half maximum distance
668            self.fwhm = self.mz_exp / (
669                self.resolving_power + delta_rp
670            )  # self.resolving_power)
671
672            # stardart deviation
673            sigma = self.fwhm / (2 * sqrt(2 * log(2)))
674
675            # half width baseline distance
676            # hw_base_distance = (3.2 * s)
677
678            # match_loz_factor = 3
679
680            # n_d = hw_base_distance * match_loz_factor
681
682            # mz_domain = linspace(
683            #    self.mz_exp - n_d, self.mz_exp + n_d, datapoint)
684
685            mz_domain = self.get_mz_domain(oversample_multiplier, mz_overlay)
686
687            # 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) )
688
689            # calc_abundance = norm.pdf(mz_domain, self.mz_exp, s)
690
691            model = models.GaussianModel()
692
693            amplitude = (sqrt(2 * pi) * sigma) * self.abundance
694
695            params = model.make_params(
696                center=self.mz_exp, amplitude=amplitude, sigma=sigma
697            )
698
699            calc_abundance = model.eval(params=params, x=mz_domain)
700
701            return mz_domain, calc_abundance
702
703        else:
704            raise LookupError(
705                "resolving power is not defined, try to use set_max_resolving_power()"
706            )

[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):
708    def get_mz_domain(self, oversample_multiplier, mz_overlay):
709        """[Legacy] function to resample/interpolate datapoints for lineshape analysis
710
711        This code is used for the legacy line fitting functions and not recommended.
712        Legacy function to support expanding mz domain for legacy lineshape functions
713
714        Parameters
715        ----------
716        oversample_multiplier : int
717            factor to increase x-axis points by for simulation of fitted lineshape function
718        mz_overlay : int
719            extra points left and right of peak definition to include in fitting
720
721        Returns
722        -------
723        mz_domain : ndarray
724            x-axis domain for fit
725
726        """
727        start_index = (
728            self.peak_left_index - mz_overlay if not self.peak_left_index == 0 else 0
729        )
730        final_index = (
731            self.peak_right_index + mz_overlay
732            if not self.peak_right_index == len(self._ms_parent.mz_exp_profile)
733            else self.peak_right_index
734        )
735
736        if oversample_multiplier == 1:
737            mz_domain = self._ms_parent.mz_exp_profile[start_index:final_index]
738
739        else:
740            # we assume a linear correlation for m/z and datapoits
741            # which is only true if the m/z range in narrow (within 1 m/z unit)
742            # this is not true for a wide m/z range
743
744            indexes = range(start_index, final_index + 1)
745            mz = self._ms_parent.mz_exp_profile[indexes]
746            pol = poly1d(polyfit(indexes, mz, 1))
747            oversampled_indexes = linspace(
748                start_index,
749                final_index,
750                (final_index - start_index) * oversample_multiplier,
751            )
752            mz_domain = pol(oversampled_indexes)
753
754        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
756    @property
757    def number_possible_assignments(
758        self,
759    ):
760        return len(self.molecular_formulas)
def molecular_formula_lowest_error(self):
762    def molecular_formula_lowest_error(self):
763        """Return the molecular formula with the smallest absolute mz error"""
764
765        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):
767    def molecular_formula_highest_prob_score(self):
768        """Return the molecular formula with the highest confidence score score"""
769
770        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):
772    def molecular_formula_earth_filter(self, lowest_error=True):
773        """Filter molecular formula using the 'Earth' filter
774
775        This function applies the Formularity-esque 'Earth' filter to possible molecular formula assignments.
776        Earth Filter:
777            O > 0 AND N <= 3 AND P <= 2 AND 3P <= O
778
779        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.
780        Otherwise, it will return all Earth-filter compliant formulas.
781
782        Parameters
783        ----------
784        lowest_error : bool, optional.
785            Return only the lowest error formula which also fits the Earth filter.
786            If False, return all Earth-filter compliant formulas. Default is True.
787
788        Returns
789        -------
790        list
791            List of molecular formula objects which fit the Earth filter
792
793        References
794        ----------
795        1. Nikola Tolic et al., "Formularity: Software for Automated Formula Assignment of Natural and Other Organic Matter from Ultrahigh-Resolution Mass Spectra"
796            Anal. Chem. 2017, 89, 23, 12659–12665
797            doi: 10.1021/acs.analchem.7b03318
798        """
799
800        candidates = list(
801            filter(
802                lambda mf: (
803                    mf.get("O") > 0
804                    and mf.get("N") <= 3
805                    and mf.get("P") <= 2
806                    and (3 * mf.get("P")) <= mf.get("O")
807                ),
808                self.molecular_formulas,
809            )
810        )
811        if len(candidates) > 0:
812            if lowest_error:
813                return min(candidates, key=lambda m: abs(m.mz_error))
814            else:
815                return candidates
816        else:
817            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):
819    def molecular_formula_water_filter(self, lowest_error=True):
820        """Filter molecular formula using the 'Water' filter
821
822        This function applies the Formularity-esque 'Water' filter to possible molecular formula assignments.
823        Water Filter:
824            O > 0 AND N <= 3 AND S <= 2 AND P <= 2
825
826        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.
827        Otherwise, it will return all Water-filter compliant formulas.
828
829        Parameters
830        ----------
831        lowest_error : bool, optional
832            Return only the lowest error formula which also fits the Water filter.
833            If False, return all Water-filter compliant formulas. Defaults to 2
834
835        Returns
836        -------
837        list
838            List of molecular formula objects which fit the Water filter
839
840        References
841        ----------
842        1. Nikola Tolic et al., "Formularity: Software for Automated Formula Assignment of Natural and Other Organic Matter from Ultrahigh-Resolution Mass Spectra"
843            Anal. Chem. 2017, 89, 23, 12659–12665
844            doi: 10.1021/acs.analchem.7b03318
845        """
846
847        candidates = list(
848            filter(
849                lambda mf: (
850                    mf.get("O") > 0
851                    and mf.get("N") <= 3
852                    and mf.get("S") <= 2
853                    and mf.get("P") <= 2
854                ),
855                self.molecular_formulas,
856            )
857        )
858        if len(candidates) > 0:
859            if lowest_error:
860                return min(candidates, key=lambda m: abs(m.mz_error))
861            else:
862                return candidates
863        else:
864            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):
866    def molecular_formula_air_filter(self, lowest_error=True):
867        """Filter molecular formula using the 'Air' filter
868
869        This function applies the Formularity-esque 'Air' filter to possible molecular formula assignments.
870        Air Filter:
871            O > 0 AND N <= 3 AND S <= 1 AND P = 0 AND 3(S+N) <= O
872
873        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.
874        Otherwise, it will return all Air-filter compliant formulas.
875
876        Parameters
877        ----------
878        lowest_error : bool, optional
879            Return only the lowest error formula which also fits the Air filter.
880            If False, return all Air-filter compliant formulas. Defaults to True.
881
882        Returns
883        -------
884        list
885            List of molecular formula objects which fit the Air filter
886
887        References
888        ----------
889        1. Nikola Tolic et al., "Formularity: Software for Automated Formula Assignment of Natural and Other Organic Matter from Ultrahigh-Resolution Mass Spectra"
890            Anal. Chem. 2017, 89, 23, 12659–12665
891            doi: 10.1021/acs.analchem.7b03318
892        """
893
894        candidates = list(
895            filter(
896                lambda mf: (
897                    mf.get("O") > 0
898                    and mf.get("N") <= 2
899                    and mf.get("S") <= 1
900                    and mf.get("P") == 0
901                    and 3 * (mf.get("S") + mf.get("N")) <= mf.get("O")
902                ),
903                self.molecular_formulas,
904            )
905        )
906
907        if len(candidates) > 0:
908            if lowest_error:
909                return min(candidates, key=lambda m: abs(m.mz_error))
910            else:
911                return candidates
912        else:
913            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):
915    def cia_score_S_P_error(self):
916        """Compound Identification Algorithm SP Error - Assignment Filter
917
918        This function applies the Compound Identification Algorithm (CIA) SP Error filter to possible molecular formula assignments.
919
920        It takes the molecular formula with the lowest S+P count, and returns the formula with the lowest absolute error from this subset.
921
922        Returns
923        -------
924        MolecularFormula
925            A single molecular formula which fits the rules of the CIA SP Error filter
926
927
928        References
929        ----------
930        1. Elizabeth B. Kujawinski and Mark D. Behn, "Automated Analysis of Electrospray Ionization Fourier Transform Ion Cyclotron Resonance Mass Spectra of Natural Organic Matter"
931            Anal. Chem. 2006, 78, 13, 4363–4373
932            doi: 10.1021/ac0600306
933        """
934        # case EFormulaScore.HAcap:
935
936        lowest_S_P_mf = min(
937            self.molecular_formulas, key=lambda mf: mf.get("S") + mf.get("P")
938        )
939        lowest_S_P_count = lowest_S_P_mf.get("S") + lowest_S_P_mf.get("P")
940
941        list_same_s_p = list(
942            filter(
943                lambda mf: mf.get("S") + mf.get("P") == lowest_S_P_count,
944                self.molecular_formulas,
945            )
946        )
947
948        # check if list is not empty
949        if list_same_s_p:
950            return min(list_same_s_p, key=lambda m: abs(m.mz_error))
951
952        else:
953            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):
 955    def cia_score_N_S_P_error(self):
 956        """Compound Identification Algorithm NSP Error - Assignment Filter
 957
 958        This function applies the Compound Identification Algorithm (CIA) NSP Error filter to possible molecular formula assignments.
 959
 960        It takes the molecular formula with the lowest N+S+P count, and returns the formula with the lowest absolute error from this subset.
 961
 962        Returns
 963        -------
 964        MolecularFormula
 965            A single molecular formula which fits the rules of the CIA NSP Error filter
 966
 967        References
 968        ----------
 969        1. Elizabeth B. Kujawinski and Mark D. Behn, "Automated Analysis of Electrospray Ionization Fourier Transform Ion Cyclotron Resonance Mass Spectra of Natural Organic Matter"
 970            Anal. Chem. 2006, 78, 13, 4363–4373
 971            doi: 10.1021/ac0600306
 972
 973        Raises
 974        -------
 975        Exception
 976            If no molecular formula are associated with mass spectrum peak.
 977        """
 978        # case EFormulaScore.HAcap:
 979        if self.molecular_formulas:
 980            lowest_N_S_P_mf = min(
 981                self.molecular_formulas,
 982                key=lambda mf: mf.get("N") + mf.get("S") + mf.get("P"),
 983            )
 984            lowest_N_S_P_count = (
 985                lowest_N_S_P_mf.get("N")
 986                + lowest_N_S_P_mf.get("S")
 987                + lowest_N_S_P_mf.get("P")
 988            )
 989
 990            list_same_N_S_P = list(
 991                filter(
 992                    lambda mf: (
 993                        mf.get("N") + mf.get("S") + mf.get("P") == lowest_N_S_P_count
 994                    ),
 995                    self.molecular_formulas,
 996                )
 997            )
 998
 999            if list_same_N_S_P:
1000                SP_filtered_list = list(
1001                    filter(
1002                        lambda mf: (mf.get("S") <= 3) and (mf.get("P") <= 1),
1003                        list_same_N_S_P,
1004                    )
1005                )
1006
1007                if SP_filtered_list:
1008                    return min(SP_filtered_list, key=lambda m: abs(m.mz_error))
1009
1010                else:
1011                    return min(list_same_N_S_P, key=lambda m: abs(m.mz_error))
1012
1013            else:
1014                return lowest_N_S_P_mf
1015        else:
1016            raise Exception(
1017                "No molecular formula associated with the mass spectrum peak at m/z: %.6f"
1018                % self.mz_exp
1019            )

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.