corems.molecular_id.search.lcms_spectral_search

  1import re
  2
  3import numpy as np
  4
  5from corems.molecular_id.factory.spectrum_search_results import SpectrumSearchResults
  6
  7
  8class LCMSSpectralSearch:
  9    """
 10    Methods for searching LCMS spectra.
 11
 12    This class is designed to be a mixin class for the :obj:`~corems.mass_spectra.factory.lc_class.LCMSBase` class.
 13
 14    """
 15
 16    @staticmethod
 17    def get_more_match_quals(
 18        query_mz_arr, lib_entry, mz_tol_da=0.1, include_fragment_types=False
 19    ):
 20        """
 21        Return additional match qualities between query and library entry.
 22
 23        Parameters
 24        ----------
 25        query_mz_arr : np.array
 26            Array of query spectrum. Shape (N, 2), with m/z in the first column
 27            and abundance in the second.
 28        lib_entry : dict
 29            Library spectrum entry, with 'mz' key containing the spectrum in
 30            the format (mz, abundance),(mz, abundance), i.e. from MetabRef.
 31        mz_tol_da : float, optional
 32            Tolerance in Da for matching peaks (in MS2). Default is 0.1.
 33        include_fragment_types : bool, optional
 34            If True, include fragment type comparisons in output.
 35            Defaults to False.
 36
 37        Returns
 38        -------
 39        tuple
 40            Tuple of (query_in_lib, query_in_lib_fract, lib_in_query, lib_in_query_fract, query_frags, lib_frags, lib_precursor_mz).
 41
 42        Notes
 43        -----
 44        query_in_lib : int
 45            Number of peaks in query that are present in the library entry (within mz_tol_da).
 46        query_in_lib_fract : float
 47            Fraction of peaks in query that are present in the library entry (within mz_tol_da).
 48        lib_in_query : int
 49            Number of peaks in the library entry that are present in the query (within mz_tol_da).
 50        lib_in_query_fract : float
 51            Fraction of peaks in the library entry that are present in the query (within mz_tol_da).
 52        query_frags : list
 53            List of unique fragment types present in the query, generally 'MLF' or 'LSF' or both.
 54        lib_frags : list
 55            List of unique fragment types present in the library entry, generally 'MLF' or 'LSF' or both.
 56
 57        Raises
 58        ------
 59        ValueError
 60            If library entry does not have 'fragment_types' key and include_fragment_types is True.
 61
 62        """
 63
 64        if "mz" in lib_entry.keys():
 65            # Get the original mz values from the library entry
 66            lib_mzs = np.array(
 67                re.findall(r"\(([^,]+),([^)]+)\)", lib_entry["mz"]), dtype=float
 68            ).reshape(-1, 2)[:, 0]
 69        elif "peaks" in lib_entry.keys() and lib_entry["peaks"] is not None:
 70            lib_mzs = lib_entry["peaks"][:, 0]
 71
 72        # Get count and fraction of peaks in query that are in lib entry
 73        query_in_lib = 0
 74        for peak in query_mz_arr:
 75            if np.any(np.isclose(lib_mzs, peak, atol=mz_tol_da)):
 76                query_in_lib += 1
 77        query_in_lib_fract = query_in_lib / len(query_mz_arr)
 78
 79        # Get count and fraction of peaks in lib that are in query
 80        lib_in_query = 0
 81        for peak in lib_mzs:
 82            if np.any(np.isclose(query_mz_arr, peak, atol=mz_tol_da)):
 83                lib_in_query += 1
 84        lib_in_query_fract = lib_in_query / len(lib_mzs)
 85
 86        if include_fragment_types:
 87            # Check that fragment types are present in the library entry
 88            if "fragment_types" not in lib_entry.keys():
 89                raise ValueError(
 90                    "Flash entropy library entry must have 'fragment_types' key to include fragment types in output."
 91                )
 92
 93            # Get types of fragments in the lib entry and convert it to a list on ", "
 94            lib_frags = [x.strip() for x in lib_entry["fragment_types"].split(",")]
 95            # make list of the fragment types that are present in the query spectrum
 96            lib_in_query_ids = list(
 97                set(
 98                    [
 99                        ind
100                        for ind, x in enumerate(lib_mzs)
101                        if len(np.where(np.isclose(query_mz_arr, x, atol=mz_tol_da))[0])
102                        > 0
103                    ]
104                )
105            )
106            query_frags = list(set([lib_frags[x] for x in lib_in_query_ids]))
107            lib_frags = list(set(lib_frags))
108
109        else:
110            query_frags = None
111            lib_frags = None
112
113        return (
114            query_in_lib,
115            query_in_lib_fract,
116            lib_in_query,
117            lib_in_query_fract,
118            query_frags,
119            lib_frags,
120        )
121
122    def fe_search(
123        self,
124        scan_list,
125        fe_lib,
126        precursor_mz_list=[],
127        use_mass_features=True,
128        peak_sep_da=0.01,
129        get_additional_metrics=True,
130        accumulate_results=False,
131    ):
132        """
133        Search LCMS spectra using a FlashEntropy approach.
134
135        Parameters
136        ----------
137        scan_list : list
138            List of scan numbers to search.
139        fe_lib : :obj:`~ms_entropy.FlashEntropySearch`
140            FlashEntropy Search instance.
141        precursor_mz_list : list, optional
142            List of precursor m/z values to search, by default [], which implies
143            matched with mass features; to enable this use_mass_features must be True.
144        use_mass_features : bool, optional
145            If True, use mass features to get precursor m/z values, by default True.
146            If True, will add search results to mass features' ms2_similarity_results attribute.
147        peak_sep_da : float, optional
148            Minimum separation between m/z peaks spectra in Da. This needs match the
149            approximate resolution of the search spectra and the FlashEntropySearch
150            instance, by default 0.01.
151        get_additional_metrics : bool, optional
152            If True, get additional metrics from FlashEntropy search, by default True.
153        accumulate_results : bool, optional
154            If True, accumulate results with existing spectral_search_results instead of
155            replacing them. This allows searching the same scans with multiple libraries
156            without overwriting previous results, by default False.
157
158        Returns
159        -------
160        None, but adds results to self.spectral_search_results and associates these
161        spectral_search_results with mass_features within the self.mass_features dictionary.
162
163        """
164        # Retrieve parameters from self
165        # include_fragment_types should used for lipids queries only, not general metabolomics
166        include_fragment_types = self.parameters.lc_ms.include_fragment_types
167        min_match_score = self.parameters.lc_ms.ms2_min_fe_score
168
169        # If precursor_mz_list is empty and use_mass_features is True, get precursor m/z values from mass features for each scan in scan_list
170        if use_mass_features and len(precursor_mz_list) == 0:
171            precursor_mz_list = []
172            for scan in scan_list:
173                mf_ids = [
174                    key
175                    for key, value in self.mass_features.items()
176                    if scan in value.ms2_mass_spectra
177                ]
178                precursor_mz = [
179                    value.mz
180                    for key, value in self.mass_features.items()
181                    if key in mf_ids
182                ]
183                precursor_mz_list.append(precursor_mz)
184
185        # Check that precursor_mz_list same length as scan_list, if not, raise error
186        if len(precursor_mz_list) != len(scan_list):
187            raise ValueError("Length of precursor_mz_list is not equal to scan_list.")
188
189        # Loop through each query spectrum / precursor match and save ids of db spectrum that are decent matches
190        overall_results_dict = {}
191        for i in np.arange(len(scan_list)):
192            scan_oi = scan_list[i]
193            if len(self._ms[scan_oi].mspeaks) > 0:
194                precursor_mzs = precursor_mz_list[i]
195                overall_results_dict[scan_oi] = {}
196                for precursor_mz in precursor_mzs:
197                    query_spectrum = fe_lib.clean_spectrum_for_search(
198                        precursor_mz=precursor_mz,
199                        peaks=np.vstack(
200                            (self._ms[scan_oi].mz_exp, self._ms[scan_oi].abundance)
201                        ).T,
202                        precursor_ions_removal_da=None,
203                        noise_threshold=self._ms[
204                            scan_oi
205                        ].parameters.mass_spectrum.noise_threshold_min_relative_abundance
206                        / 100,
207                        min_ms2_difference_in_da=peak_sep_da,
208                    )
209                    search_results = fe_lib.search(
210                        precursor_mz=precursor_mz,
211                        peaks=query_spectrum,
212                        ms1_tolerance_in_da=self.parameters.mass_spectrum[
213                            "ms1"
214                        ].molecular_search.max_ppm_error
215                        * 10**-6
216                        * precursor_mz,
217                        ms2_tolerance_in_da=peak_sep_da * 0.5,
218                        method={"identity"},
219                        precursor_ions_removal_da=None,
220                        noise_threshold=self._ms[
221                            scan_oi
222                        ].parameters.mass_spectrum.noise_threshold_min_relative_abundance
223                        / 100,
224                        target="cpu",
225                    )["identity_search"]
226                    match_inds = np.where(search_results > min_match_score)[0]
227
228                    # If any decent matches are found, add them to the results dictionary
229                    if len(match_inds) > 0:
230                        match_scores = search_results[match_inds]
231                        ref_ms_ids = [fe_lib[x]["id"] for x in match_inds]
232                        ref_mol_ids = [
233                            fe_lib[x]["molecular_data_id"] for x in match_inds
234                        ]
235                        ref_precursor_mzs = [
236                            fe_lib[x]["precursor_mz"] for x in match_inds
237                        ]
238                        ion_types = [fe_lib[x]["ion_type"] for x in match_inds]
239                        overall_results_dict[scan_oi][precursor_mz] = {
240                            "ref_mol_id": ref_mol_ids,
241                            "ref_ms_id": ref_ms_ids,
242                            "ref_precursor_mz": ref_precursor_mzs,
243                            "precursor_mz_error_ppm": [
244                                (precursor_mz - x) / precursor_mz * 10**6
245                                for x in ref_precursor_mzs
246                            ],
247                            "entropy_similarity": match_scores,
248                            "ref_ion_type": ion_types,
249                        }
250                        # Add database name, if present
251                        db_name = [
252                            fe_lib[x].get("database_name") for x in match_inds
253                        ]
254                        if db_name is not None:
255                            overall_results_dict[scan_oi][precursor_mz].update(
256                                {"database_name": db_name}
257                            )
258                        if get_additional_metrics:
259                            more_match_quals = [
260                                self.get_more_match_quals(
261                                    self._ms[scan_oi].mz_exp,
262                                    fe_lib[x],
263                                    mz_tol_da=peak_sep_da,
264                                    include_fragment_types=include_fragment_types,
265                                )
266                                for x in match_inds
267                            ]
268                            overall_results_dict[scan_oi][precursor_mz].update(
269                                {
270                                    "query_mz_in_ref_n": [
271                                        x[0] for x in more_match_quals
272                                    ],
273                                    "query_mz_in_ref_fract": [
274                                        x[1] for x in more_match_quals
275                                    ],
276                                    "ref_mz_in_query_n": [
277                                        x[2] for x in more_match_quals
278                                    ],
279                                    "ref_mz_in_query_fract": [
280                                        x[3] for x in more_match_quals
281                                    ],
282                                }
283                            )
284                            if include_fragment_types:
285                                overall_results_dict[scan_oi][precursor_mz].update(
286                                    {
287                                        "query_frag_types": [
288                                            x[4] for x in more_match_quals
289                                        ],
290                                        "ref_frag_types": [
291                                            x[5] for x in more_match_quals
292                                        ],
293                                    }
294                                )
295
296        # Drop scans with no results from dictionary
297        overall_results_dict = {k: v for k, v in overall_results_dict.items() if v}
298
299        # Cast each entry as a MS2SearchResults object
300        for scan_id in overall_results_dict.keys():
301            for precursor_mz in overall_results_dict[scan_id].keys():
302                ms2_spectrum = self._ms[scan_id]
303                ms2_search_results = overall_results_dict[scan_id][precursor_mz]
304                overall_results_dict[scan_id][precursor_mz] = SpectrumSearchResults(
305                    ms2_spectrum, precursor_mz, ms2_search_results
306                )
307
308        # Add MS2SearchResults to the existing spectral search results dictionary
309        if accumulate_results:
310            # Merge results with existing spectral_search_results
311            for scan_id, precursor_dict in overall_results_dict.items():
312                if scan_id in self.spectral_search_results:
313                    # Scan already has results, merge precursor_mz dictionaries
314                    self.spectral_search_results[scan_id].update(precursor_dict)
315                else:
316                    # New scan, add entire dictionary
317                    self.spectral_search_results[scan_id] = precursor_dict
318        else:
319            # Replace existing results (original behavior)
320            self.spectral_search_results.update(overall_results_dict)
321
322        # If there are mass features, associate the results with each mass feature
323        if len(self.mass_features) > 0:
324            # Determine which results to associate with mass features
325            if accumulate_results:
326                # When accumulating, only associate new results from this search
327                # to avoid duplicating previously associated results
328                results_to_associate = overall_results_dict
329            else:
330                # When not accumulating, clear existing associations and re-associate all results
331                for mass_feature_id in self.mass_features.keys():
332                    self.mass_features[mass_feature_id].ms2_similarity_results = []
333                results_to_associate = self.spectral_search_results
334            
335            for mass_feature_id, mass_feature in self.mass_features.items():
336                scan_ids = mass_feature.ms2_scan_numbers
337                for ms2_scan_id in scan_ids:
338                    precursor_mz = mass_feature.mz
339                    try:
340                        results_to_associate[ms2_scan_id][precursor_mz]
341                    except KeyError:
342                        pass
343                    else:
344                        self.mass_features[
345                            mass_feature_id
346                        ].ms2_similarity_results.append(
347                            results_to_associate[ms2_scan_id][precursor_mz]
348                        )
class LCMSSpectralSearch:
  9class LCMSSpectralSearch:
 10    """
 11    Methods for searching LCMS spectra.
 12
 13    This class is designed to be a mixin class for the :obj:`~corems.mass_spectra.factory.lc_class.LCMSBase` class.
 14
 15    """
 16
 17    @staticmethod
 18    def get_more_match_quals(
 19        query_mz_arr, lib_entry, mz_tol_da=0.1, include_fragment_types=False
 20    ):
 21        """
 22        Return additional match qualities between query and library entry.
 23
 24        Parameters
 25        ----------
 26        query_mz_arr : np.array
 27            Array of query spectrum. Shape (N, 2), with m/z in the first column
 28            and abundance in the second.
 29        lib_entry : dict
 30            Library spectrum entry, with 'mz' key containing the spectrum in
 31            the format (mz, abundance),(mz, abundance), i.e. from MetabRef.
 32        mz_tol_da : float, optional
 33            Tolerance in Da for matching peaks (in MS2). Default is 0.1.
 34        include_fragment_types : bool, optional
 35            If True, include fragment type comparisons in output.
 36            Defaults to False.
 37
 38        Returns
 39        -------
 40        tuple
 41            Tuple of (query_in_lib, query_in_lib_fract, lib_in_query, lib_in_query_fract, query_frags, lib_frags, lib_precursor_mz).
 42
 43        Notes
 44        -----
 45        query_in_lib : int
 46            Number of peaks in query that are present in the library entry (within mz_tol_da).
 47        query_in_lib_fract : float
 48            Fraction of peaks in query that are present in the library entry (within mz_tol_da).
 49        lib_in_query : int
 50            Number of peaks in the library entry that are present in the query (within mz_tol_da).
 51        lib_in_query_fract : float
 52            Fraction of peaks in the library entry that are present in the query (within mz_tol_da).
 53        query_frags : list
 54            List of unique fragment types present in the query, generally 'MLF' or 'LSF' or both.
 55        lib_frags : list
 56            List of unique fragment types present in the library entry, generally 'MLF' or 'LSF' or both.
 57
 58        Raises
 59        ------
 60        ValueError
 61            If library entry does not have 'fragment_types' key and include_fragment_types is True.
 62
 63        """
 64
 65        if "mz" in lib_entry.keys():
 66            # Get the original mz values from the library entry
 67            lib_mzs = np.array(
 68                re.findall(r"\(([^,]+),([^)]+)\)", lib_entry["mz"]), dtype=float
 69            ).reshape(-1, 2)[:, 0]
 70        elif "peaks" in lib_entry.keys() and lib_entry["peaks"] is not None:
 71            lib_mzs = lib_entry["peaks"][:, 0]
 72
 73        # Get count and fraction of peaks in query that are in lib entry
 74        query_in_lib = 0
 75        for peak in query_mz_arr:
 76            if np.any(np.isclose(lib_mzs, peak, atol=mz_tol_da)):
 77                query_in_lib += 1
 78        query_in_lib_fract = query_in_lib / len(query_mz_arr)
 79
 80        # Get count and fraction of peaks in lib that are in query
 81        lib_in_query = 0
 82        for peak in lib_mzs:
 83            if np.any(np.isclose(query_mz_arr, peak, atol=mz_tol_da)):
 84                lib_in_query += 1
 85        lib_in_query_fract = lib_in_query / len(lib_mzs)
 86
 87        if include_fragment_types:
 88            # Check that fragment types are present in the library entry
 89            if "fragment_types" not in lib_entry.keys():
 90                raise ValueError(
 91                    "Flash entropy library entry must have 'fragment_types' key to include fragment types in output."
 92                )
 93
 94            # Get types of fragments in the lib entry and convert it to a list on ", "
 95            lib_frags = [x.strip() for x in lib_entry["fragment_types"].split(",")]
 96            # make list of the fragment types that are present in the query spectrum
 97            lib_in_query_ids = list(
 98                set(
 99                    [
100                        ind
101                        for ind, x in enumerate(lib_mzs)
102                        if len(np.where(np.isclose(query_mz_arr, x, atol=mz_tol_da))[0])
103                        > 0
104                    ]
105                )
106            )
107            query_frags = list(set([lib_frags[x] for x in lib_in_query_ids]))
108            lib_frags = list(set(lib_frags))
109
110        else:
111            query_frags = None
112            lib_frags = None
113
114        return (
115            query_in_lib,
116            query_in_lib_fract,
117            lib_in_query,
118            lib_in_query_fract,
119            query_frags,
120            lib_frags,
121        )
122
123    def fe_search(
124        self,
125        scan_list,
126        fe_lib,
127        precursor_mz_list=[],
128        use_mass_features=True,
129        peak_sep_da=0.01,
130        get_additional_metrics=True,
131        accumulate_results=False,
132    ):
133        """
134        Search LCMS spectra using a FlashEntropy approach.
135
136        Parameters
137        ----------
138        scan_list : list
139            List of scan numbers to search.
140        fe_lib : :obj:`~ms_entropy.FlashEntropySearch`
141            FlashEntropy Search instance.
142        precursor_mz_list : list, optional
143            List of precursor m/z values to search, by default [], which implies
144            matched with mass features; to enable this use_mass_features must be True.
145        use_mass_features : bool, optional
146            If True, use mass features to get precursor m/z values, by default True.
147            If True, will add search results to mass features' ms2_similarity_results attribute.
148        peak_sep_da : float, optional
149            Minimum separation between m/z peaks spectra in Da. This needs match the
150            approximate resolution of the search spectra and the FlashEntropySearch
151            instance, by default 0.01.
152        get_additional_metrics : bool, optional
153            If True, get additional metrics from FlashEntropy search, by default True.
154        accumulate_results : bool, optional
155            If True, accumulate results with existing spectral_search_results instead of
156            replacing them. This allows searching the same scans with multiple libraries
157            without overwriting previous results, by default False.
158
159        Returns
160        -------
161        None, but adds results to self.spectral_search_results and associates these
162        spectral_search_results with mass_features within the self.mass_features dictionary.
163
164        """
165        # Retrieve parameters from self
166        # include_fragment_types should used for lipids queries only, not general metabolomics
167        include_fragment_types = self.parameters.lc_ms.include_fragment_types
168        min_match_score = self.parameters.lc_ms.ms2_min_fe_score
169
170        # If precursor_mz_list is empty and use_mass_features is True, get precursor m/z values from mass features for each scan in scan_list
171        if use_mass_features and len(precursor_mz_list) == 0:
172            precursor_mz_list = []
173            for scan in scan_list:
174                mf_ids = [
175                    key
176                    for key, value in self.mass_features.items()
177                    if scan in value.ms2_mass_spectra
178                ]
179                precursor_mz = [
180                    value.mz
181                    for key, value in self.mass_features.items()
182                    if key in mf_ids
183                ]
184                precursor_mz_list.append(precursor_mz)
185
186        # Check that precursor_mz_list same length as scan_list, if not, raise error
187        if len(precursor_mz_list) != len(scan_list):
188            raise ValueError("Length of precursor_mz_list is not equal to scan_list.")
189
190        # Loop through each query spectrum / precursor match and save ids of db spectrum that are decent matches
191        overall_results_dict = {}
192        for i in np.arange(len(scan_list)):
193            scan_oi = scan_list[i]
194            if len(self._ms[scan_oi].mspeaks) > 0:
195                precursor_mzs = precursor_mz_list[i]
196                overall_results_dict[scan_oi] = {}
197                for precursor_mz in precursor_mzs:
198                    query_spectrum = fe_lib.clean_spectrum_for_search(
199                        precursor_mz=precursor_mz,
200                        peaks=np.vstack(
201                            (self._ms[scan_oi].mz_exp, self._ms[scan_oi].abundance)
202                        ).T,
203                        precursor_ions_removal_da=None,
204                        noise_threshold=self._ms[
205                            scan_oi
206                        ].parameters.mass_spectrum.noise_threshold_min_relative_abundance
207                        / 100,
208                        min_ms2_difference_in_da=peak_sep_da,
209                    )
210                    search_results = fe_lib.search(
211                        precursor_mz=precursor_mz,
212                        peaks=query_spectrum,
213                        ms1_tolerance_in_da=self.parameters.mass_spectrum[
214                            "ms1"
215                        ].molecular_search.max_ppm_error
216                        * 10**-6
217                        * precursor_mz,
218                        ms2_tolerance_in_da=peak_sep_da * 0.5,
219                        method={"identity"},
220                        precursor_ions_removal_da=None,
221                        noise_threshold=self._ms[
222                            scan_oi
223                        ].parameters.mass_spectrum.noise_threshold_min_relative_abundance
224                        / 100,
225                        target="cpu",
226                    )["identity_search"]
227                    match_inds = np.where(search_results > min_match_score)[0]
228
229                    # If any decent matches are found, add them to the results dictionary
230                    if len(match_inds) > 0:
231                        match_scores = search_results[match_inds]
232                        ref_ms_ids = [fe_lib[x]["id"] for x in match_inds]
233                        ref_mol_ids = [
234                            fe_lib[x]["molecular_data_id"] for x in match_inds
235                        ]
236                        ref_precursor_mzs = [
237                            fe_lib[x]["precursor_mz"] for x in match_inds
238                        ]
239                        ion_types = [fe_lib[x]["ion_type"] for x in match_inds]
240                        overall_results_dict[scan_oi][precursor_mz] = {
241                            "ref_mol_id": ref_mol_ids,
242                            "ref_ms_id": ref_ms_ids,
243                            "ref_precursor_mz": ref_precursor_mzs,
244                            "precursor_mz_error_ppm": [
245                                (precursor_mz - x) / precursor_mz * 10**6
246                                for x in ref_precursor_mzs
247                            ],
248                            "entropy_similarity": match_scores,
249                            "ref_ion_type": ion_types,
250                        }
251                        # Add database name, if present
252                        db_name = [
253                            fe_lib[x].get("database_name") for x in match_inds
254                        ]
255                        if db_name is not None:
256                            overall_results_dict[scan_oi][precursor_mz].update(
257                                {"database_name": db_name}
258                            )
259                        if get_additional_metrics:
260                            more_match_quals = [
261                                self.get_more_match_quals(
262                                    self._ms[scan_oi].mz_exp,
263                                    fe_lib[x],
264                                    mz_tol_da=peak_sep_da,
265                                    include_fragment_types=include_fragment_types,
266                                )
267                                for x in match_inds
268                            ]
269                            overall_results_dict[scan_oi][precursor_mz].update(
270                                {
271                                    "query_mz_in_ref_n": [
272                                        x[0] for x in more_match_quals
273                                    ],
274                                    "query_mz_in_ref_fract": [
275                                        x[1] for x in more_match_quals
276                                    ],
277                                    "ref_mz_in_query_n": [
278                                        x[2] for x in more_match_quals
279                                    ],
280                                    "ref_mz_in_query_fract": [
281                                        x[3] for x in more_match_quals
282                                    ],
283                                }
284                            )
285                            if include_fragment_types:
286                                overall_results_dict[scan_oi][precursor_mz].update(
287                                    {
288                                        "query_frag_types": [
289                                            x[4] for x in more_match_quals
290                                        ],
291                                        "ref_frag_types": [
292                                            x[5] for x in more_match_quals
293                                        ],
294                                    }
295                                )
296
297        # Drop scans with no results from dictionary
298        overall_results_dict = {k: v for k, v in overall_results_dict.items() if v}
299
300        # Cast each entry as a MS2SearchResults object
301        for scan_id in overall_results_dict.keys():
302            for precursor_mz in overall_results_dict[scan_id].keys():
303                ms2_spectrum = self._ms[scan_id]
304                ms2_search_results = overall_results_dict[scan_id][precursor_mz]
305                overall_results_dict[scan_id][precursor_mz] = SpectrumSearchResults(
306                    ms2_spectrum, precursor_mz, ms2_search_results
307                )
308
309        # Add MS2SearchResults to the existing spectral search results dictionary
310        if accumulate_results:
311            # Merge results with existing spectral_search_results
312            for scan_id, precursor_dict in overall_results_dict.items():
313                if scan_id in self.spectral_search_results:
314                    # Scan already has results, merge precursor_mz dictionaries
315                    self.spectral_search_results[scan_id].update(precursor_dict)
316                else:
317                    # New scan, add entire dictionary
318                    self.spectral_search_results[scan_id] = precursor_dict
319        else:
320            # Replace existing results (original behavior)
321            self.spectral_search_results.update(overall_results_dict)
322
323        # If there are mass features, associate the results with each mass feature
324        if len(self.mass_features) > 0:
325            # Determine which results to associate with mass features
326            if accumulate_results:
327                # When accumulating, only associate new results from this search
328                # to avoid duplicating previously associated results
329                results_to_associate = overall_results_dict
330            else:
331                # When not accumulating, clear existing associations and re-associate all results
332                for mass_feature_id in self.mass_features.keys():
333                    self.mass_features[mass_feature_id].ms2_similarity_results = []
334                results_to_associate = self.spectral_search_results
335            
336            for mass_feature_id, mass_feature in self.mass_features.items():
337                scan_ids = mass_feature.ms2_scan_numbers
338                for ms2_scan_id in scan_ids:
339                    precursor_mz = mass_feature.mz
340                    try:
341                        results_to_associate[ms2_scan_id][precursor_mz]
342                    except KeyError:
343                        pass
344                    else:
345                        self.mass_features[
346                            mass_feature_id
347                        ].ms2_similarity_results.append(
348                            results_to_associate[ms2_scan_id][precursor_mz]
349                        )

