corems.molecular_id.search.molecularFormulaSearch

   1__author__ = "Yuri E. Corilo"
   2__date__ = "Jul 29, 2019"
   3
   4
   5from typing import Callable, List, NamedTuple, Optional, Tuple, Union
   6
   7import tqdm
   8
   9from corems import chunks, timeit
  10from corems.encapsulation.constant import Labels
  11from corems.encapsulation.factory.processingSetting import validate_used_atoms_keys
  12from corems.molecular_formula.factory.MolecularFormulaFactory import MolecularFormula
  13from corems.molecular_id.factory.MolecularLookupTable import MolecularCombinations
  14from corems.molecular_id.factory.molecularSQL import MolForm_SQL
  15from corems.ms_peak.factory.MSPeakClasses import _MSPeak
  16
  17last_error = 0
  18last_dif = 0
  19closest_error = 0
  20error_average = 0
  21nbValues = 0
  22
  23
  24class FormulaSearchJob(NamedTuple):
  25    """One ion-type candidate batch for molecular formula search.
  26
  27    Parameters
  28    ----------
  29    ion_charge : int
  30        Signed search charge for this batch.
  31    ion_type : str
  32        ``Labels`` ion type (protonated/deprotonated, radical, or adduct).
  33    adduct_atom : str or None
  34        Adduct element symbol when ``ion_type`` is adduct; otherwise None.
  35    candidate_formulas : dict
  36        Mapping of nominal m/z to formula objects, as consumed by ``run_search``.
  37    progress : str
  38        tqdm description for this class / ion type / charge.
  39    """
  40
  41    ion_charge: int
  42    ion_type: str
  43    adduct_atom: Optional[str]
  44    candidate_formulas: dict
  45    progress: str
  46
  47
  48class SearchMolecularFormulas:
  49    """Class for searching molecular formulas in a mass spectrum.
  50
  51    Parameters
  52    ----------
  53    mass_spectrum_obj : MassSpectrum
  54        The mass spectrum object.
  55    sql_db : MolForm_SQL, optional
  56        The SQL database object, by default None.
  57    first_hit : bool, optional
  58        Flag to indicate whether to skip peaks that already have a molecular formula assigned, by default False.
  59    find_isotopologues : bool, optional
  60        Flag to indicate whether to find isotopologues, by default True.
  61
  62    Attributes
  63    ----------
  64    mass_spectrum_obj : MassSpectrum
  65        The mass spectrum object.
  66    sql_db : MolForm_SQL
  67        The SQL database object.
  68    first_hit : bool
  69        Flag to indicate whether to skip peaks that already have a molecular formula assigned.
  70    find_isotopologues : bool
  71        Flag to indicate whether to find isotopologues.
  72
  73
  74    Methods
  75    -------
  76    * run_search().
  77        Run the molecular formula search.
  78    * run_worker_mass_spectrum().
  79        Run the molecular formula search on the mass spectrum object.
  80    * run_worker_ms_peaks().
  81        Run the molecular formula search on the given list of mass spectrum peaks.
  82    * database_to_dict().
  83        Convert the database results to a dictionary.
  84    * run_molecular_formula().
  85        Run the molecular formula search on the given list of mass spectrum peaks.
  86    * search_mol_formulas().
  87        Search for molecular formulas in the mass spectrum.
  88    * ion_charges_for_search().
  89        Build the signed charge tuple from polarity and absolute min/max settings.
  90
  91    """
  92
  93    def __init__(
  94        self,
  95        mass_spectrum_obj,
  96        sql_db=None,
  97        first_hit: bool = False,
  98        find_isotopologues: bool = True,
  99    ):
 100        self.first_hit = first_hit
 101
 102        self.find_isotopologues = find_isotopologues
 103
 104        self.mass_spectrum_obj = mass_spectrum_obj
 105
 106        # Catch in-place usedAtoms mutations after settings construction
 107        validate_used_atoms_keys(mass_spectrum_obj.molecular_search_settings.usedAtoms)
 108
 109        if not sql_db:
 110            self.sql_db = MolForm_SQL(
 111                url=mass_spectrum_obj.molecular_search_settings.url_database
 112            )
 113
 114        else:
 115            self.sql_db = sql_db
 116
 117    def __enter__(self):
 118        """Open the SQL database connection."""
 119        return self
 120
 121    def __exit__(self, exc_type, exc_val, exc_tb):
 122        """Close the SQL database connection."""
 123        self.sql_db.close()
 124
 125        return False
 126
 127    @staticmethod
 128    def ion_charges_for_search(
 129        polarity: Union[int, str],
 130        min_ion_charge: int = 1,
 131        max_ion_charge: int = 1,
 132    ) -> Tuple[int, ...]:
 133        """Signed ion charges to use for molecular formula search.
 134
 135        Absolute charges from ``min_ion_charge`` through ``max_ion_charge``
 136        (inclusive) are multiplied by the polarity sign. No charge-state
 137        determination is performed; every selected peak is searched at each
 138        returned charge and candidates are ranked by formula scores.
 139
 140        Parameters
 141        ----------
 142        polarity : int or str
 143            Mass-spectrum polarity as ``+1`` / ``-1``, or LCMS polarity string
 144            ``"positive"`` / ``"negative"``.
 145        min_ion_charge : int, optional
 146            Minimum absolute charge (default 1).
 147        max_ion_charge : int, optional
 148            Maximum absolute charge (default 1). Must be >= ``min_ion_charge``.
 149
 150        Returns
 151        -------
 152        tuple of int
 153            Signed charges sorted by ``(abs(z), z)`` so lower |z| is first
 154            (relevant when ``first_hit=True``).
 155
 156        Raises
 157        ------
 158        ValueError
 159            If polarity is invalid, min < 1, or max < min.
 160        """
 161        min_z = int(min_ion_charge)
 162        max_z = int(max_ion_charge)
 163        if min_z < 1:
 164            raise ValueError("min_ion_charge must be >= 1")
 165        if max_z < min_z:
 166            raise ValueError("max_ion_charge must be >= min_ion_charge")
 167
 168        if isinstance(polarity, str):
 169            p = polarity.strip().lower()
 170            if p == "positive":
 171                sign = 1
 172            elif p == "negative":
 173                sign = -1
 174            else:
 175                raise ValueError(
 176                    "polarity string must be 'positive' or 'negative', "
 177                    f"got {polarity!r}"
 178                )
 179        else:
 180            pol = int(polarity)
 181            if pol == 0:
 182                raise ValueError("polarity must be non-zero")
 183            sign = 1 if pol > 0 else -1
 184
 185        charges = [sign * z for z in range(min_z, max_z + 1)]
 186        return tuple(sorted(charges, key=lambda z: (abs(z), z)))
 187
 188    @staticmethod
 189    def _iter_ion_type_jobs(settings, dict_res, classe_str, ion_charge):
 190        """Yield ion-type candidate jobs for one heteroatom class at one charge.
 191
 192        Encodes shared search policy: protonated and radical follow the
 193        corresponding settings flags; adduct search runs only when adducts
 194        are enabled and ``abs(ion_charge) == 1``. Empty candidate lists and
 195        missing ``dict_res`` keys are skipped.
 196
 197        Parameters
 198        ----------
 199        settings : MolecularFormulaSearchSettings
 200            Molecular formula search settings (ion-type flags).
 201        dict_res : dict
 202            Result of ``database_to_dict`` for this charge: protonated/radical
 203            map class string to a nominal-m/z query dict; adduct maps adduct
 204            atom to that class mapping.
 205        classe_str : str
 206            Heteroatom class key as stored in ``dict_res``.
 207        ion_charge : int
 208            Signed ion charge for this batch.
 209
 210        Yields
 211        ------
 212        FormulaSearchJob
 213            One job per enabled ion type (and per adduct atom) with candidates.
 214        """
 215        if settings.isProtonated:
 216            by_class = (dict_res.get(Labels.protonated_de_ion) or {}).get(classe_str)
 217            if by_class:
 218                yield FormulaSearchJob(
 219                    ion_charge=ion_charge,
 220                    ion_type=Labels.protonated_de_ion,
 221                    adduct_atom=None,
 222                    candidate_formulas=by_class,
 223                    progress=(
 224                        "Started molecular formula search for class %s, "
 225                        "(de)protonated z=%s " % (classe_str, ion_charge)
 226                    ),
 227                )
 228
 229        if settings.isRadical:
 230            by_class = (dict_res.get(Labels.radical_ion) or {}).get(classe_str)
 231            if by_class:
 232                yield FormulaSearchJob(
 233                    ion_charge=ion_charge,
 234                    ion_type=Labels.radical_ion,
 235                    adduct_atom=None,
 236                    candidate_formulas=by_class,
 237                    progress=(
 238                        "Started molecular formula search for class %s, "
 239                        "radical z=%s " % (classe_str, ion_charge)
 240                    ),
 241                )
 242
 243        # Adduct search only at |z|==1; multi-charge adducts are not modeled.
 244        if settings.isAdduct and abs(ion_charge) == 1:
 245            dict_atoms_formulas = dict_res.get(Labels.adduct_ion) or {}
 246            for adduct_atom, dict_by_class in dict_atoms_formulas.items():
 247                by_class = dict_by_class.get(classe_str)
 248                if by_class:
 249                    yield FormulaSearchJob(
 250                        ion_charge=ion_charge,
 251                        ion_type=Labels.adduct_ion,
 252                        adduct_atom=adduct_atom,
 253                        candidate_formulas=by_class,
 254                        progress=(
 255                            "Started molecular formula search for class %s, "
 256                            "adduct z=%s " % (classe_str, ion_charge)
 257                        ),
 258                    )
 259
 260    @staticmethod
 261    def _run_formula_search_jobs(
 262        classes,
 263        nominal_mzs,
 264        mf_search_settings,
 265        search_charges,
 266        sql_db,
 267        apply_fn: Callable[[FormulaSearchJob], None],
 268    ):
 269        """Run charge × class × ion-type search, applying each candidate batch.
 270
 271        Loads the formula database once per charge per class chunk. Does not
 272        close ``sql_db``. tqdm iterates class chunks; the description is the
 273        job ``progress`` string.
 274
 275        Parameters
 276        ----------
 277        classes : list
 278            Heteroatom class tuples from ``MolecularCombinations.runworker``.
 279        nominal_mzs : list
 280            Nominal m/z values to load from the database.
 281        mf_search_settings : MolecularFormulaSearchSettings
 282            Search settings (chunk size, ion-type flags, verbosity).
 283        search_charges : sequence of int
 284            Signed charges from ``ion_charges_for_search``.
 285        sql_db : MolForm_SQL
 286            Open formula database connection.
 287        apply_fn : callable
 288            Called with each ``FormulaSearchJob``. Path-specific: DI uses
 289            ``run_search``; LC uses ``search_spectra_against_candidates``.
 290        """
 291        verbose = mf_search_settings.verbose_processing
 292        for classe_chunk in chunks(classes, mf_search_settings.db_chunk_size):
 293            classes_str_list = [class_tuple[0] for class_tuple in classe_chunk]
 294            for ion_charge in search_charges:
 295                dict_res = SearchMolecularFormulas.database_to_dict(
 296                    classes_str_list,
 297                    nominal_mzs,
 298                    mf_search_settings,
 299                    ion_charge,
 300                    sql_db=sql_db,
 301                )
 302                pbar = tqdm.tqdm(classe_chunk, disable=not verbose)
 303                for classe_tuple in pbar:
 304                    classe_str = classe_tuple[0]
 305                    for job in SearchMolecularFormulas._iter_ion_type_jobs(
 306                        mf_search_settings, dict_res, classe_str, ion_charge
 307                    ):
 308                        pbar.set_description_str(desc=job.progress, refresh=True)
 309                        apply_fn(job)
 310
 311    def run_search(
 312        self,
 313        mspeaks: list,
 314        query: dict,
 315        min_abundance: float,
 316        ion_type: str,
 317        ion_charge: int,
 318        adduct_atom=None,
 319    ):
 320        """Run the molecular formula search.
 321
 322        Parameters
 323        ----------
 324        mspeaks : list of MSPeak
 325            The list of mass spectrum peaks.
 326        query : dict
 327            The query dictionary containing the possible molecular formulas.
 328        min_abundance : float
 329            The minimum abundance threshold.
 330        ion_type : str
 331            The ion type.
 332        ion_charge : int
 333            The ion charge.
 334        adduct_atom : str, optional
 335            The adduct atom, by default None.
 336        """
 337
 338        def get_formulas(nominal_overlay: float = 0.1):
 339            """
 340            Get the list of formulas based on the nominal overlay.
 341
 342            Parameters
 343            ----------
 344            nominal_overlay : float, optional
 345                The nominal overlay, by default 0.1.
 346
 347            Returns
 348            -------
 349            list
 350                The list of formulas.
 351            """
 352            nominal_mz = ms_peak.nominal_mz_exp
 353
 354            defect_mass = ms_peak.mz_exp - nominal_mz
 355            nominal_masses = [nominal_mz]
 356
 357            if (defect_mass) >= 1 - nominal_overlay:
 358                nominal_masses.append(nominal_mz + 1)
 359            elif (defect_mass) <= nominal_overlay:
 360                nominal_masses.append(nominal_mz - 1)
 361
 362            list_formulas_candidates = []
 363
 364            for nominal_mass in nominal_masses:
 365                if nominal_mass in query.keys():
 366                    list_formulas_candidates.extend(query.get(nominal_mass))
 367
 368            return list_formulas_candidates
 369
 370        all_assigned_indexes = list()
 371
 372        # molecular_search_settings = self.mass_spectrum_obj.molecular_search_settings
 373
 374        search_molfrom = SearchMolecularFormulaWorker(
 375            find_isotopologues=self.find_isotopologues
 376        )
 377
 378        for ms_peak in mspeaks:
 379            # already assigned a molecular formula
 380            if self.first_hit:
 381                if ms_peak.is_assigned:
 382                    continue
 383
 384            ms_peak_indexes = search_molfrom.find_formulas(
 385                get_formulas(),
 386                min_abundance,
 387                self.mass_spectrum_obj,
 388                ms_peak,
 389                ion_type,
 390                ion_charge,
 391                adduct_atom,
 392            )
 393
 394            all_assigned_indexes.extend(ms_peak_indexes)
 395
 396        # all_assigned_indexes = MolecularFormulaSearchFilters().filter_isotopologue(all_assigned_indexes, self.mass_spectrum_obj)
 397
 398        # all_assigned_indexes = MolecularFormulaSearchFilters().filter_kendrick(all_assigned_indexes, self.mass_spectrum_obj)
 399
 400        # MolecularFormulaSearchFilters().check_min_peaks(all_assigned_indexes, self.mass_spectrum_obj)
 401        # filter per min peaks per mono isotopic class
 402
 403    def run_worker_mass_spectrum(self):
 404        """Run the molecular formula search on the mass spectrum object."""
 405        self.run_molecular_formula(
 406            self.mass_spectrum_obj.sort_by_abundance(),
 407            print_time=self.mass_spectrum_obj.molecular_search_settings.verbose_processing,
 408        )
 409
 410    def run_worker_ms_peaks(self, ms_peaks):
 411        """Run the molecular formula search on the given list of mass spectrum peaks.
 412
 413        Parameters
 414        ----------
 415        ms_peaks : list of MSPeak
 416            The list of mass spectrum peaks.
 417        """
 418        self.run_molecular_formula(
 419            ms_peaks,
 420            print_time=self.mass_spectrum_obj.molecular_search_settings.verbose_processing,
 421        )
 422
 423    @staticmethod
 424    def database_to_dict(
 425        classe_str_list,
 426        nominal_mzs,
 427        mf_search_settings,
 428        ion_charge,
 429        sql_db=None,
 430    ):
 431        """Convert the database results to a dictionary.
 432
 433        Parameters
 434        ----------
 435        classe_str_list : list
 436            The list of class strings.
 437        nominal_mzs : list
 438            The list of nominal m/z values.
 439        mf_search_settings : MolecularFormulaSearchSettings
 440            The molecular formula search settings.
 441        ion_charge : int
 442            The ion charge.
 443        sql_db : MolForm_SQL, optional
 444            The SQL database object, by default None. If None, a new MolForm_SQL object will be created.
 445
 446        Returns
 447        -------
 448        dict
 449            The dictionary containing the database results.
 450        """
 451        owns_db = sql_db is None
 452        if owns_db:
 453            sql_db = MolForm_SQL(url=mf_search_settings.url_database)
 454
 455        try:
 456            dict_res = {}
 457
 458            if mf_search_settings.isProtonated:
 459                dict_res[Labels.protonated_de_ion] = sql_db.get_dict_by_classes(
 460                    classe_str_list,
 461                    Labels.protonated_de_ion,
 462                    nominal_mzs,
 463                    ion_charge,
 464                    mf_search_settings,
 465                )
 466
 467            if mf_search_settings.isRadical:
 468                dict_res[Labels.radical_ion] = sql_db.get_dict_by_classes(
 469                    classe_str_list,
 470                    Labels.radical_ion,
 471                    nominal_mzs,
 472                    ion_charge,
 473                    mf_search_settings,
 474                )
 475
 476            if mf_search_settings.isAdduct:
 477                adduct_list = (
 478                    mf_search_settings.adduct_atoms_neg
 479                    if ion_charge < 0
 480                    else mf_search_settings.adduct_atoms_pos
 481                )
 482                dict_res[Labels.adduct_ion] = sql_db.get_dict_by_classes(
 483                    classe_str_list,
 484                    Labels.adduct_ion,
 485                    nominal_mzs,
 486                    ion_charge,
 487                    mf_search_settings,
 488                    adducts=adduct_list,
 489                )
 490
 491            return dict_res
 492        finally:
 493            if owns_db:
 494                sql_db.close()
 495
 496    @timeit(print_time=True)
 497    def run_molecular_formula(self, ms_peaks, **kwargs):
 498        """Run the molecular formula search on the given list of mass spectrum peaks.
 499
 500        Parameters
 501        ----------
 502        ms_peaks : list of MSPeak
 503            The list of mass spectrum peaks.
 504        **kwargs
 505            Additional keyword arguments.
 506            Most notably, print_time, which is a boolean flag to indicate whether to print the time
 507            and passed to the timeit decorator.
 508        """
 509        settings = self.mass_spectrum_obj.molecular_search_settings
 510        # Re-check after possible post-construction mutation of settings fields
 511        settings.validate_ion_charge_settings()
 512        # Polarity must come from the mass spectrum (DI often -1 from data).
 513        # Do not default to +1; LC already requires polarity on the LCMS object.
 514        polarity_raw = self.mass_spectrum_obj.polarity
 515        if polarity_raw is None:
 516            raise ValueError(
 517                "Mass spectrum polarity must be set from the data before "
 518                "formula search (e.g. +1 or -1). There is no default polarity."
 519            )
 520        try:
 521            polarity = int(polarity_raw)
 522        except (TypeError, ValueError) as exc:
 523            raise ValueError(
 524                "Mass spectrum polarity must be an integer +1 or -1 "
 525                f"(got {polarity_raw!r})."
 526            ) from exc
 527        if polarity == 0:
 528            raise ValueError("Mass spectrum polarity must be non-zero (+1 or -1).")
 529        # Multi-charge formula search: expand absolute min..max (default 1..1).
 530        # Does not rewrite MSPeak.polarity (remains ±1).
 531        search_charges = self.ion_charges_for_search(
 532            polarity,
 533            min_ion_charge=settings.min_ion_charge,
 534            max_ion_charge=settings.max_ion_charge,
 535        )
 536        min_abundance = self.mass_spectrum_obj.min_abundance
 537        nominal_mzs = self.mass_spectrum_obj.nominal_mz
 538
 539        # reset average error, only relevant is average mass error method is being used
 540        SearchMolecularFormulaWorker(
 541            find_isotopologues=self.find_isotopologues
 542        ).reset_error(self.mass_spectrum_obj)
 543
 544        # check database for all possible molecular formula combinations based on the setting passed to self.mass_spectrum_obj.molecular_search_settings
 545        classes = MolecularCombinations(self.sql_db).runworker(
 546            settings, print_time=settings.verbose_processing
 547        )
 548
 549        def apply_job(job):
 550            self.run_search(
 551                ms_peaks,
 552                job.candidate_formulas,
 553                min_abundance,
 554                job.ion_type,
 555                job.ion_charge,
 556                adduct_atom=job.adduct_atom,
 557            )
 558
 559        self._run_formula_search_jobs(
 560            classes,
 561            nominal_mzs,
 562            settings,
 563            search_charges,
 564            self.sql_db,
 565            apply_job,
 566        )
 567        self.sql_db.close()
 568
 569    def search_mol_formulas(
 570        self,
 571        possible_formulas_list: List[MolecularFormula],
 572        ion_type: str,
 573        neutral_molform=True,
 574        find_isotopologues=True,
 575        adduct_atom=None,
 576    ) -> List[_MSPeak]:
 577        """Search for molecular formulas in the mass spectrum.
 578
 579        Parameters
 580        ----------
 581        possible_formulas_list : list of MolecularFormula
 582            The list of possible molecular formulas.
 583        ion_type : str
 584            The ion type.
 585        neutral_molform : bool, optional
 586            Flag to indicate whether the molecular formulas are neutral, by default True.
 587        find_isotopologues : bool, optional
 588            Flag to indicate whether to find isotopologues, by default True.
 589        adduct_atom : str, optional
 590            The adduct atom, by default None.
 591
 592        Returns
 593        -------
 594        list of MSPeak
 595            The list of mass spectrum peaks with assigned molecular formulas.
 596        """
 597        # neutral_molform: some reference files already present the formula on ion mode, for instance, bruker reference files
 598        #    if that is the case than turn neutral_molform off
 599
 600        SearchMolecularFormulaWorker(find_isotopologues=find_isotopologues).reset_error(
 601            self.mass_spectrum_obj
 602        )
 603
 604        initial_min_peak_bool = (
 605            self.mass_spectrum_obj.molecular_search_settings.use_min_peaks_filter
 606        )
 607        initial_runtime_kendrick_filter = (
 608            self.mass_spectrum_obj.molecular_search_settings.use_runtime_kendrick_filter
 609        )
 610
 611        # Are the following 3 lines redundant?
 612        self.mass_spectrum_obj.molecular_search_settings.use_min_peaks_filter = False
 613        self.mass_spectrum_obj.molecular_search_settings.use_min_peaks_filter = (
 614            False  # TODO check this line
 615        )
 616        self.mass_spectrum_obj.molecular_search_settings.use_runtime_kendrick_filter = (
 617            False
 618        )
 619
 620        possible_formulas_dict_nm = {}
 621
 622        for mf in possible_formulas_list:
 623            if neutral_molform:
 624                # Bin by the nominal m/z of the requested ion hypothesis so
 625                # candidates land in the same bin run_search looks them up under.
 626                if ion_type == Labels.radical_ion:
 627                    nm = int(mf.radical_mz)
 628                elif ion_type == Labels.adduct_ion and adduct_atom:
 629                    nm = int(mf.adduct_mz(adduct_atom))
 630                else:
 631                    nm = int(mf.protonated_mz)
 632            else:
 633                nm = int(mf.mz_nominal_calc)
 634
 635            if nm in possible_formulas_dict_nm.keys():
 636                possible_formulas_dict_nm[nm].append(mf)
 637
 638            else:
 639                possible_formulas_dict_nm[nm] = [mf]
 640
 641        min_abundance = self.mass_spectrum_obj.min_abundance
 642
 643        ion_type = ion_type
 644
 645        self.run_search(
 646            self.mass_spectrum_obj,
 647            possible_formulas_dict_nm,
 648            min_abundance,
 649            ion_type,
 650            self.mass_spectrum_obj.polarity,
 651            adduct_atom=adduct_atom,
 652        )
 653
 654        self.mass_spectrum_obj.molecular_search_settings.use_min_peaks_filter = (
 655            initial_min_peak_bool
 656        )
 657        self.mass_spectrum_obj.molecular_search_settings.use_runtime_kendrick_filter = (
 658            initial_runtime_kendrick_filter
 659        )
 660
 661        mspeaks = [mspeak for mspeak in self.mass_spectrum_obj if mspeak.is_assigned]
 662
 663        self.sql_db.close()
 664
 665        return mspeaks
 666
 667
 668class SearchMolecularFormulaWorker:
 669    """Class for searching molecular formulas in a mass spectrum.
 670
 671    Parameters
 672    ----------
 673    find_isotopologues : bool, optional
 674        Flag to indicate whether to find isotopologues, by default True.
 675
 676    Attributes
 677    ----------
 678    find_isotopologues : bool
 679        Flag to indicate whether to find isotopologues.
 680
 681    Methods
 682    -------
 683    * reset_error().
 684        Reset the error variables.
 685    * set_last_error().
 686        Set the last error.
 687    * find_formulas().
 688        Find the formulas.
 689    * calc_error().
 690        Calculate the error.
 691    """
 692
 693    # TODO add reset error function
 694    # needs this wraper to pass the class to multiprocessing
 695
 696    def __init__(self, find_isotopologues=True):
 697        self.find_isotopologues = find_isotopologues
 698
 699    def __call__(self, args):
 700        """Call the find formulas function.
 701
 702        Parameters
 703        ----------
 704        args : tuple
 705            The arguments.
 706
 707        Returns
 708        -------
 709        list
 710            The list of mass spectrum peaks with assigned molecular formulas.
 711        """
 712        return self.find_formulas(*args)  # ,args[1]
 713
 714    def reset_error(self, mass_spectrum_obj):
 715        """Reset the error variables.
 716
 717        Parameters
 718        ----------
 719        mass_spectrum_obj : MassSpectrum
 720            The mass spectrum object.
 721
 722        Notes
 723        -----
 724        This function resets the error variables for the given mass spectrum object.
 725        """
 726        global last_error, last_dif, closest_error, error_average, nbValues
 727        last_error, last_dif, closest_error, nbValues = 0.0, 0.0, 0.0, 0.0
 728
 729    def set_last_error(self, error, mass_spectrum_obj):
 730        """Set the last error.
 731
 732        Parameters
 733        ----------
 734        error : float
 735            The error.
 736        mass_spectrum_obj : MassSpectrum
 737            The mass spectrum object.
 738        """
 739        # set the changes to the global variables, not internal ones
 740        global last_error, last_dif, closest_error, error_average, nbValues
 741
 742        if mass_spectrum_obj.molecular_search_settings.error_method == "distance":
 743            dif = error - last_error
 744            if dif < last_dif:
 745                last_dif = dif
 746                closest_error = error
 747                mass_spectrum_obj.molecular_search_settings.min_ppm_error = (
 748                    closest_error
 749                    - mass_spectrum_obj.molecular_search_settings.mz_error_range
 750                )
 751                mass_spectrum_obj.molecular_search_settings.max_ppm_error = (
 752                    closest_error
 753                    + mass_spectrum_obj.molecular_search_settings.mz_error_range
 754                )
 755
 756        elif mass_spectrum_obj.molecular_search_settings.error_method == "lowest":
 757            if error < last_error:
 758                mass_spectrum_obj.molecular_search_settings.min_ppm_error = (
 759                    error - mass_spectrum_obj.molecular_search_settings.mz_error_range
 760                )
 761                mass_spectrum_obj.molecular_search_settings.max_ppm_error = (
 762                    error + mass_spectrum_obj.molecular_search_settings.mz_error_range
 763                )
 764                last_error = error
 765
 766        elif mass_spectrum_obj.molecular_search_settings.error_method == "symmetrical":
 767            mass_spectrum_obj.molecular_search_settings.min_ppm_error = (
 768                mass_spectrum_obj.molecular_search_settings.mz_error_average
 769                - mass_spectrum_obj.molecular_search_settings.mz_error_range
 770            )
 771            mass_spectrum_obj.molecular_search_settings.max_ppm_error = (
 772                mass_spectrum_obj.molecular_search_settings.mz_error_average
 773                + mass_spectrum_obj.molecular_search_settings.mz_error_range
 774            )
 775
 776        elif mass_spectrum_obj.molecular_search_settings.error_method == "average":
 777            nbValues += 1
 778            error_average = error_average + ((error - error_average) / nbValues)
 779            mass_spectrum_obj.molecular_search_settings.min_ppm_error = (
 780                error_average
 781                - mass_spectrum_obj.molecular_search_settings.mz_error_range
 782            )
 783            mass_spectrum_obj.molecular_search_settings.max_ppm_error = (
 784                error_average
 785                + mass_spectrum_obj.molecular_search_settings.mz_error_range
 786            )
 787
 788        else:
 789            # using set mass_spectrum_obj.molecular_search_settings.min_ppm_error  and max_ppm_error range
 790            pass
 791
 792        # returns the error based on the selected method at mass_spectrum_obj.molecular_search_settings.method
 793
 794    @staticmethod
 795    def calc_error(mz_exp, mz_calc, method="ppm"):
 796        """Calculate the error.
 797
 798        Parameters
 799        ----------
 800        mz_exp : float
 801            The experimental m/z value.
 802        mz_calc : float
 803            The calculated m/z value.
 804        method : str, optional
 805            The method, by default 'ppm'.
 806
 807        Raises
 808        -------
 809        Exception
 810            If the method is not ppm or ppb.
 811
 812        Returns
 813        -------
 814        float
 815            The error.
 816        """
 817
 818        if method == "ppm":
 819            multi_factor = 1_000_000
 820
 821        elif method == "ppb":
 822            multi_factor = 1_000_000_000
 823
 824        elif method == "perc":
 825            multi_factor = 100
 826
 827        else:
 828            raise Exception(
 829                "method needs to be ppm or ppb, you have entered %s" % method
 830            )
 831
 832        if mz_exp:
 833            return ((mz_exp - mz_calc) / mz_calc) * multi_factor
 834
 835        else:
 836            raise Exception("Please set mz_calc first")
 837
 838    def find_formulas(
 839        self,
 840        formulas,
 841        min_abundance,
 842        mass_spectrum_obj,
 843        ms_peak,
 844        ion_type,
 845        ion_charge,
 846        adduct_atom=None,
 847    ):
 848        """Find the formulas.
 849
 850        Parameters
 851        ----------
 852        formulas : list of MolecularFormula
 853            The list of molecular formulas.
 854        min_abundance : float
 855            The minimum abundance threshold.
 856        mass_spectrum_obj : MassSpectrum
 857            The mass spectrum object.
 858        ms_peak : MSPeak
 859            The mass spectrum peak.
 860        ion_type : str
 861            The ion type.
 862        ion_charge : int
 863            The ion charge.
 864        adduct_atom : str, optional
 865            The adduct atom, by default None.
 866
 867        Returns
 868        -------
 869        list of MSPeak
 870            The list of mass spectrum peaks with assigned molecular formulas.
 871
 872        Notes
 873        -----
 874        Uses the closest error the next search (this is not ideal, it needs to use confidence
 875        metric to choose the right candidate then propagate the error using the error from the best candidate).
 876        It needs to add s/n to the equation.
 877        It need optimization to define the mz_error_range within a m/z unit since it is directly proportional
 878        with the mass, and inversely proportional to the rp. It's not linear, i.e., sigma mass.
 879        The idea it to correlate sigma to resolving power, signal to noise and sample complexity per mz unit.
 880        Method='distance'
 881        """
 882        mspeak_assigned_index = list()
 883
 884        min_ppm_error = mass_spectrum_obj.molecular_search_settings.min_ppm_error
 885        max_ppm_error = mass_spectrum_obj.molecular_search_settings.max_ppm_error
 886
 887        min_abun_error = mass_spectrum_obj.molecular_search_settings.min_abun_error
 888        max_abun_error = mass_spectrum_obj.molecular_search_settings.max_abun_error
 889
 890        # f = open("abundance_error.txt", "a+")
 891        ms_peak_mz_exp, ms_peak_abundance = ms_peak.mz_exp, ms_peak.abundance
 892        # min_error = min([pmf.mz_error for pmf in possible_formulas])
 893
 894        def mass_by_ion_type(possible_formula_obj):
 895            if ion_type == Labels.protonated_de_ion:
 896                return possible_formula_obj._protonated_mz(ion_charge)
 897
 898            elif ion_type == Labels.radical_ion:
 899                return possible_formula_obj._radical_mz(ion_charge)
 900
 901            elif ion_type == Labels.adduct_ion and adduct_atom:
 902                # Positional argument order differs between formula objects
 903                # and SQL links; keywords are unambiguous.
 904                return possible_formula_obj._adduct_mz(
 905                    ion_charge=ion_charge, adduct_atom=adduct_atom
 906                )
 907
 908            else:
 909                # will return externally calculated mz if is set, #use on Bruker Reference list import
 910                # if the ion type is known the ion mass based on molecular formula ion type
 911                # if ion type is unknow will return neutral mass
 912                return possible_formula_obj.mz_calc
 913
 914        for possible_formula in formulas:
 915            if possible_formula:
 916                error = self.calc_error(
 917                    ms_peak_mz_exp, mass_by_ion_type(possible_formula)
 918                )
 919
 920                # error = possible_formula.mz_error
 921
 922                if min_ppm_error <= error <= max_ppm_error:
 923                    # update the error
 924
 925                    self.set_last_error(error, mass_spectrum_obj)
 926
 927                    # add molecular formula match to ms_peak
 928
 929                    # get molecular formula dict from sql obj
 930                    # formula_dict = pickle.loads(possible_formula.mol_formula)
 931
 932                    formula_dict = possible_formula.to_dict()
 933                    # create the molecular formula obj to be stored
 934                    molecular_formula = MolecularFormula(
 935                        formula_dict,
 936                        ion_charge,
 937                        ion_type=ion_type,
 938                        adduct_atom=adduct_atom,
 939                    )
 940                    # add the molecular formula obj to the mspeak obj
 941                    # add the mspeak obj and it's index for tracking next assignment step
 942
 943                    if self.find_isotopologues:
 944                        # calculates isotopologues
 945                        isotopologues = molecular_formula.isotopologues(
 946                            min_abundance,
 947                            ms_peak_abundance,
 948                            mass_spectrum_obj.dynamic_range,
 949                        )
 950
 951                        # search for isotopologues
 952                        for isotopologue_formula in isotopologues:
 953                            molecular_formula.expected_isotopologues.append(
 954                                isotopologue_formula
 955                            )
 956                            # move this outside to improve preformace
 957                            # we need to increase the search space to -+1 m_z
 958                            first_index, last_index = (
 959                                mass_spectrum_obj.get_nominal_mz_first_last_indexes(
 960                                    isotopologue_formula.mz_nominal_calc
 961                                )
 962                            )
 963
 964                            for ms_peak_iso in mass_spectrum_obj[
 965                                first_index:last_index
 966                            ]:
 967                                error = self.calc_error(
 968                                    ms_peak_iso.mz_exp, isotopologue_formula.mz_calc
 969                                )
 970
 971                                if min_ppm_error <= error <= max_ppm_error:
 972                                    # need to define error distribution for abundance measurements
 973
 974                                    # if mass_spectrum_obj.is_centroid:
 975
 976                                    abundance_error = self.calc_error(
 977                                        isotopologue_formula.abundance_calc,
 978                                        ms_peak_iso.abundance,
 979                                        method="perc",
 980                                    )
 981
 982                                    # area_error = self.calc_error(ms_peak.area, ms_peak_iso.area, method='perc')
 983
 984                                    # margin of error was set empirically/ needs statistical calculation
 985                                    #  of margin of error for the measurement of the abundances
 986                                    if (
 987                                        min_abun_error
 988                                        <= abundance_error
 989                                        <= max_abun_error
 990                                    ):
 991                                        # update the error
 992
 993                                        self.set_last_error(error, mass_spectrum_obj)
 994
 995                                        # isotopologue_formula.mz_error = error
 996
 997                                        # isotopologue_formula.area_error = area_error
 998
 999                                        # isotopologue_formula.abundance_error = abundance_error
