CoreMS 4.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. corems/__init__.py +63 -0
  2. corems/chroma_peak/__init__.py +0 -0
  3. corems/chroma_peak/calc/ChromaPeakCalc.py +480 -0
  4. corems/chroma_peak/calc/__init__.py +0 -0
  5. corems/chroma_peak/calc/subset.py +196 -0
  6. corems/chroma_peak/factory/__init__.py +0 -0
  7. corems/chroma_peak/factory/chroma_peak_classes.py +1178 -0
  8. corems/encapsulation/__init__.py +0 -0
  9. corems/encapsulation/constant.py +1283 -0
  10. corems/encapsulation/factory/__init__.py +0 -0
  11. corems/encapsulation/factory/parameters.py +392 -0
  12. corems/encapsulation/factory/processingSetting.py +1244 -0
  13. corems/encapsulation/input/__init__.py +0 -0
  14. corems/encapsulation/input/parameter_from_json.py +579 -0
  15. corems/encapsulation/output/__init__.py +0 -0
  16. corems/encapsulation/output/parameter_to_dict.py +142 -0
  17. corems/encapsulation/output/parameter_to_json.py +332 -0
  18. corems/mass_spectra/__init__.py +0 -0
  19. corems/mass_spectra/calc/GC_Calc.py +84 -0
  20. corems/mass_spectra/calc/GC_Deconvolution.py +558 -0
  21. corems/mass_spectra/calc/GC_RI_Calibration.py +44 -0
  22. corems/mass_spectra/calc/MZSearch.py +249 -0
  23. corems/mass_spectra/calc/SignalProcessing.py +656 -0
  24. corems/mass_spectra/calc/__init__.py +0 -0
  25. corems/mass_spectra/calc/lc_calc.py +5790 -0
  26. corems/mass_spectra/calc/lc_calc_operations.py +1127 -0
  27. corems/mass_spectra/factory/GC_Class.py +791 -0
  28. corems/mass_spectra/factory/__init__.py +0 -0
  29. corems/mass_spectra/factory/chromat_data.py +51 -0
  30. corems/mass_spectra/factory/lc_class.py +2721 -0
  31. corems/mass_spectra/input/__init__.py +0 -0
  32. corems/mass_spectra/input/andiNetCDF.py +200 -0
  33. corems/mass_spectra/input/boosterHDF5.py +216 -0
  34. corems/mass_spectra/input/brukerSolarix.py +153 -0
  35. corems/mass_spectra/input/brukerSolarix_utils.py +73 -0
  36. corems/mass_spectra/input/corems_hdf5.py +1710 -0
  37. corems/mass_spectra/input/massList.py +133 -0
  38. corems/mass_spectra/input/mzml.py +668 -0
  39. corems/mass_spectra/input/parserbase.py +239 -0
  40. corems/mass_spectra/input/rawFileReader.py +1839 -0
  41. corems/mass_spectra/output/__init__.py +0 -0
  42. corems/mass_spectra/output/export.py +2800 -0
  43. corems/mass_spectrum/__init__.py +0 -0
  44. corems/mass_spectrum/calc/AutoRecalibration.py +237 -0
  45. corems/mass_spectrum/calc/Calibration.py +602 -0
  46. corems/mass_spectrum/calc/CalibrationCalc.py +253 -0
  47. corems/mass_spectrum/calc/KendrickGroup.py +239 -0
  48. corems/mass_spectrum/calc/MassErrorPrediction.py +436 -0
  49. corems/mass_spectrum/calc/MassSpectrumCalc.py +303 -0
  50. corems/mass_spectrum/calc/MeanResolvingPowerFilter.py +212 -0
  51. corems/mass_spectrum/calc/NoiseCalc.py +371 -0
  52. corems/mass_spectrum/calc/NoiseCalc_Bayes.py +93 -0
  53. corems/mass_spectrum/calc/PeakPicking.py +994 -0
  54. corems/mass_spectrum/calc/__init__.py +0 -0
  55. corems/mass_spectrum/factory/MassSpectrumClasses.py +1753 -0
  56. corems/mass_spectrum/factory/__init__.py +0 -0
  57. corems/mass_spectrum/input/__init__.py +0 -0
  58. corems/mass_spectrum/input/baseClass.py +531 -0
  59. corems/mass_spectrum/input/boosterHDF5.py +161 -0
  60. corems/mass_spectrum/input/coremsHDF5.py +475 -0
  61. corems/mass_spectrum/input/massList.py +402 -0
  62. corems/mass_spectrum/input/numpyArray.py +133 -0
  63. corems/mass_spectrum/output/__init__.py +0 -0
  64. corems/mass_spectrum/output/export.py +841 -0
  65. corems/molecular_formula/__init__.py +0 -0
  66. corems/molecular_formula/calc/MolecularFormulaCalc.py +885 -0
  67. corems/molecular_formula/calc/__init__.py +0 -0
  68. corems/molecular_formula/factory/MolecularFormulaFactory.py +895 -0
  69. corems/molecular_formula/factory/__init__.py +0 -0
  70. corems/molecular_formula/input/__init__.py +0 -0
  71. corems/molecular_formula/input/masslist_ref.py +355 -0
  72. corems/molecular_id/__init__.py +0 -0
  73. corems/molecular_id/calc/ClusterFilter.py +251 -0
  74. corems/molecular_id/calc/MolecularFilter.py +122 -0
  75. corems/molecular_id/calc/SpectralSimilarity.py +632 -0
  76. corems/molecular_id/calc/__init__.py +0 -0
  77. corems/molecular_id/calc/math_distance.py +1637 -0
  78. corems/molecular_id/data/FAMES_REF.msp +980 -0
  79. corems/molecular_id/data/PNNLMetV20191015.msp +157267 -0
  80. corems/molecular_id/factory/EI_SQL.py +650 -0
  81. corems/molecular_id/factory/MolecularLookupTable.py +914 -0
  82. corems/molecular_id/factory/__init__.py +0 -0
  83. corems/molecular_id/factory/classification.py +884 -0
  84. corems/molecular_id/factory/lipid_molecular_metadata.py +50 -0
  85. corems/molecular_id/factory/molecularSQL.py +827 -0
  86. corems/molecular_id/factory/spectrum_search_results.py +119 -0
  87. corems/molecular_id/input/__init__.py +0 -0
  88. corems/molecular_id/input/nistMSI.py +148 -0
  89. corems/molecular_id/search/__init__.py +0 -0
  90. corems/molecular_id/search/compoundSearch.py +214 -0
  91. corems/molecular_id/search/database_interfaces.py +1527 -0
  92. corems/molecular_id/search/findOxygenPeaks.py +330 -0
  93. corems/molecular_id/search/lcms_spectral_search.py +348 -0
  94. corems/molecular_id/search/molecularFormulaSearch.py +1117 -0
  95. corems/molecular_id/search/priorityAssignment.py +723 -0
  96. corems/ms_peak/__init__.py +0 -0
  97. corems/ms_peak/calc/MSPeakCalc.py +1010 -0
  98. corems/ms_peak/calc/__init__.py +0 -0
  99. corems/ms_peak/factory/MSPeakClasses.py +542 -0
  100. corems/ms_peak/factory/__init__.py +0 -0
  101. corems/transient/__init__.py +0 -0
  102. corems/transient/calc/TransientCalc.py +362 -0
  103. corems/transient/calc/__init__.py +0 -0
  104. corems/transient/factory/TransientClasses.py +457 -0
  105. corems/transient/factory/__init__.py +0 -0
  106. corems/transient/input/__init__.py +0 -0
  107. corems/transient/input/brukerSolarix.py +461 -0
  108. corems/transient/input/midasDatFile.py +172 -0
  109. corems-4.0.0.dist-info/METADATA +475 -0
  110. corems-4.0.0.dist-info/RECORD +171 -0
  111. corems-4.0.0.dist-info/WHEEL +5 -0
  112. corems-4.0.0.dist-info/licenses/LICENSE +22 -0
  113. corems-4.0.0.dist-info/top_level.txt +4 -0
  114. examples/archive/scripts/CoreMS_tutorial.py +94 -0
  115. examples/archive/scripts/DI HR-MS Halogens Bruker.py +196 -0
  116. examples/archive/scripts/DI HR-MS MassList.py +385 -0
  117. examples/archive/scripts/GC-MS MetabRef.py +213 -0
  118. examples/archive/scripts/GC-MS NetCDF.py +217 -0
  119. examples/archive/scripts/HR-MS Thermo Raw 21T.py +136 -0
  120. examples/archive/scripts/LC-ICPMS_metal_peaks.py +297 -0
  121. examples/archive/scripts/LCMS-Thermo.py +460 -0
  122. examples/archive/scripts/LCMS_isotopes.py +283 -0
  123. examples/archive/scripts/MSParams_example.py +21 -0
  124. examples/archive/scripts/Molecular Formula Data Aggreation.py +84 -0
  125. examples/archive/scripts/Single Mz Search.py +69 -0
  126. examples/test_notebooks.py +145 -0
  127. ext_lib/ChemstationMSFileReader.dll +0 -0
  128. ext_lib/ChemstationMSFileReader.xml +126 -0
  129. ext_lib/RawFileReaderLicense.doc +0 -0
  130. ext_lib/ThermoFisher.CommonCore.BackgroundSubtraction.dll +0 -0
  131. ext_lib/ThermoFisher.CommonCore.BackgroundSubtraction.xml +2307 -0
  132. ext_lib/ThermoFisher.CommonCore.Data.dll +0 -0
  133. ext_lib/ThermoFisher.CommonCore.Data.xml +28974 -0
  134. ext_lib/ThermoFisher.CommonCore.MassPrecisionEstimator.dll +0 -0
  135. ext_lib/ThermoFisher.CommonCore.MassPrecisionEstimator.xml +241 -0
  136. ext_lib/ThermoFisher.CommonCore.RawFileReader.dll +0 -0
  137. ext_lib/ThermoFisher.CommonCore.RawFileReader.xml +31174 -0
  138. ext_lib/__init__.py +0 -0
  139. ext_lib/dotnet/OpenMcdf.Extensions.dll +0 -0
  140. ext_lib/dotnet/OpenMcdf.dll +0 -0
  141. ext_lib/dotnet/OpenMcdf.xml +1154 -0
  142. ext_lib/dotnet/System.IO.FileSystem.AccessControl.dll +0 -0
  143. ext_lib/dotnet/System.IO.FileSystem.AccessControl.xml +506 -0
  144. ext_lib/dotnet/System.Security.AccessControl.dll +0 -0
  145. ext_lib/dotnet/System.Security.AccessControl.xml +2043 -0
  146. ext_lib/dotnet/System.Security.Principal.Windows.dll +0 -0
  147. ext_lib/dotnet/System.Security.Principal.Windows.xml +1011 -0
  148. ext_lib/dotnet/ThermoFisher.CommonCore.BackgroundSubtraction.dll +0 -0
  149. ext_lib/dotnet/ThermoFisher.CommonCore.BackgroundSubtraction.xml +2307 -0
  150. ext_lib/dotnet/ThermoFisher.CommonCore.Data.dll +0 -0
  151. ext_lib/dotnet/ThermoFisher.CommonCore.Data.xml +29148 -0
  152. ext_lib/dotnet/ThermoFisher.CommonCore.MassPrecisionEstimator.dll +0 -0
  153. ext_lib/dotnet/ThermoFisher.CommonCore.MassPrecisionEstimator.xml +241 -0
  154. ext_lib/dotnet/ThermoFisher.CommonCore.RawFileReader.dll +0 -0
  155. ext_lib/dotnet/ThermoFisher.CommonCore.RawFileReader.xml +31492 -0
  156. ext_lib/version +18 -0
  157. support_code/atom_parsers/AtomsDescription.py +359 -0
  158. support_code/atom_parsers/CreateAtomsDescription.py +56 -0
  159. support_code/nmdc/filefinder.py +90 -0
  160. support_code/nmdc/lipidomics/lipidomics_workflow.py +748 -0
  161. support_code/nmdc/lipidomics/manifest_examples.py +49 -0
  162. support_code/nmdc/metabolomics/gcms_workflow.py +158 -0
  163. support_code/nmdc/metabolomics/lcms_metabolomics_targeted_search.py +59 -0
  164. support_code/nmdc/metabolomics/lcms_metabolomics_workflow.py +248 -0
  165. support_code/nmdc/metabolomics/metabolomics_collection.py +628 -0
  166. support_code/nmdc/metadata/dms_api.py +42 -0
  167. support_code/nmdc/nom/archived_scripts/nmdc_metadata_gen.py +288 -0
  168. support_code/nmdc/nom/archived_scripts/nom_grow_workflow.py +209 -0
  169. support_code/nmdc/nom/nom_workflow.py +312 -0
  170. support_code/windows_only_importers/BrukerCompassXtract.py +180 -0
  171. support_code/windows_only_importers/ThermoMSFileReader.py +405 -0