Methods for searching LCMS spectra.

This class is designed to be a mixin class for the ~corems.mass_spectra.factory.lc_class.LCMSBase class.

@staticmethod
def get_more_match_quals(query_mz_arr, lib_entry, mz_tol_da=0.1, include_fragment_types=False):
 17    @staticmethod
 18    def get_more_match_quals(
 19        query_mz_arr, lib_entry, mz_tol_da=0.1, include_fragment_types=False
 20    ):
 21        """
 22        Return additional match qualities between query and library entry.
 23
 24        Parameters
 25        ----------
 26        query_mz_arr : np.array
 27            Array of query spectrum. Shape (N, 2), with m/z in the first column
 28            and abundance in the second.
 29        lib_entry : dict
 30            Library spectrum entry, with 'mz' key containing the spectrum in
 31            the format (mz, abundance),(mz, abundance), i.e. from MetabRef.
 32        mz_tol_da : float, optional
 33            Tolerance in Da for matching peaks (in MS2). Default is 0.1.
 34        include_fragment_types : bool, optional
 35            If True, include fragment type comparisons in output.
 36            Defaults to False.
 37
 38        Returns
 39        -------
 40        tuple
 41            Tuple of (query_in_lib, query_in_lib_fract, lib_in_query, lib_in_query_fract, query_frags, lib_frags, lib_precursor_mz).
 42
 43        Notes
 44        -----
 45        query_in_lib : int
 46            Number of peaks in query that are present in the library entry (within mz_tol_da).
 47        query_in_lib_fract : float
 48            Fraction of peaks in query that are present in the library entry (within mz_tol_da).
 49        lib_in_query : int
 50            Number of peaks in the library entry that are present in the query (within mz_tol_da).
 51        lib_in_query_fract : float
 52            Fraction of peaks in the library entry that are present in the query (within mz_tol_da).
 53        query_frags : list
 54            List of unique fragment types present in the query, generally 'MLF' or 'LSF' or both.
 55        lib_frags : list
 56            List of unique fragment types present in the library entry, generally 'MLF' or 'LSF' or both.
 57
 58        Raises
 59        ------
 60        ValueError
 61            If library entry does not have 'fragment_types' key and include_fragment_types is True.
 62
 63        """
 64
 65        if "mz" in lib_entry.keys():
 66            # Get the original mz values from the library entry
 67            lib_mzs = np.array(
 68                re.findall(r"\(([^,]+),([^)]+)\)", lib_entry["mz"]), dtype=float
 69            ).reshape(-1, 2)[:, 0]
 70        elif "peaks" in lib_entry.keys() and lib_entry["peaks"] is not None:
 71            lib_mzs = lib_entry["peaks"][:, 0]
 72
 73        # Get count and fraction of peaks in query that are in lib entry
 74        query_in_lib = 0
 75        for peak in query_mz_arr:
 76            if np.any(np.isclose(lib_mzs, peak, atol=mz_tol_da)):
 77                query_in_lib += 1
 78        query_in_lib_fract = query_in_lib / len(query_mz_arr)
 79
 80        # Get count and fraction of peaks in lib that are in query
 81        lib_in_query = 0
 82        for peak in lib_mzs:
 83            if np.any(np.isclose(query_mz_arr, peak, atol=mz_tol_da)):
 84                lib_in_query += 1
 85        lib_in_query_fract = lib_in_query / len(lib_mzs)
 86
 87        if include_fragment_types:
 88            # Check that fragment types are present in the library entry
 89            if "fragment_types" not in lib_entry.keys():
 90                raise ValueError(
 91                    "Flash entropy library entry must have 'fragment_types' key to include fragment types in output."
 92                )
 93
 94            # Get types of fragments in the lib entry and convert it to a list on ", "
 95            lib_frags = [x.strip() for x in lib_entry["fragment_types"].split(",")]
 96            # make list of the fragment types that are present in the query spectrum
 97            lib_in_query_ids = list(
 98                set(
 99                    [
100                        ind
101                        for ind, x in enumerate(lib_mzs)
102                        if len(np.where(np.isclose(query_mz_arr, x, atol=mz_tol_da))[0])
103                        > 0
104                    ]
105                )
106            )
107            query_frags = list(set([lib_frags[x] for x in lib_in_query_ids]))
108            lib_frags = list(set(lib_frags))
109
110        else:
111            query_frags = None
112            lib_frags = None
113
114        return (
115            query_in_lib,
116            query_in_lib_fract,
117            lib_in_query,
118            lib_in_query_fract,
119            query_frags,
120            lib_frags,
121        )