1000
1001                                        isotopologue_formula.mspeak_index_mono_isotopic = ms_peak.index
1002
1003                                        mono_isotopic_formula_index = len(ms_peak)
1004
1005                                        isotopologue_formula.mspeak_index_mono_isotopic = ms_peak.index
1006
1007                                        isotopologue_formula.mono_isotopic_formula_index = mono_isotopic_formula_index
1008
1009                                        # add mspeaks isotopologue index to the mono isotopic MolecularFormula obj and the respective formula position
1010
1011                                        # add molecular formula match to ms_peak
1012                                        x = ms_peak_iso.add_molecular_formula(
1013                                            isotopologue_formula
1014                                        )
1015
1016                                        molecular_formula.mspeak_mf_isotopologues_indexes.append(
1017                                            (ms_peak_iso.index, x)
1018                                        )
1019                                        # add mspeaks mono isotopic index to the isotopologue MolecularFormula obj
1020
1021                    y = ms_peak.add_molecular_formula(molecular_formula)
1022
1023                    mspeak_assigned_index.append((ms_peak.index, y))
1024
1025        return mspeak_assigned_index
1026
1027
1028class SearchMolecularFormulasLC:
1029    """Class for searching molecular formulas in a LC object.
1030
1031    Parameters
1032    ----------
1033    lcms_obj : LCMSBase
1034        The LCMSBase object.
1035    sql_db : MolForm_SQL, optional
1036        The SQL database object, by default None.
1037    first_hit : bool, optional
1038        Flag to indicate whether to skip peaks that already have a molecular formula assigned, by default False.
1039    find_isotopologues : bool, optional
1040        Flag to indicate whether to find isotopologues, by default True.
1041
1042    Methods
1043    -------
1044
1045    * search_spectra_against_candidates().
1046        Search a list of mass spectra against a list of candidate formulas with a given ion type, charge, and optional adduct_atom.
1047    * bulk_run_molecular_formula_search().
1048        Run the molecular formula search on the given list of mass spectra.
1049        Pulls the settings from the LCMSBase object to set ion type and charge to search for.
1050    * run_mass_feature_search().
1051        Run the molecular formula search on mass features.
1052        Calls bulk_run_molecular_formula_search() with specified mass spectra and mass peaks.
1053    * run_untargeted_worker_ms1().
1054        Run untargeted molecular formula search on the ms1 mass spectrum.
1055        DEPRECATED: use run_mass_feature_search() or bulk_run_molecular_formula_search() instead.
1056    * run_target_worker_ms1().
1057        Run targeted molecular formula search on the ms1 mass spectrum.
1058        DEPRECATED: use run_mass_feature_search() or bulk_run_molecular_formula_search() instead.
1059    """
1060
1061    def __init__(self, lcms_obj, sql_db=None, first_hit=False, find_isotopologues=True):
1062        self.first_hit = first_hit
1063
1064        self.find_isotopologues = find_isotopologues
1065
1066        self.lcms_obj = lcms_obj
1067
1068        # Catch in-place usedAtoms mutations after settings construction
1069        validate_used_atoms_keys(
1070            self.lcms_obj.parameters.mass_spectrum["ms1"].molecular_search.usedAtoms
1071        )
1072
1073        if not sql_db:
1074            self.sql_db = MolForm_SQL(
1075                url=self.lcms_obj.parameters.mass_spectrum[
1076                    "ms1"
1077                ].molecular_search.url_database
1078            )
1079
1080        else:
1081            self.sql_db = sql_db
1082
1083    def search_spectra_against_candidates(
1084        self,
1085        mass_spectrum_list,
1086        ms_peaks_list,
1087        candidate_formulas,
1088        ion_type,
1089        ion_charge,
1090        adduct_atom=None,
1091    ):
1092        """Search a list of mass spectra against a list of candidate formulas with a given ion type and charge.
1093
1094        Parameters
1095        ----------
1096        mass_spectrum_list : list of MassSpectrum
1097            The list of mass spectra to perform the search on.
1098        ms_peaks_list : list of lists of MSPeak objects
1099            The list of mass spectrum peaks to search within each mass spectrum.
1100        candidate_formulas : dict
1101            The candidate formulas.
1102        ion_type : str
1103            The ion type.
1104        ion_charge : int
1105            signed ion charge used for the candidate formulas.
1106        adduct_atom : str, optional
1107            The adduct atom, by default None.
1108
1109        Notes
1110        -----
1111        This function is designed to be used with the bulk_run_molecular_formula_search function.
1112        """
1113        for mass_spectrum, ms_peaks in zip(mass_spectrum_list, ms_peaks_list):
1114            single_ms_search = SearchMolecularFormulas(
1115                mass_spectrum,
1116                sql_db=self.sql_db,
1117                first_hit=self.first_hit,
1118                find_isotopologues=self.find_isotopologues,
1119            )
1120            single_ms_search.run_search(
1121                ms_peaks,
1122                candidate_formulas,
1123                mass_spectrum.min_abundance,
1124                ion_type,
1125                ion_charge,
1126                adduct_atom=adduct_atom,
1127            )
1128
1129    def bulk_run_molecular_formula_search(
1130        self, mass_spectrum_list, ms_peaks_list, mass_spectrum_setting_key="ms1"
1131    ):
1132        """Run the molecular formula search on the given list of mass spectra.
1133
1134        Uses ``min_ion_charge`` / ``max_ion_charge`` from molecular search settings
1135        to search each peak at every signed charge in that absolute range
1136        (defaults 1..1 = single-charge). Does not determine or rewrite peak
1137        ``polarity``; assignment charge is on each ``MolecularFormula``.
1138
1139        Parameters
1140        ----------
1141        mass_spectrum_list : list of MassSpectrum
1142            The list of mass spectra to search.
1143        ms_peaks_list : list of lists of MSPeak objects
1144            The mass peaks to perform molecular formula search within each mass spectrum
1145        mass_spectrum_setting_key : str, optional
1146            The mass spectrum setting key, by default 'ms1'.
1147            This is used to get the appropriate molecular search settings from the LCMSBase object
1148        """
1149        if self.lcms_obj.polarity not in ("positive", "negative"):
1150            raise ValueError("Polarity must be either 'positive' or 'negative'")
1151
1152        # Check that the length of the mass spectrum list and the ms_peaks list are the same
1153        if len(mass_spectrum_list) != len(ms_peaks_list):
1154            raise ValueError(
1155                "The length of the mass spectrum list and the ms_peaks list must be the same"
1156            )
1157
1158        mol_settings = self.lcms_obj.parameters.mass_spectrum[
1159            mass_spectrum_setting_key
1160        ].molecular_search
1161        # Re-check after possible post-construction mutation of settings fields
1162        mol_settings.validate_ion_charge_settings()
1163        search_charges = SearchMolecularFormulas.ion_charges_for_search(
1164            self.lcms_obj.polarity,
1165            min_ion_charge=mol_settings.min_ion_charge,
1166            max_ion_charge=mol_settings.max_ion_charge,
1167        )
1168
1169        nominal_mzs = [x.nominal_mz for x in mass_spectrum_list]
1170        nominal_mzs = list(set([item for sublist in nominal_mzs for item in sublist]))
1171
1172        # reset average error, only relevant if average mass error method is being used
1173        SearchMolecularFormulaWorker(
1174            find_isotopologues=self.find_isotopologues
1175        ).reset_error(mass_spectrum_list[0])
1176
1177        # check database for all possible molecular formula combinations based on the setting passed to self.mass_spectrum_obj.molecular_search_settings
1178        classes = MolecularCombinations(self.sql_db).runworker(
1179            mol_settings, print_time=mol_settings.verbose_processing
1180        )
1181
1182        try:
1183
1184            def apply_job(job):
1185                self.search_spectra_against_candidates(
1186                    mass_spectrum_list=mass_spectrum_list,
1187                    ms_peaks_list=ms_peaks_list,
1188                    candidate_formulas=job.candidate_formulas,
1189                    ion_type=job.ion_type,
1190                    ion_charge=job.ion_charge,
1191                    adduct_atom=job.adduct_atom,
1192                )
1193
1194            SearchMolecularFormulas._run_formula_search_jobs(
1195                classes,
1196                nominal_mzs,
1197                mol_settings,
1198                search_charges,
1199                self.sql_db,
1200                apply_job,
1201            )
1202        finally:
1203            self.sql_db.close()
1204
1205    def run_mass_feature_search(self):
1206        """Run the molecular formula search on the mass features.
1207
1208        Calls bulk_run_molecular_formula_search() with specified mass spectra and mass peaks.
1209        Multi-charge range comes from molecular search ``min_ion_charge`` /
1210        ``max_ion_charge``. Peak ``polarity`` is not rewritten.
1211        """
1212        mass_features_df = self.lcms_obj.mass_features_to_df()
1213
1214        # Get the list of mass spectrum (and peaks to search with each mass spectrum) for all mass features
1215        scan_list = mass_features_df.apex_scan.unique()
1216        mass_spectrum_list = [self.lcms_obj._ms[x] for x in scan_list]
1217        ms_peaks = []
1218        for scan in scan_list:
1219            mf_df_scan = mass_features_df[mass_features_df.apex_scan == scan]
1220            peaks_to_search = [
1221                self.lcms_obj.mass_features[x].ms1_peak
1222                for x in mf_df_scan.index.tolist()
1223            ]
1224            ms_peaks.append(peaks_to_search)
1225
1226        # Run the molecular formula search
1227        self.bulk_run_molecular_formula_search(mass_spectrum_list, ms_peaks)
1228
1229    def run_untargeted_worker_ms1(self):
1230        """Run untargeted molecular formula search on the ms1 mass spectrum."""
1231        raise NotImplementedError(
1232            "run_untargeted_worker_ms1 search is not implemented in CoreMS 3.0 and greater"
1233        )
1234
1235    def run_target_worker_ms1(self):
1236        """Run targeted molecular formula search on the ms1 mass spectrum."""
1237        raise NotImplementedError(
1238            "run_target_worker_ms1 formula search is not yet implemented in CoreMS 3.0 and greater"
1239        )
last_error = 0
last_dif = 0
closest_error = 0
error_average = 0
nbValues = 0
class FormulaSearchJob(typing.NamedTuple):
25class FormulaSearchJob(NamedTuple):
26    """One ion-type candidate batch for molecular formula search.
27
28    Parameters
29    ----------
30    ion_charge : int
31        Signed search charge for this batch.
32    ion_type : str
33        ``Labels`` ion type (protonated/deprotonated, radical, or adduct).
34    adduct_atom : str or None
35        Adduct element symbol when ``ion_type`` is adduct; otherwise None.
36    candidate_formulas : dict
37        Mapping of nominal m/z to formula objects, as consumed by ``run_search``.
38    progress : str
39        tqdm description for this class / ion type / charge.
40    """
41
42    ion_charge: int
43    ion_type: str
44    adduct_atom: Optional[str]
45    candidate_formulas: dict
46    progress: str