@@ -0,0 +1,668 @@
1
+ from collections import defaultdict
2
+ from pathlib import Path
3
+ from typing import Optional, Union, List, Tuple
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ import pymzml
8
+ import datetime
9
+
10
+ from corems.encapsulation.constant import Labels
11
+ from corems.encapsulation.factory.parameters import default_parameters
12
+ from corems.mass_spectra.factory.lc_class import LCMSBase, MassSpectraBase
13
+ from corems.mass_spectra.input.parserbase import SpectraParserInterface
14
+ from corems.mass_spectrum.factory.MassSpectrumClasses import (
15
+ MassSpecCentroid,
16
+ MassSpecProfile,
17
+ )
18
+
19
+
20
+ class MZMLSpectraParser(SpectraParserInterface):
21
+ """A class for parsing mzml spectrometry data files into MassSpectraBase or LCMSBase objects
22
+
23
+ Parameters
24
+ ----------
25
+ file_location : str or Path
26
+ The path to the RAW file to be parsed.
27
+ analyzer : str, optional
28
+ The type of mass analyzer used in the instrument. Default is "Unknown".
29
+ instrument_label : str, optional
30
+ The name of the instrument used to acquire the data. Default is "Unknown".
31
+ sample_name : str, optional
32
+ The name of the sample being analyzed. If not provided, the stem of the file_location path will be used.
33
+
34
+ Attributes
35
+ ----------
36
+ file_location : Path
37
+ The path to the RAW file being parsed.
38
+ analyzer : str
39
+ The type of mass analyzer used in the instrument.
40
+ instrument_label : str
41
+ The name of the instrument used to acquire the data.
42
+ sample_name : str
43
+ The name of the sample being analyzed.
44
+
45
+ Methods
46
+ -------
47
+ * load().
48
+ Load mzML file using pymzml.run.Reader and return the data as a numpy array.
49
+ * run(spectra=True).
50
+ Parses the mzml file and returns a dictionary of mass spectra dataframes and a scan metadata dataframe.
51
+ * get_mass_spectrum_from_scan(scan_number, polarity, auto_process=True)
52
+ Parses the mzml file and returns a MassSpecBase object from a single scan.
53
+ * get_mass_spectra_obj().
54
+ Parses the mzml file and instantiates a MassSpectraBase object.
55
+ * get_lcms_obj().
56
+ Parses the mzml file and instantiates an LCMSBase object.
57
+ * get_instrument_info().
58
+ Return instrument information from the mzML file.
59
+ * get_creation_time().
60
+ Return the creation time of the mzML file as a datetime object.
61
+
62
+ Inherits from ThermoBaseClass and SpectraParserInterface
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ file_location,
68
+ analyzer="Unknown",
69
+ instrument_label="Unknown",
70
+ sample_name=None,
71
+ ):
72
+ # implementation details
73
+ if isinstance(file_location, str):
74
+ # if obj is a string it defaults to create a Path obj, pass the S3Path if needed
75
+ file_location = Path(file_location)
76
+ if not file_location.exists():
77
+ raise FileExistsError("File does not exist: " + str(file_location))
78
+ self.file_location = file_location
79
+ self.analyzer = analyzer
80
+ self.instrument_label = instrument_label
81
+
82
+ if sample_name:
83
+ self.sample_name = sample_name
84
+ else:
85
+ self.sample_name = file_location.stem
86
+
87
+ def load(self):
88
+ """
89
+ Load mzML file using pymzml.run.Reader and return the data as a numpy array.
90
+
91
+ Returns
92
+ -------
93
+ numpy.ndarray
94
+ The mass spectra data as a numpy array.
95
+ """
96
+ data = pymzml.run.Reader(self.file_location)
97
+ return data
98
+
99
+ def get_scan_df(self, data=None, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None):
100
+ """
101
+ Return scan data as a pandas DataFrame.
102
+
103
+ Parameters
104
+ ----------
105
+ data : pymzml.run.Reader, optional
106
+ The mass spectra data. If None, will load the data.
107
+ time_range : tuple or list of tuples, optional
108
+ Retention time range(s) to filter scans. Can be:
109
+ - Single range: (start_time, end_time) in minutes
110
+ - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes
111
+ If None, returns all scans.
112
+
113
+ Returns
114
+ -------
115
+ pandas.DataFrame
116
+ A pandas DataFrame containing metadata for each scan, including scan number, MS level, polarity, and scan time.
117
+ """
118
+ if data is None:
119
+ data = self.load()
120
+ # Scan dict
121
+ # instatinate scan dict, with empty lists of size of scans
122
+ n_scans = data.get_spectrum_count()
123
+ scan_dict = {
124
+ "scan": np.empty(n_scans, dtype=np.int32),
125
+ "scan_time": np.empty(n_scans, dtype=np.float32),
126
+ "ms_level": [None] * n_scans,
127
+ "polarity": [None] * n_scans,
128
+ "precursor_mz": [None] * n_scans,
129
+ "scan_text": [None] * n_scans,
130
+ "scan_window_lower": np.empty(n_scans, dtype=np.float32),
131
+ "scan_window_upper": np.empty(n_scans, dtype=np.float32),
132
+ "scan_precision": [None] * n_scans,
133
+ "tic": np.empty(n_scans, dtype=np.float32),
134
+ "ms_format": [None] * n_scans,
135
+ }
136
+
137
+ # First pass: loop through scans to get scan info
138
+ for i, spec in enumerate(data):
139
+ scan_dict["scan"][i] = spec.ID
140
+ scan_dict["ms_level"][i] = spec.ms_level
141
+ scan_dict["scan_precision"][i] = spec._measured_precision
142
+ scan_dict["tic"][i] = spec.TIC
143
+ if spec.selected_precursors:
144
+ scan_dict["precursor_mz"][i] = spec.selected_precursors[0].get(
145
+ "mz", None
146
+ )
147
+ if spec["negative scan"] is not None:
148
+ scan_dict["polarity"][i] = "negative"
149
+ if spec["positive scan"] is not None:
150
+ scan_dict["polarity"][i] = "positive"
151
+ if spec["negative scan"] is not None and spec["positive scan"] is not None:
152
+ raise ValueError(
153
+ "Error: scan {0} has both negative and positive polarity".format(
154
+ spec.ID
155
+ )
156
+ )
157
+
158
+ scan_dict["scan_time"][i] = spec.get("MS:1000016")
159
+ scan_dict["scan_text"][i] = spec.get("MS:1000512")
160
+ scan_dict["scan_window_lower"][i] = spec.get("MS:1000501")
161
+ scan_dict["scan_window_upper"][i] = spec.get("MS:1000500")
162
+ if spec.get("MS:1000128"):
163
+ scan_dict["ms_format"][i] = "profile"
164
+ elif spec.get("MS:1000127"):
165
+ scan_dict["ms_format"][i] = "centroid"
166
+ else:
167
+ scan_dict["ms_format"][i] = None
168
+
169
+ scan_df = pd.DataFrame(scan_dict)
170
+
171
+ # Remove any non-mass spectra scans (e.g., MS level 0 or None)
172
+ scan_df = scan_df[scan_df.ms_level.notnull() & (scan_df.ms_level > 0)].reset_index(drop=True)
173
+
174
+ # Apply time range filtering if specified
175
+ if time_range is not None:
176
+ time_ranges = self._normalize_time_range(time_range)
177
+ # Create a mask for scans within any of the time ranges
178
+ mask = np.zeros(len(scan_df), dtype=bool)
179
+ for start_time, end_time in time_ranges:
180
+ mask |= (scan_df["scan_time"] >= start_time) & (scan_df["scan_time"] <= end_time)
181
+ scan_df = scan_df[mask].reset_index(drop=True)
182
+
183
+ return scan_df
184
+
185
+ def get_ms_raw(self, spectra, scan_df, data=None, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None):
186
+ """Return a dictionary of mass spectra data as a pandas DataFrame.
187
+
188
+ Parameters
189
+ ----------
190
+ spectra : str
191
+ Which mass spectra data to include in the output.
192
+ Options: None, "ms1", "ms2", "all".
193
+ scan_df : pandas.DataFrame
194
+ Scan dataframe. Output from get_scan_df().
195
+ data : pymzml.run.Reader, optional
196
+ The mass spectra data. If None, will load the data.
197
+ time_range : tuple or list of tuples, optional
198
+ Retention time range(s) to filter scans. Can be:
199
+ - Single range: (start_time, end_time) in minutes
200
+ - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes
201
+ If None, returns all scans. Note: filtering is typically done at scan_df level.
202
+
203
+ Returns
204
+ -------
205
+ dict
206
+ A dictionary containing the mass spectra data as pandas DataFrames, with keys corresponding to the MS level.
207
+
208
+ """
209
+ if data is None:
210
+ data = self.load()
211
+ if spectra == "all":
212
+ scan_df_forspec = scan_df
213
+ elif spectra == "ms1":
214
+ scan_df_forspec = scan_df[scan_df.ms_level == 1]
215
+ elif spectra == "ms2":
216
+ scan_df_forspec = scan_df[scan_df.ms_level == 2]
217
+ else:
218
+ raise ValueError("spectra must be 'all', 'ms1', or 'ms2'")
219
+
220
+ # Result container
221
+ res = {}
222
+
223
+ # Row count container
224
+ counter = {}
225
+
226
+ # Column name container
227
+ cols = {}
228
+
229
+ # set at float32
230
+ dtype = np.float32
231
+
232
+ # First pass: get nrows
233
+ N = defaultdict(lambda: 0)
234
+ for i, spec in enumerate(data):
235
+ if spec.ID in scan_df_forspec.scan.values:
236
+ # Get ms level
237
+ level = "ms{}".format(spec.ms_level)
238
+
239
+ # Number of rows
240
+ N[level] += spec.mz.shape[0]
241
+
242
+ # Second pass: parse
243
+ for i, spec in enumerate(data):
244
+ if spec.ID in scan_df_forspec.scan.values:
245
+ # Number of rows
246
+ n = spec.mz.shape[0]
247
+
248
+ # No measurements
249
+ if n == 0:
250
+ continue
251
+
252
+ # Dimension check
253
+ if len(spec.mz) != len(spec.i):
254
+ # raise an error if the mz and intensity arrays are not the same length
255
+ raise ValueError("m/z and intensity array dimension mismatch")
256
+
257
+ # Scan/frame info
258
+ id_dict = spec.id_dict
259
+
260
+ # Get ms level
261
+ level = "ms{}".format(spec.ms_level)
262
+
263
+ # Columns
264
+ cols[level] = list(id_dict.keys()) + ["mz", "intensity"]
265
+ m = len(cols[level])
266
+
267
+ # Subarray init
268
+ arr = np.empty((n, m), dtype=dtype)
269
+ inx = 0
270
+
271
+ # Populate scan/frame info
272
+ for k, v in id_dict.items():
273
+ arr[:, inx] = v
274
+ inx += 1
275
+
276
+ # Populate m/z
277
+ arr[:, inx] = spec.mz
278
+ inx += 1
279
+
280
+ # Populate intensity
281
+ arr[:, inx] = spec.i
282
+ inx += 1
283
+
284
+ # Initialize output container
285
+ if level not in res:
286
+ res[level] = np.empty((N[level], m), dtype=dtype)
287
+ counter[level] = 0
288
+
289
+ # Insert subarray
290
+ res[level][counter[level] : counter[level] + n, :] = arr
291
+ counter[level] += n
292
+
293
+ # Construct ms1 and ms2 mz dataframes
294
+ for level in res.keys():
295
+ res[level] = pd.DataFrame(res[level], columns=cols[level]).drop(
296
+ columns=["controllerType", "controllerNumber"],
297
+ )
298
+
299
+ return res
300
+
301
+ def run(self, spectra="all", scan_df=None, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None):
302
+ """Parse the mzML file and return a dictionary of spectra dataframes and a scan metadata dataframe.
303
+
304
+ Parameters
305
+ ----------
306
+ spectra : str, optional
307
+ Which mass spectra data to include in the output. Default is "all".
308
+ Other options: None, "ms1", "ms2".
309
+ scan_df : pandas.DataFrame, optional
310
+ Scan dataframe. If not provided, the scan dataframe is created from the mzML file.
311
+ time_range : tuple or list of tuples, optional
312
+ Retention time range(s) to load. Can be:
313
+ - Single range: (start_time, end_time) in minutes
314
+ - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes
315
+ If None, loads all scans.
316
+
317
+ Returns
318
+ -------
319
+ tuple
320
+ A tuple containing two elements:
321
+ - A dictionary containing the mass spectra data as numpy arrays, with keys corresponding to the MS level.
322
+ - A pandas DataFrame containing metadata for each scan, including scan number, MS level, polarity, and scan time.
323
+ """
324
+
325
+ # Open file
326
+ data = self.load()
327
+
328
+ if scan_df is None:
329
+ scan_df = self.get_scan_df(data, time_range=time_range)
330
+
331
+ if spectra != "none":
332
+ res = self.get_ms_raw(spectra, scan_df, data)
333
+
334
+ else:
335
+ res = None
336
+
337
+ return res, scan_df
338
+
339
+ def get_mass_spectrum_from_scan(
340
+ self, scan_number, spectrum_mode, auto_process=True
341
+ ):
342
+ """Instatiate a mass spectrum object from the mzML file.
343
+
344
+ Parameters
345
+ ----------
346
+ scan_number : int
347
+ The scan number to be parsed.
348
+ spectrum_mode : str
349
+ The type of spectrum to instantiate. Must be'profile' or 'centroid'.
350
+ polarity : int
351
+ The polarity of the scan. Must be -1 or 1.
352
+ auto_process : bool, optional
353
+ If True, process the mass spectrum. Default is True.
354
+
355
+ Returns
356
+ -------
357
+ MassSpecProfile | MassSpecCentroid
358
+ The MassSpecProfile or MassSpecCentroid object containing the parsed mass spectrum.
359
+ """
360
+ # Use the batch function and return the first result
361
+ result_list = self.get_mass_spectra_from_scan_list(
362
+ [scan_number], spectrum_mode, auto_process
363
+ )
364
+ return result_list[0] if result_list else None
365
+
366
+ def get_mass_spectra_from_scan_list(
367
+ self, scan_list, spectrum_mode, auto_process=True
368
+ ):
369
+ """Instatiate mass spectrum objects from the mzML file.
370
+
371
+ Parameters
372
+ ----------
373
+ scan_list : list of int
374
+ The scan numbers to be parsed.
375
+ spectrum_mode : str
376
+ The type of spectrum to instantiate. Must be'profile' or 'centroid'.
377
+ auto_process : bool, optional
378
+ If True, process the mass spectrum. Default is True.
379
+
380
+ Returns
381
+ -------
382
+ list of MassSpecProfile | MassSpecCentroid
383
+ List of MassSpecProfile or MassSpecCentroid objects containing the parsed mass spectra.
384
+ """
385
+
386
+ def set_metadata(
387
+ scan_number: int,
388
+ polarity: int,
389
+ file_location: str,
390
+ label=Labels.thermo_profile,
391
+ ):
392
+ """
393
+ Set the output parameters for creating a MassSpecProfile or MassSpecCentroid object.
394
+
395
+ Parameters
396
+ ----------
397
+ scan_number : int
398
+ The scan number.
399
+ polarity : int
400
+ The polarity of the data.
401
+ file_location : str
402
+ The file location.
403
+ label : str, optional
404
+ The label for the mass spectrum. Default is Labels.thermo_profile.
405
+
406
+ Returns
407
+ -------
408
+ dict
409
+ The output parameters ready for creating a MassSpecProfile or MassSpecCentroid object.
410
+ """
411
+ d_params = default_parameters(file_location)
412
+ d_params["label"] = label
413
+ d_params["polarity"] = polarity
414
+ d_params["filename_path"] = file_location
415
+ d_params["scan_number"] = scan_number
416
+
417
+ return d_params
418
+
419
+ # Open file
420
+ data = self.load()
421
+
422
+ mass_spectrum_objects = []
423
+ scan_set = set(scan_list)
424
+ spec_by_id = {}
425
+
426
+ # Iterate once through the file to collect all requested scans.
427
+ # Direct random-access via data[scan_number] uses pymzml's byte-offset
428
+ # index, which is unreliable on Windows (CRLF vs LF byte offsets).
429
+ for spec in data:
430
+ if spec.ID in scan_set:
431
+ spec_by_id[spec.ID] = spec
432
+
433
+ for scan_number in scan_list:
434
+ spec = spec_by_id.get(scan_number)
435
+ if spec is None:
436
+ raise ValueError(
437
+ "Scan number %d not found in mzML file" % scan_number
438
+ )
439
+
440
+ # Get polarity
441
+ if spec["negative scan"] is not None:
442
+ polarity = -1
443
+ elif spec["positive scan"] is not None:
444
+ polarity = 1
445
+
446
+ # Get mass spectrum
447
+ if spectrum_mode == "profile":
448
+ # Check if profile
449
+ if not spec.get("MS:1000128"):
450
+ raise ValueError("spectrum is not profile")
451
+ data_dict = {
452
+ Labels.mz: spec.mz,
453
+ Labels.abundance: spec.i,
454
+ }
455
+ d_params = set_metadata(
456
+ scan_number,
457
+ polarity,
458
+ self.file_location,
459
+ label=Labels.simulated_profile,
460
+ )
461
+ mass_spectrum_obj = MassSpecProfile(
462
+ data_dict, d_params, auto_process=auto_process
463
+ )
464
+ elif spectrum_mode == "centroid":
465
+ # Check if centroided
466
+ if not spec.get("MS:1000127"):
467
+ raise ValueError("spectrum is not centroided")
468
+ data_dict = {
469
+ Labels.mz: spec.mz,
470
+ Labels.abundance: spec.i,
471
+ Labels.rp: [np.nan] * len(spec.mz),
472
+ Labels.s2n: [np.nan] * len(spec.i),
473
+ }
474
+ d_params = set_metadata(
475
+ scan_number, polarity, self.file_location, label=Labels.corems_centroid
476
+ )
477
+ mass_spectrum_obj = MassSpecCentroid(
478
+ data_dict, d_params, auto_process=auto_process
479
+ )
480
+
481
+ mass_spectrum_objects.append(mass_spectrum_obj)
482
+
483
+ return mass_spectrum_objects
484
+
485
+ def get_mass_spectra_obj(self, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None):
486
+ """Instatiate a MassSpectraBase object from the mzML file.
487
+
488
+ Parameters
489
+ ----------
490
+ time_range : tuple or list of tuples, optional
491
+ Retention time range(s) to load. Can be:
492
+ - Single range: (start_time, end_time) in minutes
493
+ - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes
494
+ If None, loads all scans.
495
+
496
+ Returns
497
+ -------
498
+ MassSpectraBase
499
+ The MassSpectra object containing the parsed mass spectra.
500
+ The object is instatiated with the mzML file, analyzer, instrument, sample name, and scan dataframe.
501
+ """
502
+ _, scan_df = self.run(spectra=False, time_range=time_range)
503
+ mass_spectra_obj = MassSpectraBase(
504
+ self.file_location,
505
+ self.analyzer,
506
+ self.instrument_label,
507
+ self.sample_name,
508
+ self,
509
+ )
510
+ scan_df = scan_df.set_index("scan", drop=False)
511
+ mass_spectra_obj.scan_df = scan_df
512
+
513
+ return mass_spectra_obj
514
+
515
+ def get_lcms_obj(self, spectra="all", time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None):
516
+ """Instatiates a LCMSBase object from the mzML file.
517
+
518
+ Parameters
519
+ ----------
520
+ spectra : str, optional
521
+ Which mass spectra data to include in the output. Default is all. Other options: none, ms1, ms2.
522
+ time_range : tuple or list of tuples, optional
523
+ Retention time range(s) to load. Can be:
524
+ - Single range: (start_time, end_time) in minutes
525
+ - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes
526
+ If None, loads all scans. Useful for targeted workflows to improve performance.
527
+
528
+ Returns
529
+ -------
530
+ LCMSBase
531
+ LCMS object containing mass spectra data.
532
+ The object is instatiated with the mzML file, analyzer, instrument, sample name, scan dataframe,
533
+ and mz dataframe(s), as well as lists of scan numbers, retention times, and TICs.
534
+ """
535
+ _, scan_df = self.run(spectra="none", time_range=time_range) # first run it to just get scan info
536
+ if spectra != "none":
537
+ res, scan_df = self.run(
538
+ scan_df=scan_df, spectra=spectra, time_range=time_range
539
+ ) # second run to parse data
540
+ lcms_obj = LCMSBase(
541
+ self.file_location,
542
+ self.analyzer,
543
+ self.instrument_label,
544
+ self.sample_name,
545
+ self,
546
+ )
547
+ if spectra != "none":
548
+ for key in res:
549
+ key_int = int(key.replace("ms", ""))
550
+ res[key] = res[key][res[key].intensity > 0]
551
+ res[key] = res[key].sort_values(by=["scan", "mz"]).reset_index(drop=True)
552
+ lcms_obj._ms_unprocessed[key_int] = res[key]
553
+ lcms_obj.scan_df = scan_df.set_index("scan", drop=False)
554
+ # Check if polarity is mixed
555
+ if len(set(scan_df.polarity)) > 1:
556
+ raise ValueError("Mixed polarities detected in scan data")
557
+ lcms_obj.polarity = scan_df.polarity[0]
558
+ lcms_obj._scans_number_list = list(scan_df.scan)
559
+ lcms_obj._retention_time_list = list(scan_df.scan_time)
560
+ lcms_obj._tic_list = list(scan_df.tic)
561
+
562
+ return lcms_obj
563
+
564
+ def get_scans_in_time_range(
565
+ self,
566
+ time_range: Union[Tuple[float, float], List[Tuple[float, float]]],
567
+ ms_level: Optional[int] = None
568
+ ) -> List[int]:
569
+ """
570
+ Return scan numbers within specified retention time range(s).
571
+
572
+ This method provides efficient filtering of scans by retention time,
573
+ which is particularly useful for targeted workflows where only specific
574
+ time windows are of interest.
575
+
576
+ Parameters
577
+ ----------
578
+ time_range : tuple or list of tuples
579
+ Retention time range(s) in minutes. Can be:
580
+ - Single range: (start_time, end_time)
581
+ - Multiple ranges: [(start1, end1), (start2, end2), ...]
582
+ ms_level : int, optional
583
+ If specified, only return scans of this MS level (e.g., 1 for MS1, 2 for MS2).
584
+ If None, returns scans of all MS levels.
585
+
586
+ Returns
587
+ -------
588
+ list of int
589
+ List of scan numbers within the specified time range(s) and MS level.
590
+
591
+ Examples
592
+ --------
593
+ Get MS1 scans between 1.0 and 2.0 minutes:
594
+
595
+ >>> scans = parser.get_scans_in_time_range((1.0, 2.0), ms_level=1)
596
+
597
+ Get scans in multiple time windows:
598
+
599
+ >>> scans = parser.get_scans_in_time_range([(0.5, 1.5), (3.0, 4.0)])
600
+ """
601
+ # Get scan dataframe filtered by time range
602
+ scan_df = self.get_scan_df(time_range=time_range)
603
+
604
+ # Further filter by MS level if specified
605
+ if ms_level is not None:
606
+ scan_df = scan_df[scan_df.ms_level == ms_level]
607
+
608
+ # Return list of scan numbers
609
+ return scan_df.scan.tolist()
610
+
611
+ def get_instrument_info(self):
612
+ """
613
+ Return instrument information.
614
+
615
+ Returns
616
+ -------
617
+ dict
618
+ A dictionary with the keys 'model' and 'serial_number'.
619
+ """
620
+ # Load the pymzml data
621
+ data = self.load()
622
+ instrument_info = data.info.get('referenceable_param_group_list_element')[0]
623
+ cv_params = instrument_info.findall('{http://psi.hupo.org/ms/mzml}cvParam')
624
+
625
+ # Extract details from each cvParam
626
+ params = []
627
+ for param in cv_params:
628
+ accession = param.get('accession') # Get 'accession' attribute
629
+ name = param.get('name') # Get 'name' attribute
630
+ value = param.get('value') # Get 'value' attribute
631
+ params.append({
632
+ 'accession': accession,
633
+ 'name': name,
634
+ 'value': value
635
+ })
636
+
637
+ # Loop through params and try to find the relevant information
638
+ instrument_dict = {
639
+ 'model': 'Unknown',
640
+ 'serial_number': 'Unknown'
641
+ }
642
+
643
+ # Assuming there are only two paramters here - one is for the serial number (agnostic to the model) and the other is for the model
644
+ # If there are more than two, we raise an error
645
+ if len(params) < 2:
646
+ raise ValueError("Not enough parameters found in the instrument info, cannot parse.")
647
+ if len(params) > 2:
648
+ raise ValueError("Too many parameters found in the instrument info, cannot parse.")
649
+ for param in params:
650
+ if param['accession'] == 'MS:1000529':
651
+ instrument_dict['serial_number'] = param['value']
652
+ else:
653
+ instrument_dict['model'] = data.OT[param['accession']]
654
+
655
+ return instrument_dict
656
+
657
+ def get_creation_time(self) -> datetime.datetime:
658
+ """
659
+ Return the creation time of the mzML file.
660
+ """
661
+ data = self.load()
662
+ write_time = data.info.get('start_time')
663
+ if write_time:
664
+ # Convert the write time to a datetime object
665
+ return datetime.datetime.strptime(write_time, "%Y-%m-%dT%H:%M:%SZ")
666
+ else:
667
+ raise ValueError("Creation time is not available in the mzML file. "
668
+ "Please ensure the file contains the 'start_time' information.")