Return additional match qualities between query and library entry.

Parameters
  • query_mz_arr (np.array): Array of query spectrum. Shape (N, 2), with m/z in the first column and abundance in the second.
  • lib_entry (dict): Library spectrum entry, with 'mz' key containing the spectrum in the format (mz, abundance),(mz, abundance), i.e. from MetabRef.
  • mz_tol_da (float, optional): Tolerance in Da for matching peaks (in MS2). Default is 0.1.
  • include_fragment_types (bool, optional): If True, include fragment type comparisons in output. Defaults to False.
Returns
  • tuple: Tuple of (query_in_lib, query_in_lib_fract, lib_in_query, lib_in_query_fract, query_frags, lib_frags, lib_precursor_mz).
Notes

query_in_lib : int Number of peaks in query that are present in the library entry (within mz_tol_da). query_in_lib_fract : float Fraction of peaks in query that are present in the library entry (within mz_tol_da). lib_in_query : int Number of peaks in the library entry that are present in the query (within mz_tol_da). lib_in_query_fract : float Fraction of peaks in the library entry that are present in the query (within mz_tol_da). query_frags : list List of unique fragment types present in the query, generally 'MLF' or 'LSF' or both. lib_frags : list List of unique fragment types present in the library entry, generally 'MLF' or 'LSF' or both.

Raises
  • ValueError: If library entry does not have 'fragment_types' key and include_fragment_types is True.