One ion-type candidate batch for molecular formula search.

Parameters
  • ion_charge (int): Signed search charge for this batch.
  • ion_type (str): Labels ion type (protonated/deprotonated, radical, or adduct).
  • adduct_atom (str or None): Adduct element symbol when ion_type is adduct; otherwise None.
  • candidate_formulas (dict): Mapping of nominal m/z to formula objects, as consumed by run_search.
  • progress (str): tqdm description for this class / ion type / charge.
FormulaSearchJob( ion_charge: int, ion_type: str, adduct_atom: Optional[str], candidate_formulas: dict, progress: str)

Create new instance of FormulaSearchJob(ion_charge, ion_type, adduct_atom, candidate_formulas, progress)

ion_charge: int

Alias for field number 0

ion_type: str

Alias for field number 1

adduct_atom: Optional[str]

Alias for field number 2

candidate_formulas: dict

Alias for field number 3

progress: str

Alias for field number 4

class SearchMolecularFormulas:
 49class SearchMolecularFormulas:
 50    """Class for searching molecular formulas in a mass spectrum.
 51
 52    Parameters
 53    ----------
 54    mass_spectrum_obj : MassSpectrum
 55        The mass spectrum object.
 56    sql_db : MolForm_SQL, optional
 57        The SQL database object, by default None.
 58    first_hit : bool, optional
 59        Flag to indicate whether to skip peaks that already have a molecular formula assigned, by default False.
 60    find_isotopologues : bool, optional
 61        Flag to indicate whether to find isotopologues, by default True.
 62
 63    Attributes
 64    ----------
 65    mass_spectrum_obj : MassSpectrum
 66        The mass spectrum object.
 67    sql_db : MolForm_SQL
 68        The SQL database object.
 69    first_hit : bool
 70        Flag to indicate whether to skip peaks that already have a molecular formula assigned.
 71    find_isotopologues : bool
 72        Flag to indicate whether to find isotopologues.
 73
 74
 75    Methods
 76    -------
 77    * run_search().
 78        Run the molecular formula search.
 79    * run_worker_mass_spectrum().
 80        Run the molecular formula search on the mass spectrum object.
 81    * run_worker_ms_peaks().
 82        Run the molecular formula search on the given list of mass spectrum peaks.
 83    * database_to_dict().
 84        Convert the database results to a dictionary.
 85    * run_molecular_formula().
 86        Run the molecular formula search on the given list of mass spectrum peaks.
 87    * search_mol_formulas().
 88        Search for molecular formulas in the mass spectrum.
 89    * ion_charges_for_search().
 90        Build the signed charge tuple from polarity and absolute min/max settings.
 91
 92    """
 93
 94    def __init__(
 95        self,
 96        mass_spectrum_obj,
 97        sql_db=None,
 98        first_hit: bool = False,
 99        find_isotopologues: bool = True,
100    ):
101        self.first_hit = first_hit
102
103        self.find_isotopologues = find_isotopologues
104
105        self.mass_spectrum_obj = mass_spectrum_obj
106
107        # Catch in-place usedAtoms mutations after settings construction
108        validate_used_atoms_keys(mass_spectrum_obj.molecular_search_settings.usedAtoms)
109
110        if not sql_db:
111            self.sql_db = MolForm_SQL(
112                url=mass_spectrum_obj.molecular_search_settings.url_database
113            )
114
115        else:
116            self.sql_db = sql_db
117
118    def __enter__(self):
119        """Open the SQL database connection."""
120        return self
121
122    def __exit__(self, exc_type, exc_val, exc_tb):
123        """Close the SQL database connection."""
124        self.sql_db.close()
125
126        return False
127
128    @staticmethod
129    def ion_charges_for_search(
130        polarity: Union[int, str],
131        min_ion_charge: int = 1,
132        max_ion_charge: int = 1,
133    ) -> Tuple[int, ...]:
134        """Signed ion charges to use for molecular formula search.
135
136        Absolute charges from ``min_ion_charge`` through ``max_ion_charge``
137        (inclusive) are multiplied by the polarity sign. No charge-state
138        determination is performed; every selected peak is searched at each
139        returned charge and candidates are ranked by formula scores.
140
141        Parameters
142        ----------
143        polarity : int or str
144            Mass-spectrum polarity as ``+1`` / ``-1``, or LCMS polarity string
145            ``"positive"`` / ``"negative"``.
146        min_ion_charge : int, optional
147            Minimum absolute charge (default 1).
148        max_ion_charge : int, optional
149            Maximum absolute charge (default 1). Must be >= ``min_ion_charge``.
150
151        Returns
152        -------
153        tuple of int
154            Signed charges sorted by ``(abs(z), z)`` so lower |z| is first
155            (relevant when ``first_hit=True``).
156
157        Raises
158        ------
159        ValueError
160            If polarity is invalid, min < 1, or max < min.
161        """
162        min_z = int(min_ion_charge)
163        max_z = int(max_ion_charge)
164        if min_z < 1:
165            raise ValueError("min_ion_charge must be >= 1")
166        if max_z < min_z:
167            raise ValueError("max_ion_charge must be >= min_ion_charge")
168
169        if isinstance(polarity, str):
170            p = polarity.strip().lower()
171            if p == "positive":
172                sign = 1
173            elif p == "negative":
174                sign = -1
175            else:
176                raise ValueError(
177                    "polarity string must be 'positive' or 'negative', "
178                    f"got {polarity!r}"
179                )
180        else:
181            pol = int(polarity)
182            if pol == 0:
183                raise ValueError("polarity must be non-zero")
184            sign = 1 if pol > 0 else -1
185
186        charges = [sign * z for z in range(min_z, max_z + 1)]
187        return tuple(sorted(charges, key=lambda z: (abs(z), z)))
188
189    @staticmethod
190    def _iter_ion_type_jobs(settings, dict_res, classe_str, ion_charge):
191        """Yield ion-type candidate jobs for one heteroatom class at one charge.
192
193        Encodes shared search policy: protonated and radical follow the
194        corresponding settings flags; adduct search runs only when adducts
195        are enabled and ``abs(ion_charge) == 1``. Empty candidate lists and
196        missing ``dict_res`` keys are skipped.
197
198        Parameters
199        ----------
200        settings : MolecularFormulaSearchSettings
201            Molecular formula search settings (ion-type flags).
202        dict_res : dict
203            Result of ``database_to_dict`` for this charge: protonated/radical
204            map class string to a nominal-m/z query dict; adduct maps adduct
205            atom to that class mapping.
206        classe_str : str
207            Heteroatom class key as stored in ``dict_res``.
208        ion_charge : int
209            Signed ion charge for this batch.
210
211        Yields
212        ------
213        FormulaSearchJob
214            One job per enabled ion type (and per adduct atom) with candidates.
215        """
216        if settings.isProtonated:
217            by_class = (dict_res.get(Labels.protonated_de_ion) or {}).get(classe_str)
218            if by_class:
219                yield FormulaSearchJob(
220                    ion_charge=ion_charge,
221                    ion_type=Labels.protonated_de_ion,
222                    adduct_atom=None,
223                    candidate_formulas=by_class,
224                    progress=(
225                        "Started molecular formula search for class %s, "
226                        "(de)protonated z=%s " % (classe_str, ion_charge)
227                    ),
228                )
229
230        if settings.isRadical:
231            by_class = (dict_res.get(Labels.radical_ion) or {}).get(classe_str)
232            if by_class:
233                yield FormulaSearchJob(
234                    ion_charge=ion_charge,
235                    ion_type=Labels.radical_ion,
236                    adduct_atom=None,
237                    candidate_formulas=by_class,
238                    progress=(
239                        "Started molecular formula search for class %s, "
240                        "radical z=%s " % (classe_str, ion_charge)
241                    ),
242                )
243
244        # Adduct search only at |z|==1; multi-charge adducts are not modeled.
245        if settings.isAdduct and abs(ion_charge) == 1:
246            dict_atoms_formulas = dict_res.get(Labels.adduct_ion) or {}
247            for adduct_atom, dict_by_class in dict_atoms_formulas.items():
248                by_class = dict_by_class.get(classe_str)
249                if by_class:
250                    yield FormulaSearchJob(
251                        ion_charge=ion_charge,
252                        ion_type=Labels.adduct_ion,
253                        adduct_atom=adduct_atom,
254                        candidate_formulas=by_class,
255                        progress=(
256                            "Started molecular formula search for class %s, "
257                            "adduct z=%s " % (classe_str, ion_charge)
258                        ),
259                    )
260
261    @staticmethod
262    def _run_formula_search_jobs(
263        classes,
264        nominal_mzs,
265        mf_search_settings,
266        search_charges,
267        sql_db,
268        apply_fn: Callable[[FormulaSearchJob], None],
269    ):
270        """Run charge × class × ion-type search, applying each candidate batch.
271
272        Loads the formula database once per charge per class chunk. Does not
273        close ``sql_db``. tqdm iterates class chunks; the description is the
274        job ``progress`` string.
275
276        Parameters
277        ----------
278        classes : list
279            Heteroatom class tuples from ``MolecularCombinations.runworker``.
280        nominal_mzs : list
281            Nominal m/z values to load from the database.
282        mf_search_settings : MolecularFormulaSearchSettings
283            Search settings (chunk size, ion-type flags, verbosity).
284        search_charges : sequence of int
285            Signed charges from ``ion_charges_for_search``.
286        sql_db : MolForm_SQL
287            Open formula database connection.
288        apply_fn : callable
289            Called with each ``FormulaSearchJob``. Path-specific: DI uses
290            ``run_search``; LC uses ``search_spectra_against_candidates``.
291        """
292        verbose = mf_search_settings.verbose_processing
293        for classe_chunk in chunks(classes, mf_search_settings.db_chunk_size):
294            classes_str_list = [class_tuple[0] for class_tuple in classe_chunk]
295            for ion_charge in search_charges:
296                dict_res = SearchMolecularFormulas.database_to_dict(
297                    classes_str_list,
298                    nominal_mzs,
299                    mf_search_settings,
300                    ion_charge,
301                    sql_db=sql_db,
302                )
303                pbar = tqdm.tqdm(classe_chunk, disable=not verbose)
304                for classe_tuple in pbar:
305                    classe_str = classe_tuple[0]
306                    for job in SearchMolecularFormulas._iter_ion_type_jobs(
307                        mf_search_settings, dict_res, classe_str, ion_charge
308                    ):
309                        pbar.set_description_str(desc=job.progress, refresh=True)
310                        apply_fn(job)
311
312    def run_search(
313        self,
314        mspeaks: list,
315        query: dict,
316        min_abundance: float,
317        ion_type: str,
318        ion_charge: int,
319        adduct_atom=None,
320    ):
321        """Run the molecular formula search.
322
323        Parameters
324        ----------
325        mspeaks : list of MSPeak
326            The list of mass spectrum peaks.
327        query : dict
328            The query dictionary containing the possible molecular formulas.
329        min_abundance : float
330            The minimum abundance threshold.
331        ion_type : str
332            The ion type.
333        ion_charge : int
334            The ion charge.
335        adduct_atom : str, optional
336            The adduct atom, by default None.
337        """
338
339        def get_formulas(nominal_overlay: float = 0.1):
340            """
341            Get the list of formulas based on the nominal overlay.
342
343            Parameters
344            ----------
345            nominal_overlay : float, optional
346                The nominal overlay, by default 0.1.
347
348            Returns
349            -------
350            list
351                The list of formulas.
352            """
353            nominal_mz = ms_peak.nominal_mz_exp
354
355            defect_mass = ms_peak.mz_exp - nominal_mz
356            nominal_masses = [nominal_mz]
357
358            if (defect_mass) >= 1 - nominal_overlay:
359                nominal_masses.append(nominal_mz + 1)
360            elif (defect_mass) <= nominal_overlay:
361                nominal_masses.append(nominal_mz - 1)
362
363            list_formulas_candidates = []
364
365            for nominal_mass in nominal_masses:
366                if nominal_mass in query.keys():
367                    list_formulas_candidates.extend(query.get(nominal_mass))
368
369            return list_formulas_candidates
370
371        all_assigned_indexes = list()
372
373        # molecular_search_settings = self.mass_spectrum_obj.molecular_search_settings
374
375        search_molfrom = SearchMolecularFormulaWorker(
376            find_isotopologues=self.find_isotopologues
377        )
378
379        for ms_peak in mspeaks:
380            # already assigned a molecular formula
381            if self.first_hit:
382                if ms_peak.is_assigned:
383                    continue
384
385            ms_peak_indexes = search_molfrom.find_formulas(
386                get_formulas(),
387                min_abundance,
388                self.mass_spectrum_obj,
389                ms_peak,
390                ion_type,
391                ion_charge,
392                adduct_atom,
393            )
394
395            all_assigned_indexes.extend(ms_peak_indexes)
396
397        # all_assigned_indexes = MolecularFormulaSearchFilters().filter_isotopologue(all_assigned_indexes, self.mass_spectrum_obj)
398
399        # all_assigned_indexes = MolecularFormulaSearchFilters().filter_kendrick(all_assigned_indexes, self.mass_spectrum_obj)
400
401        # MolecularFormulaSearchFilters().check_min_peaks(all_assigned_indexes, self.mass_spectrum_obj)
402        # filter per min peaks per mono isotopic class
403
404    def run_worker_mass_spectrum(self):
405        """Run the molecular formula search on the mass spectrum object."""
406        self.run_molecular_formula(
407            self.mass_spectrum_obj.sort_by_abundance(),
408            print_time=self.mass_spectrum_obj.molecular_search_settings.verbose_processing,
409        )
410
411    def run_worker_ms_peaks(self, ms_peaks):
412        """Run the molecular formula search on the given list of mass spectrum peaks.
413
414        Parameters
415        ----------
416        ms_peaks : list of MSPeak
417            The list of mass spectrum peaks.
418        """
419        self.run_molecular_formula(
420            ms_peaks,
421            print_time=self.mass_spectrum_obj.molecular_search_settings.verbose_processing,
422        )
423
424    @staticmethod
425    def database_to_dict(
426        classe_str_list,
427        nominal_mzs,
428        mf_search_settings,
429        ion_charge,
430        sql_db=None,
431    ):
432        """Convert the database results to a dictionary.
433
434        Parameters
435        ----------
436        classe_str_list : list
437            The list of class strings.
438        nominal_mzs : list
439            The list of nominal m/z values.
440        mf_search_settings : MolecularFormulaSearchSettings
441            The molecular formula search settings.
442        ion_charge : int
443            The ion charge.
444        sql_db : MolForm_SQL, optional
445            The SQL database object, by default None. If None, a new MolForm_SQL object will be created.
446
447        Returns
448        -------
449        dict
450            The dictionary containing the database results.
451        """
452        owns_db = sql_db is None
453        if owns_db:
454            sql_db = MolForm_SQL(url=mf_search_settings.url_database)
455
456        try:
457            dict_res = {}
458
459            if mf_search_settings.isProtonated:
460                dict_res[Labels.protonated_de_ion] = sql_db.get_dict_by_classes(
461                    classe_str_list,
462                    Labels.protonated_de_ion,
463                    nominal_mzs,
464                    ion_charge,
465                    mf_search_settings,
466                )
467
468            if mf_search_settings.isRadical:
469                dict_res[Labels.radical_ion] = sql_db.get_dict_by_classes(
470                    classe_str_list,
471                    Labels.radical_ion,
472                    nominal_mzs,
473                    ion_charge,
474                    mf_search_settings,
475                )
476
477            if mf_search_settings.isAdduct:
478                adduct_list = (
479                    mf_search_settings.adduct_atoms_neg
480                    if ion_charge < 0
481                    else mf_search_settings.adduct_atoms_pos
482                )
483                dict_res[Labels.adduct_ion] = sql_db.get_dict_by_classes(
484                    classe_str_list,
485                    Labels.adduct_ion,
486                    nominal_mzs,
487                    ion_charge,
488                    mf_search_settings,
489                    adducts=adduct_list,
490                )
491
492            return dict_res
493        finally:
494            if owns_db:
495                sql_db.close()
496
497    @timeit(print_time=True)
498    def run_molecular_formula(self, ms_peaks, **kwargs):
499        """Run the molecular formula search on the given list of mass spectrum peaks.
500
501        Parameters
502        ----------
503        ms_peaks : list of MSPeak
504            The list of mass spectrum peaks.
505        **kwargs
506            Additional keyword arguments.
507            Most notably, print_time, which is a boolean flag to indicate whether to print the time
508            and passed to the timeit decorator.
509        """
510        settings = self.mass_spectrum_obj.molecular_search_settings
511        # Re-check after possible post-construction mutation of settings fields
512        settings.validate_ion_charge_settings()
513        # Polarity must come from the mass spectrum (DI often -1 from data).
514        # Do not default to +1; LC already requires polarity on the LCMS object.
515        polarity_raw = self.mass_spectrum_obj.polarity
516        if polarity_raw is None:
517            raise ValueError(
518                "Mass spectrum polarity must be set from the data before "
519                "formula search (e.g. +1 or -1). There is no default polarity."
520            )
521        try:
522            polarity = int(polarity_raw)
523        except (TypeError, ValueError) as exc:
524            raise ValueError(
525                "Mass spectrum polarity must be an integer +1 or -1 "
526                f"(got {polarity_raw!r})."
527            ) from exc
528        if polarity == 0:
529            raise ValueError("Mass spectrum polarity must be non-zero (+1 or -1).")
530        # Multi-charge formula search: expand absolute min..max (default 1..1).
531        # Does not rewrite MSPeak.polarity (remains ±1).
532        search_charges = self.ion_charges_for_search(
533            polarity,
534            min_ion_charge=settings.min_ion_charge,
535            max_ion_charge=settings.max_ion_charge,
536        )
537        min_abundance = self.mass_spectrum_obj.min_abundance
538        nominal_mzs = self.mass_spectrum_obj.nominal_mz
539
540        # reset average error, only relevant is average mass error method is being used
541        SearchMolecularFormulaWorker(
542            find_isotopologues=self.find_isotopologues
543        ).reset_error(self.mass_spectrum_obj)
544
545        # check database for all possible molecular formula combinations based on the setting passed to self.mass_spectrum_obj.molecular_search_settings
546        classes = MolecularCombinations(self.sql_db).runworker(
547            settings, print_time=settings.verbose_processing
548        )
549
550        def apply_job(job):
551            self.run_search(
552                ms_peaks,
553                job.candidate_formulas,
554                min_abundance,
555                job.ion_type,
556                job.ion_charge,
557                adduct_atom=job.adduct_atom,
558            )
559
560        self._run_formula_search_jobs(
561            classes,
562            nominal_mzs,
563            settings,
564            search_charges,
565            self.sql_db,
566            apply_job,
567        )
568        self.sql_db.close()
569
570    def search_mol_formulas(
571        self,
572        possible_formulas_list: List[MolecularFormula],
573        ion_type: str,
574        neutral_molform=True,
575        find_isotopologues=True,
576        adduct_atom=None,
577    ) -> List[_MSPeak]:
578        """Search for molecular formulas in the mass spectrum.
579
580        Parameters
581        ----------
582        possible_formulas_list : list of MolecularFormula
583            The list of possible molecular formulas.
584        ion_type : str
585            The ion type.
586        neutral_molform : bool, optional
587            Flag to indicate whether the molecular formulas are neutral, by default True.
588        find_isotopologues : bool, optional
589            Flag to indicate whether to find isotopologues, by default True.
590        adduct_atom : str, optional
591            The adduct atom, by default None.
592
593        Returns
594        -------
595        list of MSPeak
596            The list of mass spectrum peaks with assigned molecular formulas.
597        """
598        # neutral_molform: some reference files already present the formula on ion mode, for instance, bruker reference files
599        #    if that is the case than turn neutral_molform off
600
601        SearchMolecularFormulaWorker(find_isotopologues=find_isotopologues).reset_error(
602            self.mass_spectrum_obj
603        )
604
605        initial_min_peak_bool = (
606            self.mass_spectrum_obj.molecular_search_settings.use_min_peaks_filter
607        )
608        initial_runtime_kendrick_filter = (
609            self.mass_spectrum_obj.molecular_search_settings.use_runtime_kendrick_filter
610        )
611
612        # Are the following 3 lines redundant?
613        self.mass_spectrum_obj.molecular_search_settings.use_min_peaks_filter = False
614        self.mass_spectrum_obj.molecular_search_settings.use_min_peaks_filter = (
615            False  # TODO check this line
616        )
617        self.mass_spectrum_obj.molecular_search_settings.use_runtime_kendrick_filter = (
618            False
619        )
620
621        possible_formulas_dict_nm = {}
622
623        for mf in possible_formulas_list:
624            if neutral_molform:
625                # Bin by the nominal m/z of the requested ion hypothesis so
626                # candidates land in the same bin run_search looks them up under.
627                if ion_type == Labels.radical_ion:
628                    nm = int(mf.radical_mz)
629                elif ion_type == Labels.adduct_ion and adduct_atom:
630                    nm = int(mf.adduct_mz(adduct_atom))
631                else:
632                    nm = int(mf.protonated_mz)
633            else:
634                nm = int(mf.mz_nominal_calc)
635
636            if nm in possible_formulas_dict_nm.keys():
637                possible_formulas_dict_nm[nm].append(mf)
638
639            else:
640                possible_formulas_dict_nm[nm] = [mf]
641
642        min_abundance = self.mass_spectrum_obj.min_abundance
643
644        ion_type = ion_type
645
646        self.run_search(
647            self.mass_spectrum_obj,
648            possible_formulas_dict_nm,
649            min_abundance,
650            ion_type,
651            self.mass_spectrum_obj.polarity,
652            adduct_atom=adduct_atom,
653        )
654
655        self.mass_spectrum_obj.molecular_search_settings.use_min_peaks_filter = (
656            initial_min_peak_bool
657        )
658        self.mass_spectrum_obj.molecular_search_settings.use_runtime_kendrick_filter = (
659            initial_runtime_kendrick_filter
660        )
661
662        mspeaks = [mspeak for mspeak in self.mass_spectrum_obj if mspeak.is_assigned]
663
664        self.sql_db.close()
665
666        return mspeaks

Class for searching molecular formulas in a mass spectrum.

Parameters
  • mass_spectrum_obj (MassSpectrum): The mass spectrum object.
  • sql_db (MolForm_SQL, optional): The SQL database object, by default None.
  • first_hit (bool, optional): Flag to indicate whether to skip peaks that already have a molecular formula assigned, by default False.
  • find_isotopologues (bool, optional): Flag to indicate whether to find isotopologues, by default True.
Attributes
  • mass_spectrum_obj (MassSpectrum): The mass spectrum object.
  • sql_db (MolForm_SQL): The SQL database object.
  • first_hit (bool): Flag to indicate whether to skip peaks that already have a molecular formula assigned.
  • find_isotopologues (bool): Flag to indicate whether to find isotopologues.
Methods
  • run_search(). Run the molecular formula search.
  • run_worker_mass_spectrum(). Run the molecular formula search on the mass spectrum object.
  • run_worker_ms_peaks(). Run the molecular formula search on the given list of mass spectrum peaks.
  • database_to_dict(). Convert the database results to a dictionary.
  • run_molecular_formula(). Run the molecular formula search on the given list of mass spectrum peaks.
  • search_mol_formulas(). Search for molecular formulas in the mass spectrum.
  • ion_charges_for_search(). Build the signed charge tuple from polarity and absolute min/max settings.
SearchMolecularFormulas( mass_spectrum_obj, sql_db=None, first_hit: bool = False, find_isotopologues: bool = True)
 94    def __init__(
 95        self,
 96        mass_spectrum_obj,
 97        sql_db=None,
 98        first_hit: bool = False,
 99        find_isotopologues: bool = True,
100    ):
101        self.first_hit = first_hit
102
103        self.find_isotopologues = find_isotopologues
104
105        self.mass_spectrum_obj = mass_spectrum_obj
106
107        # Catch in-place usedAtoms mutations after settings construction
108        validate_used_atoms_keys(mass_spectrum_obj.molecular_search_settings.usedAtoms)
109
110        if not sql_db:
111            self.sql_db = MolForm_SQL(
112                url=mass_spectrum_obj.molecular_search_settings.url_database
113            )
114
115        else:
116            self.sql_db = sql_db
first_hit
find_isotopologues
mass_spectrum_obj
def run_worker_mass_spectrum(self):
404    def run_worker_mass_spectrum(self):
405        """Run the molecular formula search on the mass spectrum object."""
406        self.run_molecular_formula(
407            self.mass_spectrum_obj.sort_by_abundance(),
408            print_time=self.mass_spectrum_obj.molecular_search_settings.verbose_processing,
409        )

Run the molecular formula search on the mass spectrum object.

def run_worker_ms_peaks(self, ms_peaks):
411    def run_worker_ms_peaks(self, ms_peaks):
412        """Run the molecular formula search on the given list of mass spectrum peaks.
413
414        Parameters
415        ----------
416        ms_peaks : list of MSPeak
417            The list of mass spectrum peaks.
418        """
419        self.run_molecular_formula(
420            ms_peaks,
421            print_time=self.mass_spectrum_obj.molecular_search_settings.verbose_processing,
422        )

Run the molecular formula search on the given list of mass spectrum peaks.

Parameters
  • ms_peaks (list of MSPeak): The list of mass spectrum peaks.
@staticmethod
def database_to_dict( classe_str_list, nominal_mzs, mf_search_settings, ion_charge, sql_db=None):
424    @staticmethod
425    def database_to_dict(
426        classe_str_list,
427        nominal_mzs,
428        mf_search_settings,
429        ion_charge,
430        sql_db=None,
431    ):
432        """Convert the database results to a dictionary.
433
434        Parameters
435        ----------
436        classe_str_list : list
437            The list of class strings.
438        nominal_mzs : list
439            The list of nominal m/z values.
440        mf_search_settings : MolecularFormulaSearchSettings
441            The molecular formula search settings.
442        ion_charge : int
443            The ion charge.
444        sql_db : MolForm_SQL, optional
445            The SQL database object, by default None. If None, a new MolForm_SQL object will be created.
446
447        Returns
448        -------
449        dict
450            The dictionary containing the database results.
451        """
452        owns_db = sql_db is None
453        if owns_db:
454            sql_db = MolForm_SQL(url=mf_search_settings.url_database)
455
456        try:
457            dict_res = {}
458
459            if mf_search_settings.isProtonated:
460                dict_res[Labels.protonated_de_ion] = sql_db.get_dict_by_classes(
461                    classe_str_list,
462                    Labels.protonated_de_ion,
463                    nominal_mzs,
464                    ion_charge,
465                    mf_search_settings,
466                )
467
468            if mf_search_settings.isRadical:
469                dict_res[Labels.radical_ion] = sql_db.get_dict_by_classes(
470                    classe_str_list,
471                    Labels.radical_ion,
472                    nominal_mzs,
473                    ion_charge,
474                    mf_search_settings,
475                )
476
477            if mf_search_settings.isAdduct:
478                adduct_list = (
479                    mf_search_settings.adduct_atoms_neg
480                    if ion_charge < 0
481                    else mf_search_settings.adduct_atoms_pos
482                )
483                dict_res[Labels.adduct_ion] = sql_db.get_dict_by_classes(
484                    classe_str_list,
485                    Labels.adduct_ion,
486                    nominal_mzs,
487                    ion_charge,
488                    mf_search_settings,
489                    adducts=adduct_list,
490                )
491
492            return dict_res
493        finally:
494            if owns_db:
495                sql_db.close()

Convert the database results to a dictionary.

Parameters
  • classe_str_list (list): The list of class strings.
  • nominal_mzs (list): The list of nominal m/z values.
  • mf_search_settings (MolecularFormulaSearchSettings): The molecular formula search settings.
  • ion_charge (int): The ion charge.
  • sql_db (MolForm_SQL, optional): The SQL database object, by default None. If None, a new MolForm_SQL object will be created.
Returns
  • dict: The dictionary containing the database results.
def run_molecular_formula(*args, **kw):
39        def timed(*args, **kw):
40            # Extract print_time from kwargs if provided
41            local_print_time = kw.pop("print_time", print_time)
42            ts = time.time()
43            result = method(*args, **kw)
44            te = time.time()
45            if "log_time" in kw:
46                name = kw.get("log_name", method.__name__.upper())
47                kw["log_time"][name] = int((te - ts) * 1000)
48            elif local_print_time:
49                print("%r  %2.2f ms" % (method.__name__, (te - ts) * 1000))
50            return result

Run the molecular formula search on the given list of mass spectrum peaks.

Parameters
  • ms_peaks (list of MSPeak): The list of mass spectrum peaks.
  • **kwargs: Additional keyword arguments. Most notably, print_time, which is a boolean flag to indicate whether to print the time and passed to the timeit decorator.
def search_mol_formulas( self, possible_formulas_list: List[corems.molecular_formula.factory.MolecularFormulaFactory.MolecularFormula], ion_type: str, neutral_molform=True, find_isotopologues=True, adduct_atom=None) -> List[corems.ms_peak.factory.MSPeakClasses._MSPeak]:
570    def search_mol_formulas(
571        self,
572        possible_formulas_list: List[MolecularFormula],
573        ion_type: str,
574        neutral_molform=True,
575        find_isotopologues=True,
576        adduct_atom=None,
577    ) -> List[_MSPeak]:
578        """Search for molecular formulas in the mass spectrum.
579
580        Parameters
581        ----------
582        possible_formulas_list : list of MolecularFormula
583            The list of possible molecular formulas.
584        ion_type : str
585            The ion type.
586        neutral_molform : bool, optional
587            Flag to indicate whether the molecular formulas are neutral, by default True.
588        find_isotopologues : bool, optional
589            Flag to indicate whether to find isotopologues, by default True.
590        adduct_atom : str, optional
591            The adduct atom, by default None.
592
593        Returns
594        -------
595        list of MSPeak
596            The list of mass spectrum peaks with assigned molecular formulas.
597        """
598        # neutral_molform: some reference files already present the formula on ion mode, for instance, bruker reference files
599        #    if that is the case than turn neutral_molform off
600
601        SearchMolecularFormulaWorker(find_isotopologues=find_isotopologues).reset_error(
602            self.mass_spectrum_obj
603        )
604
605        initial_min_peak_bool = (
606            self.mass_spectrum_obj.molecular_search_settings.use_min_peaks_filter
607        )
608        initial_runtime_kendrick_filter = (
609            self.mass_spectrum_obj.molecular_search_settings.use_runtime_kendrick_filter
610        )
611
612        # Are the following 3 lines redundant?
613        self.mass_spectrum_obj.molecular_search_settings.use_min_peaks_filter = False
614        self.mass_spectrum_obj.molecular_search_settings.use_min_peaks_filter = (
615            False  # TODO check this line
616        )
617        self.mass_spectrum_obj.molecular_search_settings.use_runtime_kendrick_filter = (
618            False
619        )
620
621        possible_formulas_dict_nm = {}
622
623        for mf in possible_formulas_list:
624            if neutral_molform:
625                # Bin by the nominal m/z of the requested ion hypothesis so
626                # candidates land in the same bin run_search looks them up under.
627                if ion_type == Labels.radical_ion:
628                    nm = int(mf.radical_mz)
629                elif ion_type == Labels.adduct_ion and adduct_atom:
630                    nm = int(mf.adduct_mz(adduct_atom))
631                else:
632                    nm = int(mf.protonated_mz)
633            else:
634                nm = int(mf.mz_nominal_calc)
635
636            if nm in possible_formulas_dict_nm.keys():
637                possible_formulas_dict_nm[nm].append(mf)
638
639            else:
640                possible_formulas_dict_nm[nm] = [mf]
641
642        min_abundance = self.mass_spectrum_obj.min_abundance
643
644        ion_type = ion_type
645
646        self.run_search(
647            self.mass_spectrum_obj,
648            possible_formulas_dict_nm,
649            min_abundance,
650            ion_type,
651            self.mass_spectrum_obj.polarity,
652            adduct_atom=adduct_atom,
653        )
654
655        self.mass_spectrum_obj.molecular_search_settings.use_min_peaks_filter = (
656            initial_min_peak_bool
657        )
658        self.mass_spectrum_obj.molecular_search_settings.use_runtime_kendrick_filter = (
659            initial_runtime_kendrick_filter
660        )
661
662        mspeaks = [mspeak for mspeak in self.mass_spectrum_obj if mspeak.is_assigned]
663
664        self.sql_db.close()
665
666        return mspeaks

Search for molecular formulas in the mass spectrum.

Parameters
  • possible_formulas_list (list of MolecularFormula): The list of possible molecular formulas.
  • ion_type (str): The ion type.
  • neutral_molform (bool, optional): Flag to indicate whether the molecular formulas are neutral, by default True.
  • find_isotopologues (bool, optional): Flag to indicate whether to find isotopologues, by default True.
  • adduct_atom (str, optional): The adduct atom, by default None.
Returns
  • list of MSPeak: The list of mass spectrum peaks with assigned molecular formulas.
class SearchMolecularFormulaWorker:
 669class SearchMolecularFormulaWorker:
 670    """Class for searching molecular formulas in a mass spectrum.
 671
 672    Parameters
 673    ----------
 674    find_isotopologues : bool, optional
 675        Flag to indicate whether to find isotopologues, by default True.
 676
 677    Attributes
 678    ----------
 679    find_isotopologues : bool
 680        Flag to indicate whether to find isotopologues.
 681
 682    Methods
 683    -------
 684    * reset_error().
 685        Reset the error variables.
 686    * set_last_error().
 687        Set the last error.
 688    * find_formulas().
 689        Find the formulas.
 690    * calc_error().
 691        Calculate the error.
 692    """
 693
 694    # TODO add reset error function
 695    # needs this wraper to pass the class to multiprocessing
 696
 697    def __init__(self, find_isotopologues=True):
 698        self.find_isotopologues = find_isotopologues
 699
 700    def __call__(self, args):
 701        """Call the find formulas function.
 702
 703        Parameters
 704        ----------
 705        args : tuple
 706            The arguments.
 707
 708        Returns
 709        -------
 710        list
 711            The list of mass spectrum peaks with assigned molecular formulas.
 712        """
 713        return self.find_formulas(*args)  # ,args[1]
 714
 715    def reset_error(self, mass_spectrum_obj):
 716        """Reset the error variables.
 717
 718        Parameters
 719        ----------
 720        mass_spectrum_obj : MassSpectrum
 721            The mass spectrum object.
 722
 723        Notes
 724        -----
 725        This function resets the error variables for the given mass spectrum object.
 726        """
 727        global last_error, last_dif, closest_error, error_average, nbValues
 728        last_error, last_dif, closest_error, nbValues = 0.0, 0.0, 0.0, 0.0
 729
 730    def set_last_error(self, error, mass_spectrum_obj):
 731        """Set the last error.
 732
 733        Parameters
 734        ----------
 735        error : float
 736            The error.
 737        mass_spectrum_obj : MassSpectrum
 738            The mass spectrum object.
 739        """
 740        # set the changes to the global variables, not internal ones
 741        global last_error, last_dif, closest_error, error_average, nbValues
 742
 743        if mass_spectrum_obj.molecular_search_settings.error_method == "distance":
 744            dif = error - last_error
 745            if dif < last_dif:
 746                last_dif = dif
 747                closest_error = error
 748                mass_spectrum_obj.molecular_search_settings.min_ppm_error = (
 749                    closest_error
 750                    - mass_spectrum_obj.molecular_search_settings.mz_error_range
 751                )
 752                mass_spectrum_obj.molecular_search_settings.max_ppm_error = (
 753                    closest_error
 754                    + mass_spectrum_obj.molecular_search_settings.mz_error_range
 755                )
 756
 757        elif mass_spectrum_obj.molecular_search_settings.error_method == "lowest":
 758            if error < last_error:
 759                mass_spectrum_obj.molecular_search_settings.min_ppm_error = (
 760                    error - mass_spectrum_obj.molecular_search_settings.mz_error_range
 761                )
 762                mass_spectrum_obj.molecular_search_settings.max_ppm_error = (
 763                    error + mass_spectrum_obj.molecular_search_settings.mz_error_range
 764                )
 765                last_error = error
 766
 767        elif mass_spectrum_obj.molecular_search_settings.error_method == "symmetrical":
 768            mass_spectrum_obj.molecular_search_settings.min_ppm_error = (
 769                mass_spectrum_obj.molecular_search_settings.mz_error_average
 770                - mass_spectrum_obj.molecular_search_settings.mz_error_range
 771            )
 772            mass_spectrum_obj.molecular_search_settings.max_ppm_error = (
 773                mass_spectrum_obj.molecular_search_settings.mz_error_average
 774                + mass_spectrum_obj.molecular_search_settings.mz_error_range
 775            )
 776
 777        elif mass_spectrum_obj.molecular_search_settings.error_method == "average":
 778            nbValues += 1
 779            error_average = error_average + ((error - error_average) / nbValues)
 780            mass_spectrum_obj.molecular_search_settings.min_ppm_error = (
 781                error_average
 782                - mass_spectrum_obj.molecular_search_settings.mz_error_range
 783            )
 784            mass_spectrum_obj.molecular_search_settings.max_ppm_error = (
 785                error_average
 786                + mass_spectrum_obj.molecular_search_settings.mz_error_range
 787            )
 788
 789        else:
 790            # using set mass_spectrum_obj.molecular_search_settings.min_ppm_error  and max_ppm_error range
 791            pass
 792
 793        # returns the error based on the selected method at mass_spectrum_obj.molecular_search_settings.method
 794
 795    @staticmethod
 796    def calc_error(mz_exp, mz_calc, method="ppm"):
 797        """Calculate the error.
 798
 799        Parameters
 800        ----------
 801        mz_exp : float
 802            The experimental m/z value.
 803        mz_calc : float
 804            The calculated m/z value.
 805        method : str, optional
 806            The method, by default 'ppm'.
 807
 808        Raises
 809        -------
 810        Exception
 811            If the method is not ppm or ppb.
 812
 813        Returns
 814        -------
 815        float
 816            The error.
 817        """
 818
 819        if method == "ppm":
 820            multi_factor = 1_000_000
 821
 822        elif method == "ppb":
 823            multi_factor = 1_000_000_000
 824
 825        elif method == "perc":
 826            multi_factor = 100
 827
 828        else:
 829            raise Exception(
 830                "method needs to be ppm or ppb, you have entered %s" % method
 831            )
 832
 833        if mz_exp:
 834            return ((mz_exp - mz_calc) / mz_calc) * multi_factor
 835
 836        else:
 837            raise Exception("Please set mz_calc first")
 838
 839    def find_formulas(
 840        self,
 841        formulas,
 842        min_abundance,
 843        mass_spectrum_obj,
 844        ms_peak,
 845        ion_type,
 846        ion_charge,
 847        adduct_atom=None,
 848    ):
 849        """Find the formulas.
 850
 851        Parameters
 852        ----------
 853        formulas : list of MolecularFormula
 854            The list of molecular formulas.
 855        min_abundance : float
 856            The minimum abundance threshold.
 857        mass_spectrum_obj : MassSpectrum
 858            The mass spectrum object.
 859        ms_peak : MSPeak
 860            The mass spectrum peak.
 861        ion_type : str
 862            The ion type.
 863        ion_charge : int
 864            The ion charge.
 865        adduct_atom : str, optional
 866            The adduct atom, by default None.
 867
 868        Returns
 869        -------
 870        list of MSPeak
 871            The list of mass spectrum peaks with assigned molecular formulas.
 872
 873        Notes
 874        -----
 875        Uses the closest error the next search (this is not ideal, it needs to use confidence
 876        metric to choose the right candidate then propagate the error using the error from the best candidate).
 877        It needs to add s/n to the equation.
 878        It need optimization to define the mz_error_range within a m/z unit since it is directly proportional
 879        with the mass, and inversely proportional to the rp. It's not linear, i.e., sigma mass.
 880        The idea it to correlate sigma to resolving power, signal to noise and sample complexity per mz unit.
 881        Method='distance'
 882        """
 883        mspeak_assigned_index = list()
 884
 885        min_ppm_error = mass_spectrum_obj.molecular_search_settings.min_ppm_error
 886        max_ppm_error = mass_spectrum_obj.molecular_search_settings.max_ppm_error
 887
 888        min_abun_error = mass_spectrum_obj.molecular_search_settings.min_abun_error
 889        max_abun_error = mass_spectrum_obj.molecular_search_settings.max_abun_error
 890
 891        # f = open("abundance_error.txt", "a+")
 892        ms_peak_mz_exp, ms_peak_abundance = ms_peak.mz_exp, ms_peak.abundance
 893        # min_error = min([pmf.mz_error for pmf in possible_formulas])
 894
 895        def mass_by_ion_type(possible_formula_obj):
 896            if ion_type == Labels.protonated_de_ion:
 897                return possible_formula_obj._protonated_mz(ion_charge)
 898
 899            elif ion_type == Labels.radical_ion:
 900                return possible_formula_obj._radical_mz(ion_charge)
 901
 902            elif ion_type == Labels.adduct_ion and adduct_atom:
 903                # Positional argument order differs between formula objects
 904                # and SQL links; keywords are unambiguous.
 905                return possible_formula_obj._adduct_mz(
 906                    ion_charge=ion_charge, adduct_atom=adduct_atom
 907                )
 908
 909            else:
 910                # will return externally calculated mz if is set, #use on Bruker Reference list import
 911                # if the ion type is known the ion mass based on molecular formula ion type
 912                # if ion type is unknow will return neutral mass
 913                return possible_formula_obj.mz_calc
 914
 915        for possible_formula in formulas:
 916            if possible_formula:
 917                error = self.calc_error(
 918                    ms_peak_mz_exp, mass_by_ion_type(possible_formula)
 919                )
 920
 921                # error = possible_formula.mz_error
 922
 923                if min_ppm_error <= error <= max_ppm_error:
 924                    # update the error
 925
 926                    self.set_last_error(error, mass_spectrum_obj)
 927
 928                    # add molecular formula match to ms_peak
 929
 930                    # get molecular formula dict from sql obj
 931                    # formula_dict = pickle.loads(possible_formula.mol_formula)
 932
 933                    formula_dict = possible_formula.to_dict()
 934                    # create the molecular formula obj to be stored
 935                    molecular_formula = MolecularFormula(
 936                        formula_dict,
 937                        ion_charge,
 938                        ion_type=ion_type,
 939                        adduct_atom=adduct_atom,
 940                    )
 941                    # add the molecular formula obj to the mspeak obj
 942                    # add the mspeak obj and it's index for tracking next assignment step
 943
 944                    if self.find_isotopologues:
 945                        # calculates isotopologues
 946                        isotopologues = molecular_formula.isotopologues(
 947                            min_abundance,
 948                            ms_peak_abundance,
 949                            mass_spectrum_obj.dynamic_range,
 950                        )
 951
 952                        # search for isotopologues
 953                        for isotopologue_formula in isotopologues:
 954                            molecular_formula.expected_isotopologues.append(
 955                                isotopologue_formula
 956                            )
 957                            # move this outside to improve preformace
 958                            # we need to increase the search space to -+1 m_z
 959                            first_index, last_index = (
 960                                mass_spectrum_obj.get_nominal_mz_first_last_indexes(
 961                                    isotopologue_formula.mz_nominal_calc
 962                                )
 963                            )
 964
 965                            for ms_peak_iso in mass_spectrum_obj[
 966                                first_index:last_index
 967                            ]:
 968                                error = self.calc_error(
 969                                    ms_peak_iso.mz_exp, isotopologue_formula.mz_calc
 970                                )
 971
 972                                if min_ppm_error <= error <= max_ppm_error:
 973                                    # need to define error distribution for abundance measurements
 974
 975                                    # if mass_spectrum_obj.is_centroid:
 976
 977                                    abundance_error = self.calc_error(
 978                                        isotopologue_formula.abundance_calc,
 979                                        ms_peak_iso.abundance,
 980                                        method="perc",
 981                                    )
 982
 983                                    # area_error = self.calc_error(ms_peak.area, ms_peak_iso.area, method='perc')
 984
 985                                    # margin of error was set empirically/ needs statistical calculation
 986                                    #  of margin of error for the measurement of the abundances
 987                                    if (
 988                                        min_abun_error
 989                                        <= abundance_error
 990                                        <= max_abun_error
 991                                    ):
 992                                        # update the error
 993
 994                                        self.set_last_error(error, mass_spectrum_obj)
 995
 996                                        # isotopologue_formula.mz_error = error
 997
 998                                        # isotopologue_formula.area_error = area_error
 999
1000                                        # isotopologue_formula.abundance_error = abundance_error
1001
1002                                        isotopologue_formula.mspeak_index_mono_isotopic = ms_peak.index
1003
1004                                        mono_isotopic_formula_index = len(ms_peak)
1005
1006                                        isotopologue_formula.mspeak_index_mono_isotopic = ms_peak.index
1007
1008                                        isotopologue_formula.mono_isotopic_formula_index = mono_isotopic_formula_index
1009
1010                                        # add mspeaks isotopologue index to the mono isotopic MolecularFormula obj and the respective formula position
1011
1012                                        # add molecular formula match to ms_peak
1013                                        x = ms_peak_iso.add_molecular_formula(
1014                                            isotopologue_formula
1015                                        )
1016
1017                                        molecular_formula.mspeak_mf_isotopologues_indexes.append(
1018                                            (ms_peak_iso.index, x)
1019                                        )
1020                                        # add mspeaks mono isotopic index to the isotopologue MolecularFormula obj
1021
1022                    y = ms_peak.add_molecular_formula(molecular_formula)
1023
1024                    mspeak_assigned_index.append((ms_peak.index, y))
1025
1026        return mspeak_assigned_index

Class for searching molecular formulas in a mass spectrum.

Parameters
  • find_isotopologues (bool, optional): Flag to indicate whether to find isotopologues, by default True.
Attributes
  • find_isotopologues (bool): Flag to indicate whether to find isotopologues.
Methods
  • reset_error(). Reset the error variables.
  • set_last_error(). Set the last error.
  • find_formulas(). Find the formulas.
  • calc_error(). Calculate the error.
SearchMolecularFormulaWorker(find_isotopologues=True)
697    def __init__(self, find_isotopologues=True):
698        self.find_isotopologues = find_isotopologues
find_isotopologues
def reset_error(self, mass_spectrum_obj):
715    def reset_error(self, mass_spectrum_obj):
716        """Reset the error variables.
717
718        Parameters
719        ----------
720        mass_spectrum_obj : MassSpectrum
721            The mass spectrum object.
722
723        Notes
724        -----
725        This function resets the error variables for the given mass spectrum object.
726        """
727        global last_error, last_dif, closest_error, error_average, nbValues
728        last_error, last_dif, closest_error, nbValues = 0.0, 0.0, 0.0, 0.0

Reset the error variables.

Parameters
  • mass_spectrum_obj (MassSpectrum): The mass spectrum object.
Notes

This function resets the error variables for the given mass spectrum object.

def set_last_error(self, error, mass_spectrum_obj):
730    def set_last_error(self, error, mass_spectrum_obj):
731        """Set the last error.
732
733        Parameters
734        ----------
735        error : float
736            The error.
737        mass_spectrum_obj : MassSpectrum
738            The mass spectrum object.
739        """
740        # set the changes to the global variables, not internal ones
741        global last_error, last_dif, closest_error, error_average, nbValues
742
743        if mass_spectrum_obj.molecular_search_settings.error_method == "distance":
744            dif = error - last_error
745            if dif < last_dif:
746                last_dif = dif
747                closest_error = error
748                mass_spectrum_obj.molecular_search_settings.min_ppm_error = (
749                    closest_error
750                    - mass_spectrum_obj.molecular_search_settings.mz_error_range
751                )
752                mass_spectrum_obj.molecular_search_settings.max_ppm_error = (
753                    closest_error
754                    + mass_spectrum_obj.molecular_search_settings.mz_error_range
755                )
756
757        elif mass_spectrum_obj.molecular_search_settings.error_method == "lowest":
758            if error < last_error:
759                mass_spectrum_obj.molecular_search_settings.min_ppm_error = (
760                    error - mass_spectrum_obj.molecular_search_settings.mz_error_range
761                )
762                mass_spectrum_obj.molecular_search_settings.max_ppm_error = (
763                    error + mass_spectrum_obj.molecular_search_settings.mz_error_range
764                )
765                last_error = error
766
767        elif mass_spectrum_obj.molecular_search_settings.error_method == "symmetrical":
768            mass_spectrum_obj.molecular_search_settings.min_ppm_error = (
769                mass_spectrum_obj.molecular_search_settings.mz_error_average
770                - mass_spectrum_obj.molecular_search_settings.mz_error_range
771            )
772            mass_spectrum_obj.molecular_search_settings.max_ppm_error = (
773                mass_spectrum_obj.molecular_search_settings.mz_error_average
774                + mass_spectrum_obj.molecular_search_settings.mz_error_range
775            )
776
777        elif mass_spectrum_obj.molecular_search_settings.error_method == "average":
778            nbValues += 1
779            error_average = error_average + ((error - error_average) / nbValues)
780            mass_spectrum_obj.molecular_search_settings.min_ppm_error = (
781                error_average
782                - mass_spectrum_obj.molecular_search_settings.mz_error_range
783            )
784            mass_spectrum_obj.molecular_search_settings.max_ppm_error = (
785                error_average
786                + mass_spectrum_obj.molecular_search_settings.mz_error_range
787            )
788
789        else:
790            # using set mass_spectrum_obj.molecular_search_settings.min_ppm_error  and max_ppm_error range
791            pass
792
793        # returns the error based on the selected method at mass_spectrum_obj.molecular_search_settings.method

Set the last error.

Parameters
  • error (float): The error.
  • mass_spectrum_obj (MassSpectrum): The mass spectrum object.
@staticmethod
def calc_error(mz_exp, mz_calc, method='ppm'):
795    @staticmethod
796    def calc_error(mz_exp, mz_calc, method="ppm"):
797        """Calculate the error.
798
799        Parameters
800        ----------
801        mz_exp : float
802            The experimental m/z value.
803        mz_calc : float
804            The calculated m/z value.
805        method : str, optional
806            The method, by default 'ppm'.
807
808        Raises
809        -------
810        Exception
811            If the method is not ppm or ppb.
812
813        Returns
814        -------
815        float
816            The error.
817        """
818
819        if method == "ppm":
820            multi_factor = 1_000_000
821
822        elif method == "ppb":
823            multi_factor = 1_000_000_000
824
825        elif method == "perc":
826            multi_factor = 100
827
828        else:
829            raise Exception(
830                "method needs to be ppm or ppb, you have entered %s" % method
831            )
832
833        if mz_exp:
834            return ((mz_exp - mz_calc) / mz_calc) * multi_factor
835
836        else:
837            raise Exception("Please set mz_calc first")

Calculate the error.

Parameters
  • mz_exp (float): The experimental m/z value.
  • mz_calc (float): The calculated m/z value.
  • method (str, optional): The method, by default 'ppm'.
Raises
  • Exception: If the method is not ppm or ppb.
Returns
  • float: The error.
def find_formulas( self, formulas, min_abundance, mass_spectrum_obj, ms_peak, ion_type, ion_charge, adduct_atom=None):
 839    def find_formulas(
 840        self,
 841        formulas,
 842        min_abundance,
 843        mass_spectrum_obj,
 844        ms_peak,
 845        ion_type,
 846        ion_charge,
 847        adduct_atom=None,
 848    ):
 849        """Find the formulas.
 850
 851        Parameters
 852        ----------
 853        formulas : list of MolecularFormula
 854            The list of molecular formulas.
 855        min_abundance : float
 856            The minimum abundance threshold.
 857        mass_spectrum_obj : MassSpectrum
 858            The mass spectrum object.
 859        ms_peak : MSPeak
 860            The mass spectrum peak.
 861        ion_type : str
 862            The ion type.
 863        ion_charge : int
 864            The ion charge.
 865        adduct_atom : str, optional
 866            The adduct atom, by default None.
 867
 868        Returns
 869        -------
 870        list of MSPeak
 871            The list of mass spectrum peaks with assigned molecular formulas.
 872
 873        Notes
 874        -----
 875        Uses the closest error the next search (this is not ideal, it needs to use confidence
 876        metric to choose the right candidate then propagate the error using the error from the best candidate).
 877        It needs to add s/n to the equation.
 878        It need optimization to define the mz_error_range within a m/z unit since it is directly proportional
 879        with the mass, and inversely proportional to the rp. It's not linear, i.e., sigma mass.
 880        The idea it to correlate sigma to resolving power, signal to noise and sample complexity per mz unit.
 881        Method='distance'
 882        """
 883        mspeak_assigned_index = list()
 884
 885        min_ppm_error = mass_spectrum_obj.molecular_search_settings.min_ppm_error
 886        max_ppm_error = mass_spectrum_obj.molecular_search_settings.max_ppm_error
 887
 888        min_abun_error = mass_spectrum_obj.molecular_search_settings.min_abun_error
 889        max_abun_error = mass_spectrum_obj.molecular_search_settings.max_abun_error
 890
 891        # f = open("abundance_error.txt", "a+")
 892        ms_peak_mz_exp, ms_peak_abundance = ms_peak.mz_exp, ms_peak.abundance
 893        # min_error = min([pmf.mz_error for pmf in possible_formulas])
 894
 895        def mass_by_ion_type(possible_formula_obj):
 896            if ion_type == Labels.protonated_de_ion:
 897                return possible_formula_obj._protonated_mz(ion_charge)
 898
 899            elif ion_type == Labels.radical_ion:
 900                return possible_formula_obj._radical_mz(ion_charge)
 901
 902            elif ion_type == Labels.adduct_ion and adduct_atom:
 903                # Positional argument order differs between formula objects
 904                # and SQL links; keywords are unambiguous.
 905                return possible_formula_obj._adduct_mz(
 906                    ion_charge=ion_charge, adduct_atom=adduct_atom
 907                )
 908
 909            else:
 910                # will return externally calculated mz if is set, #use on Bruker Reference list import
 911                # if the ion type is known the ion mass based on molecular formula ion type
 912                # if ion type is unknow will return neutral mass
 913                return possible_formula_obj.mz_calc
 914
 915        for possible_formula in formulas:
 916            if possible_formula:
 917                error = self.calc_error(
 918                    ms_peak_mz_exp, mass_by_ion_type(possible_formula)
 919                )
 920
 921                # error = possible_formula.mz_error
 922
 923                if min_ppm_error <= error <= max_ppm_error:
 924                    # update the error
 925
 926                    self.set_last_error(error, mass_spectrum_obj)
 927
 928                    # add molecular formula match to ms_peak
 929
 930                    # get molecular formula dict from sql obj
 931                    # formula_dict = pickle.loads(possible_formula.mol_formula)
 932
 933                    formula_dict = possible_formula.to_dict()
 934                    # create the molecular formula obj to be stored
 935                    molecular_formula = MolecularFormula(
 936                        formula_dict,
 937                        ion_charge,
 938                        ion_type=ion_type,
 939                        adduct_atom=adduct_atom,
 940                    )
 941                    # add the molecular formula obj to the mspeak obj
 942                    # add the mspeak obj and it's index for tracking next assignment step
 943
 944                    if self.find_isotopologues:
 945                        # calculates isotopologues
 946                        isotopologues = molecular_formula.isotopologues(
 947                            min_abundance,
 948                            ms_peak_abundance,
 949                            mass_spectrum_obj.dynamic_range,
 950                        )
 951
 952                        # search for isotopologues
 953                        for isotopologue_formula in isotopologues:
 954                            molecular_formula.expected_isotopologues.append(
 955                                isotopologue_formula
 956                            )
 957                            # move this outside to improve preformace
 958                            # we need to increase the search space to -+1 m_z
 959                            first_index, last_index = (
 960                                mass_spectrum_obj.get_nominal_mz_first_last_indexes(
 961                                    isotopologue_formula.mz_nominal_calc
 962                                )
 963                            )
 964
 965                            for ms_peak_iso in mass_spectrum_obj[
 966                                first_index:last_index
 967                            ]:
 968                                error = self.calc_error(
 969                                    ms_peak_iso.mz_exp, isotopologue_formula.mz_calc
 970                                )
 971
 972                                if min_ppm_error <= error <= max_ppm_error:
 973                                    # need to define error distribution for abundance measurements
 974
 975                                    # if mass_spectrum_obj.is_centroid:
 976
 977                                    abundance_error = self.calc_error(
 978                                        isotopologue_formula.abundance_calc,
 979                                        ms_peak_iso.abundance,
 980                                        method="perc",
 981                                    )
 982
 983                                    # area_error = self.calc_error(ms_peak.area, ms_peak_iso.area, method='perc')
 984
 985                                    # margin of error was set empirically/ needs statistical calculation
 986                                    #  of margin of error for the measurement of the abundances
 987                                    if (
 988                                        min_abun_error
 989                                        <= abundance_error
 990                                        <= max_abun_error
 991                                    ):
 992                                        # update the error
 993
 994                                        self.set_last_error(error, mass_spectrum_obj)
 995
 996                                        # isotopologue_formula.mz_error = error
 997
 998                                        # isotopologue_formula.area_error = area_error
 999
1000                                        # isotopologue_formula.abundance_error = abundance_error
1001
1002                                        isotopologue_formula.mspeak_index_mono_isotopic = ms_peak.index
1003
1004                                        mono_isotopic_formula_index = len(ms_peak)
1005
1006                                        isotopologue_formula.mspeak_index_mono_isotopic = ms_peak.index
1007
1008                                        isotopologue_formula.mono_isotopic_formula_index = mono_isotopic_formula_index
1009
1010                                        # add mspeaks isotopologue index to the mono isotopic MolecularFormula obj and the respective formula position
1011
1012                                        # add molecular formula match to ms_peak
1013                                        x = ms_peak_iso.add_molecular_formula(
1014                                            isotopologue_formula
1015                                        )
1016
1017                                        molecular_formula.mspeak_mf_isotopologues_indexes.append(
1018                                            (ms_peak_iso.index, x)
1019                                        )
1020                                        # add mspeaks mono isotopic index to the isotopologue MolecularFormula obj
1021
1022                    y = ms_peak.add_molecular_formula(molecular_formula)
1023
1024                    mspeak_assigned_index.append((ms_peak.index, y))
1025
1026        return mspeak_assigned_index

Find the formulas.

Parameters
  • formulas (list of MolecularFormula): The list of molecular formulas.
  • min_abundance (float): The minimum abundance threshold.
  • mass_spectrum_obj (MassSpectrum): The mass spectrum object.
  • ms_peak (MSPeak): The mass spectrum peak.
  • ion_type (str): The ion type.
  • ion_charge (int): The ion charge.
  • adduct_atom (str, optional): The adduct atom, by default None.
Returns
  • list of MSPeak: The list of mass spectrum peaks with assigned molecular formulas.
Notes

Uses the closest error the next search (this is not ideal, it needs to use confidence metric to choose the right candidate then propagate the error using the error from the best candidate). It needs to add s/n to the equation. It need optimization to define the mz_error_range within a m/z unit since it is directly proportional with the mass, and inversely proportional to the rp. It's not linear, i.e., sigma mass. The idea it to correlate sigma to resolving power, signal to noise and sample complexity per mz unit. Method='distance'

class SearchMolecularFormulasLC:
1029class SearchMolecularFormulasLC:
1030    """Class for searching molecular formulas in a LC object.
1031
1032    Parameters
1033    ----------
1034    lcms_obj : LCMSBase
1035        The LCMSBase object.
1036    sql_db : MolForm_SQL, optional
1037        The SQL database object, by default None.
1038    first_hit : bool, optional
1039        Flag to indicate whether to skip peaks that already have a molecular formula assigned, by default False.
1040    find_isotopologues : bool, optional
1041        Flag to indicate whether to find isotopologues, by default True.
1042
1043    Methods
1044    -------
1045
1046    * search_spectra_against_candidates().
1047        Search a list of mass spectra against a list of candidate formulas with a given ion type, charge, and optional adduct_atom.
1048    * bulk_run_molecular_formula_search().
1049        Run the molecular formula search on the given list of mass spectra.
1050        Pulls the settings from the LCMSBase object to set ion type and charge to search for.
1051    * run_mass_feature_search().
1052        Run the molecular formula search on mass features.
1053        Calls bulk_run_molecular_formula_search() with specified mass spectra and mass peaks.
1054    * run_untargeted_worker_ms1().
1055        Run untargeted molecular formula search on the ms1 mass spectrum.
1056        DEPRECATED: use run_mass_feature_search() or bulk_run_molecular_formula_search() instead.
1057    * run_target_worker_ms1().
1058        Run targeted molecular formula search on the ms1 mass spectrum.
1059        DEPRECATED: use run_mass_feature_search() or bulk_run_molecular_formula_search() instead.
1060    """
1061
1062    def __init__(self, lcms_obj, sql_db=None, first_hit=False, find_isotopologues=True):
1063        self.first_hit = first_hit
1064
1065        self.find_isotopologues = find_isotopologues
1066
1067        self.lcms_obj = lcms_obj
1068
1069        # Catch in-place usedAtoms mutations after settings construction
1070        validate_used_atoms_keys(
1071            self.lcms_obj.parameters.mass_spectrum["ms1"].molecular_search.usedAtoms
1072        )
1073
1074        if not sql_db:
1075            self.sql_db = MolForm_SQL(
1076                url=self.lcms_obj.parameters.mass_spectrum[
1077                    "ms1"
1078                ].molecular_search.url_database
1079            )
1080
1081        else:
1082            self.sql_db = sql_db
1083
1084    def search_spectra_against_candidates(
1085        self,
1086        mass_spectrum_list,
1087        ms_peaks_list,
1088        candidate_formulas,
1089        ion_type,
1090        ion_charge,
1091        adduct_atom=None,
1092    ):
1093        """Search a list of mass spectra against a list of candidate formulas with a given ion type and charge.
1094
1095        Parameters
1096        ----------
1097        mass_spectrum_list : list of MassSpectrum
1098            The list of mass spectra to perform the search on.
1099        ms_peaks_list : list of lists of MSPeak objects
1100            The list of mass spectrum peaks to search within each mass spectrum.
1101        candidate_formulas : dict
1102            The candidate formulas.
1103        ion_type : str
1104            The ion type.
1105        ion_charge : int
1106            signed ion charge used for the candidate formulas.
1107        adduct_atom : str, optional
1108            The adduct atom, by default None.
1109
1110        Notes
1111        -----
1112        This function is designed to be used with the bulk_run_molecular_formula_search function.
1113        """
1114        for mass_spectrum, ms_peaks in zip(mass_spectrum_list, ms_peaks_list):
1115            single_ms_search = SearchMolecularFormulas(
1116                mass_spectrum,
1117                sql_db=self.sql_db,
1118                first_hit=self.first_hit,
1119                find_isotopologues=self.find_isotopologues,
1120            )
1121            single_ms_search.run_search(
1122                ms_peaks,
1123                candidate_formulas,
1124                mass_spectrum.min_abundance,
1125                ion_type,
1126                ion_charge,
1127                adduct_atom=adduct_atom,
1128            )
1129
1130    def bulk_run_molecular_formula_search(
1131        self, mass_spectrum_list, ms_peaks_list, mass_spectrum_setting_key="ms1"
1132    ):
1133        """Run the molecular formula search on the given list of mass spectra.
1134
1135        Uses ``min_ion_charge`` / ``max_ion_charge`` from molecular search settings
1136        to search each peak at every signed charge in that absolute range
1137        (defaults 1..1 = single-charge). Does not determine or rewrite peak
1138        ``polarity``; assignment charge is on each ``MolecularFormula``.
1139
1140        Parameters
1141        ----------
1142        mass_spectrum_list : list of MassSpectrum
1143            The list of mass spectra to search.
1144        ms_peaks_list : list of lists of MSPeak objects
1145            The mass peaks to perform molecular formula search within each mass spectrum
1146        mass_spectrum_setting_key : str, optional
1147            The mass spectrum setting key, by default 'ms1'.
1148            This is used to get the appropriate molecular search settings from the LCMSBase object
1149        """
1150        if self.lcms_obj.polarity not in ("positive", "negative"):
1151            raise ValueError("Polarity must be either 'positive' or 'negative'")
1152
1153        # Check that the length of the mass spectrum list and the ms_peaks list are the same
1154        if len(mass_spectrum_list) != len(ms_peaks_list):
1155            raise ValueError(
1156                "The length of the mass spectrum list and the ms_peaks list must be the same"
1157            )
1158
1159        mol_settings = self.lcms_obj.parameters.mass_spectrum[
1160            mass_spectrum_setting_key
1161        ].molecular_search
1162        # Re-check after possible post-construction mutation of settings fields
1163        mol_settings.validate_ion_charge_settings()
1164        search_charges = SearchMolecularFormulas.ion_charges_for_search(
1165            self.lcms_obj.polarity,
1166            min_ion_charge=mol_settings.min_ion_charge,
1167            max_ion_charge=mol_settings.max_ion_charge,
1168        )
1169
1170        nominal_mzs = [x.nominal_mz for x in mass_spectrum_list]
1171        nominal_mzs = list(set([item for sublist in nominal_mzs for item in sublist]))
1172
1173        # reset average error, only relevant if average mass error method is being used
1174        SearchMolecularFormulaWorker(
1175            find_isotopologues=self.find_isotopologues
1176        ).reset_error(mass_spectrum_list[0])
1177
1178        # check database for all possible molecular formula combinations based on the setting passed to self.mass_spectrum_obj.molecular_search_settings
1179        classes = MolecularCombinations(self.sql_db).runworker(
1180            mol_settings, print_time=mol_settings.verbose_processing
1181        )
1182
1183        try:
1184
1185            def apply_job(job):
1186                self.search_spectra_against_candidates(
1187                    mass_spectrum_list=mass_spectrum_list,
1188                    ms_peaks_list=ms_peaks_list,
1189                    candidate_formulas=job.candidate_formulas,
1190                    ion_type=job.ion_type,
1191                    ion_charge=job.ion_charge,
1192                    adduct_atom=job.adduct_atom,
1193                )
1194
1195            SearchMolecularFormulas._run_formula_search_jobs(
1196                classes,
1197                nominal_mzs,
1198                mol_settings,
1199                search_charges,
1200                self.sql_db,
1201                apply_job,
1202            )
1203        finally:
1204            self.sql_db.close()
1205
1206    def run_mass_feature_search(self):
1207        """Run the molecular formula search on the mass features.
1208
1209        Calls bulk_run_molecular_formula_search() with specified mass spectra and mass peaks.
1210        Multi-charge range comes from molecular search ``min_ion_charge`` /
1211        ``max_ion_charge``. Peak ``polarity`` is not rewritten.
1212        """
1213        mass_features_df = self.lcms_obj.mass_features_to_df()
1214
1215        # Get the list of mass spectrum (and peaks to search with each mass spectrum) for all mass features
1216        scan_list = mass_features_df.apex_scan.unique()
1217        mass_spectrum_list = [self.lcms_obj._ms[x] for x in scan_list]
1218        ms_peaks = []
1219        for scan in scan_list:
1220            mf_df_scan = mass_features_df[mass_features_df.apex_scan == scan]
1221            peaks_to_search = [
1222                self.lcms_obj.mass_features[x].ms1_peak
1223                for x in mf_df_scan.index.tolist()
1224            ]
1225            ms_peaks.append(peaks_to_search)
1226
1227        # Run the molecular formula search
1228        self.bulk_run_molecular_formula_search(mass_spectrum_list, ms_peaks)
1229
1230    def run_untargeted_worker_ms1(self):
1231        """Run untargeted molecular formula search on the ms1 mass spectrum."""
1232        raise NotImplementedError(
1233            "run_untargeted_worker_ms1 search is not implemented in CoreMS 3.0 and greater"
1234        )
1235
1236    def run_target_worker_ms1(self):
1237        """Run targeted molecular formula search on the ms1 mass spectrum."""
1238        raise NotImplementedError(
1239            "run_target_worker_ms1 formula search is not yet implemented in CoreMS 3.0 and greater"
1240        )

Class for searching molecular formulas in a LC object.

Parameters
  • lcms_obj (LCMSBase): The LCMSBase object.
  • sql_db (MolForm_SQL, optional): The SQL database object, by default None.
  • first_hit (bool, optional): Flag to indicate whether to skip peaks that already have a molecular formula assigned, by default False.
  • find_isotopologues (bool, optional): Flag to indicate whether to find isotopologues, by default True.
Methods
  • search_spectra_against_candidates(). Search a list of mass spectra against a list of candidate formulas with a given ion type, charge, and optional adduct_atom.
  • bulk_run_molecular_formula_search(). Run the molecular formula search on the given list of mass spectra. Pulls the settings from the LCMSBase object to set ion type and charge to search for.
  • run_mass_feature_search(). Run the molecular formula search on mass features. Calls bulk_run_molecular_formula_search() with specified mass spectra and mass peaks.
  • run_untargeted_worker_ms1(). Run untargeted molecular formula search on the ms1 mass spectrum. DEPRECATED: use run_mass_feature_search() or bulk_run_molecular_formula_search() instead.
  • run_target_worker_ms1(). Run targeted molecular formula search on the ms1 mass spectrum. DEPRECATED: use run_mass_feature_search() or bulk_run_molecular_formula_search() instead.
SearchMolecularFormulasLC(lcms_obj, sql_db=None, first_hit=False, find_isotopologues=True)
1062    def __init__(self, lcms_obj, sql_db=None, first_hit=False, find_isotopologues=True):
1063        self.first_hit = first_hit
1064
1065        self.find_isotopologues = find_isotopologues
1066
1067        self.lcms_obj = lcms_obj
1068
1069        # Catch in-place usedAtoms mutations after settings construction
1070        validate_used_atoms_keys(
1071            self.lcms_obj.parameters.mass_spectrum["ms1"].molecular_search.usedAtoms
1072        )
1073
1074        if not sql_db:
1075            self.sql_db = MolForm_SQL(
1076                url=self.lcms_obj.parameters.mass_spectrum[
1077                    "ms1"
1078                ].molecular_search.url_database
1079            )
1080
1081        else:
1082            self.sql_db = sql_db
first_hit
find_isotopologues
lcms_obj
def search_spectra_against_candidates( self, mass_spectrum_list, ms_peaks_list, candidate_formulas, ion_type, ion_charge, adduct_atom=None):
1084    def search_spectra_against_candidates(
1085        self,
1086        mass_spectrum_list,
1087        ms_peaks_list,
1088        candidate_formulas,
1089        ion_type,
1090        ion_charge,
1091        adduct_atom=None,
1092    ):
1093        """Search a list of mass spectra against a list of candidate formulas with a given ion type and charge.
1094
1095        Parameters
1096        ----------
1097        mass_spectrum_list : list of MassSpectrum
1098            The list of mass spectra to perform the search on.
1099        ms_peaks_list : list of lists of MSPeak objects
1100            The list of mass spectrum peaks to search within each mass spectrum.
1101        candidate_formulas : dict
1102            The candidate formulas.
1103        ion_type : str
1104            The ion type.
1105        ion_charge : int
1106            signed ion charge used for the candidate formulas.
1107        adduct_atom : str, optional
1108            The adduct atom, by default None.
1109
1110        Notes
1111        -----
1112        This function is designed to be used with the bulk_run_molecular_formula_search function.
1113        """
1114        for mass_spectrum, ms_peaks in zip(mass_spectrum_list, ms_peaks_list):
1115            single_ms_search = SearchMolecularFormulas(
1116                mass_spectrum,
1117                sql_db=self.sql_db,
1118                first_hit=self.first_hit,
1119                find_isotopologues=self.find_isotopologues,
1120            )
1121            single_ms_search.run_search(
1122                ms_peaks,
1123                candidate_formulas,
1124                mass_spectrum.min_abundance,
1125                ion_type,
1126                ion_charge,
1127                adduct_atom=adduct_atom,
1128            )

Search a list of mass spectra against a list of candidate formulas with a given ion type and charge.

Parameters
  • mass_spectrum_list (list of MassSpectrum): The list of mass spectra to perform the search on.
  • ms_peaks_list (list of lists of MSPeak objects): The list of mass spectrum peaks to search within each mass spectrum.
  • candidate_formulas (dict): The candidate formulas.
  • ion_type (str): The ion type.
  • ion_charge (int): signed ion charge used for the candidate formulas.
  • adduct_atom (str, optional): The adduct atom, by default None.
Notes

This function is designed to be used with the bulk_run_molecular_formula_search function.

def run_untargeted_worker_ms1(self):
1230    def run_untargeted_worker_ms1(self):
1231        """Run untargeted molecular formula search on the ms1 mass spectrum."""
1232        raise NotImplementedError(
1233            "run_untargeted_worker_ms1 search is not implemented in CoreMS 3.0 and greater"
1234        )

Run untargeted molecular formula search on the ms1 mass spectrum.

def run_target_worker_ms1(self):
1236    def run_target_worker_ms1(self):
1237        """Run targeted molecular formula search on the ms1 mass spectrum."""
1238        raise NotImplementedError(
1239            "run_target_worker_ms1 formula search is not yet implemented in CoreMS 3.0 and greater"
1240        )

Run targeted molecular formula search on the ms1 mass spectrum.