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,1710 @@
1
+ __author__ = "Yuri E. Corilo"
2
+ __date__ = "Oct 29, 2019"
3
+
4
+
5
+ from threading import Thread
6
+ import h5py
7
+ import toml
8
+ import json
9
+ import multiprocessing
10
+ from pathlib import Path
11
+ import datetime
12
+ from typing import Union, Tuple, List, Optional
13
+
14
+ import numpy as np
15
+ import pandas as pd
16
+ import warnings
17
+
18
+ from corems.chroma_peak.factory.chroma_peak_classes import LCMSMassFeature
19
+ from corems.encapsulation.input.parameter_from_json import (
20
+ load_and_set_json_parameters_lcms,
21
+ load_and_set_toml_parameters_lcms,
22
+ )
23
+ from corems.mass_spectra.factory.lc_class import LCMSBase, MassSpectraBase, LCMSCollection
24
+ from corems.mass_spectra.factory.chromat_data import EIC_Data
25
+ from corems.mass_spectra.input.parserbase import SpectraParserInterface
26
+ from corems.mass_spectrum.input.coremsHDF5 import ReadCoreMSHDF_MassSpectrum
27
+ from corems.molecular_id.factory.spectrum_search_results import SpectrumSearchResults
28
+ from corems.mass_spectra.input.rawFileReader import ImportMassSpectraThermoMSFileReader
29
+ from corems.mass_spectra.input.mzml import MZMLSpectraParser
30
+
31
+
32
+ def create_manifest_from_folder(
33
+ folder_path: Path,
34
+ output_path: Path = None,
35
+ batch_time_threshold_hours: float = 12.0,
36
+ center_name: str = None,
37
+ overwrite: bool = False
38
+ ) -> Path:
39
+ """
40
+ Create a manifest CSV file for ReadCoreMSHDFMassSpectraCollection from CoreMS HDF5 files.
41
+
42
+ Scans a folder for .corems subdirectories and generates a manifest with columns:
43
+ sample_name, batch, order, center, time. Files are batched by creation time, and
44
+ one sample is designated as the retention time alignment center.
45
+
46
+ Parameters
47
+ ----------
48
+ folder_path : Path
49
+ Path to folder containing .corems subdirectories with HDF5 files.
50
+ output_path : Path, optional
51
+ Output manifest CSV path. Default: folder_path/manifest.csv.
52
+ batch_time_threshold_hours : float, optional
53
+ Time gap in hours for batch separation. Default: 12.0.
54
+ center_name : str, optional
55
+ Sample name to designate as RT alignment center (must exist in samples).
56
+ If None, the middle sample (by creation time) is used.
57
+ overwrite : bool, optional
58
+ Whether to overwrite existing manifest. Default: False.
59
+
60
+ Returns
61
+ -------
62
+ Path
63
+ Path to created manifest file.
64
+
65
+ Raises
66
+ ------
67
+ FileNotFoundError
68
+ If folder_path doesn't exist or contains no .corems subdirectories.
69
+ FileExistsError
70
+ If output file exists and overwrite is False.
71
+ ValueError
72
+ If no HDF5 files found, or center_name doesn't match any sample.
73
+ """
74
+ if not folder_path.exists():
75
+ raise FileNotFoundError(f"Folder {folder_path} does not exist.")
76
+
77
+ # Set default output path if not provided
78
+ if output_path is None:
79
+ output_path = folder_path / "manifest.csv"
80
+
81
+ # Check if output file exists
82
+ if output_path.exists() and not overwrite:
83
+ raise FileExistsError(
84
+ f"Manifest file {output_path} already exists. "
85
+ "Set overwrite=True to replace it."
86
+ )
87
+
88
+ # Find all .corems subdirectories
89
+ corems_dirs = sorted([d for d in folder_path.iterdir() if d.is_dir() and d.suffix == ".corems"])
90
+
91
+ if not corems_dirs:
92
+ raise FileNotFoundError(
93
+ f"No .corems subdirectories found in {folder_path}. "
94
+ "Ensure the folder contains processed CoreMS data."
95
+ )
96
+
97
+ # Collect sample information
98
+ sample_data = []
99
+
100
+ for corems_dir in corems_dirs:
101
+ sample_name = corems_dir.stem # Remove .corems extension
102
+ hdf5_file = corems_dir / f"{sample_name}.hdf5"
103
+
104
+ if not hdf5_file.exists():
105
+ print(f"Warning: HDF5 file not found for {sample_name}, skipping.")
106
+ continue
107
+
108
+ # Get creation time using the ReadCoreMSHDFMassSpectra method
109
+ try:
110
+ # Use context manager to ensure file is properly closed
111
+ with ReadCoreMSHDFMassSpectra(str(hdf5_file)) as parser:
112
+ # Use the get_original_creation_time() method which checks HDF5 attrs first,
113
+ # then falls back to original parser if needed
114
+ creation_time = parser.get_original_creation_time()
115
+
116
+ # Skip sample if creation time unavailable
117
+ if creation_time is None:
118
+ print(f"Warning: Could not get original creation time for {sample_name}, skipping.")
119
+ continue
120
+
121
+ except Exception as e:
122
+ print(f"Warning: Error getting creation time for {sample_name}: {e}, skipping.")
123
+ continue
124
+
125
+ sample_data.append({
126
+ 'sample_name': sample_name,
127
+ 'creation_time': creation_time,
128
+ 'hdf5_path': hdf5_file
129
+ })
130
+
131
+ if not sample_data:
132
+ raise ValueError(
133
+ f"No valid HDF5 files found in {folder_path}. "
134
+ "Ensure .corems subdirectories contain .hdf5 files."
135
+ )
136
+
137
+ # Sort by creation time
138
+ sample_data.sort(key=lambda x: x['creation_time'])
139
+
140
+ # Assign batches based on time threshold
141
+ batch_assignments = []
142
+ current_batch = 1
143
+
144
+ for i, sample in enumerate(sample_data):
145
+ if i == 0:
146
+ batch_assignments.append(current_batch)
147
+ else:
148
+ time_diff = sample['creation_time'] - sample_data[i-1]['creation_time']
149
+ time_diff_hours = time_diff.total_seconds() / 3600
150
+
151
+ if time_diff_hours > batch_time_threshold_hours:
152
+ current_batch += 1
153
+
154
+ batch_assignments.append(current_batch)
155
+
156
+ # Determine which sample should be the center for retention time alignment
157
+ sample_names = [s['sample_name'] for s in sample_data]
158
+
159
+ if center_name is not None:
160
+ # Validate that center_name is in the discovered samples
161
+ if center_name not in sample_names:
162
+ raise ValueError(
163
+ f"Specified center_name '{center_name}' not found in discovered samples. "
164
+ f"Available samples: {', '.join(sample_names)}"
165
+ )
166
+ center_sample = center_name
167
+ else:
168
+ # Use the middle sample (by creation time) as center
169
+ middle_idx = len(sample_data) // 2
170
+ center_sample = sample_data[middle_idx]['sample_name']
171
+ print(f"Auto-selected center sample: {center_sample} (index {middle_idx} of {len(sample_data)}, middle by creation time)")
172
+
173
+ # Create manifest dataframe with center column as TRUE/FALSE
174
+ manifest_df = pd.DataFrame({
175
+ 'sample_name': sample_names,
176
+ 'batch': batch_assignments,
177
+ 'order': list(range(1, len(sample_data) + 1)),
178
+ 'center': ['TRUE' if name == center_sample else 'FALSE' for name in sample_names],
179
+ 'time': [s['creation_time'].strftime('%Y-%m-%dT%H:%M:%SZ') for s in sample_data]
180
+ })
181
+
182
+ # Sort manifest by time before saving to ensure proper order
183
+ manifest_df = manifest_df.sort_values('time').reset_index(drop=True)
184
+ # Update order column to reflect sorted order
185
+ manifest_df['order'] = list(range(1, len(manifest_df) + 1))
186
+
187
+ # Save manifest
188
+ manifest_df.to_csv(output_path, index=False)
189
+
190
+ print(f"Manifest created successfully at {output_path}")
191
+ print(f"Total samples: {len(sample_data)}")
192
+ print(f"Number of batches: {current_batch}")
193
+ print(f"Batch assignments: {dict(zip(range(1, current_batch + 1), [batch_assignments.count(b) for b in range(1, current_batch + 1)]))}")
194
+
195
+ return output_path
196
+
197
+
198
+ class ReadCoreMSHDFMassSpectra(
199
+ SpectraParserInterface, ReadCoreMSHDF_MassSpectrum, Thread
200
+ ):
201
+ """Class to read CoreMS HDF5 files and populate a LCMS or MassSpectraBase object.
202
+
203
+ Parameters
204
+ ----------
205
+ file_location : str
206
+ The location of the HDF5 file to read, including the suffix.
207
+
208
+ Attributes
209
+ ----------
210
+ file_location : str
211
+ The location of the HDF5 file to read.
212
+ h5pydata : h5py.File
213
+ The HDF5 file object.
214
+ scans : list
215
+ A list of the location of individual mass spectra within the HDF5 file.
216
+ scan_number_list : list
217
+ A list of the scan numbers of the mass spectra within the HDF5 file.
218
+ parameters_location : str
219
+ The location of the parameters file (json or toml).
220
+
221
+ Methods
222
+ -------
223
+ * import_mass_spectra(mass_spectra).
224
+ Imports all mass spectra from the HDF5 file onto the LCMS or MassSpectraBase object.
225
+ * get_mass_spectrum_from_scan(scan_number).
226
+ Return mass spectrum data object from scan number.
227
+ * load().
228
+ Placeholder method to meet the requirements of the SpectraParserInterface.
229
+ * run(mass_spectra).
230
+ Runs the importer functions to populate a LCMS or MassSpectraBase object.
231
+ * import_scan_info(mass_spectra).
232
+ Imports the scan info from the HDF5 file to populate the _scan_info attribute
233
+ on the LCMS or MassSpectraBase object
234
+ * import_ms_unprocessed(mass_spectra).
235
+ Imports the unprocessed mass spectra from the HDF5 file to populate the
236
+ _ms_unprocessed attribute on the LCMS or MassSpectraBase object
237
+ * import_parameters(mass_spectra).
238
+ Imports the parameters from the HDF5 file to populate the parameters
239
+ attribute on the LCMS or MassSpectraBase object
240
+ * import_mass_features(mass_spectra).
241
+ Imports the mass features from the HDF5 file to populate the mass_features
242
+ attribute on the LCMS or MassSpectraBase object
243
+ * import_eics(mass_spectra).
244
+ Imports the extracted ion chromatograms from the HDF5 file to populate the
245
+ eics attribute on the LCMS or MassSpectraBase object
246
+ * import_spectral_search_results(mass_spectra).
247
+ Imports the spectral search results from the HDF5 file to populate the
248
+ spectral_search_results attribute on the LCMS or MassSpectraBase object
249
+ * get_mass_spectra_obj().
250
+ Return mass spectra data object, populating the _ms list on the LCMS or
251
+ MassSpectraBase object from the HDF5 file
252
+ * get_lcms_obj().
253
+ Return LCMSBase object, populating the majority of the attributes on the
254
+ LCMS object from the HDF5 file
255
+
256
+ """
257
+
258
+ def __init__(self, file_location: str):
259
+ Thread.__init__(self)
260
+ ReadCoreMSHDF_MassSpectrum.__init__(self, file_location)
261
+
262
+ # override the scans attribute on ReadCoreMSHDF_MassSpectrum class to expect a nested location within the HDF5 file
263
+ self.scans = [
264
+ "mass_spectra/" + x for x in list(self.h5pydata["mass_spectra"].keys())
265
+ ]
266
+ self.scan_number_list = sorted(
267
+ [int(float(i)) for i in list(self.h5pydata["mass_spectra"].keys())]
268
+ )
269
+
270
+ # set the location of the parameters file (json or toml)
271
+ add_files = [
272
+ x
273
+ for x in self.file_location.parent.glob(
274
+ self.file_location.name.replace(".hdf5", ".*")
275
+ )
276
+ if x.suffix != ".hdf5"
277
+ ]
278
+ if len([x for x in add_files if x.suffix == ".json"]) > 0:
279
+ self.parameters_location = [x for x in add_files if x.suffix == ".json"][0]
280
+ elif len([x for x in add_files if x.suffix == ".toml"]) > 0:
281
+ self.parameters_location = [x for x in add_files if x.suffix == ".toml"][0]
282
+ else:
283
+ self.parameters_location = None
284
+
285
+ def __enter__(self):
286
+ """Context manager entry."""
287
+ return self
288
+
289
+ def __exit__(self, exc_type, exc_val, exc_tb):
290
+ """Context manager exit - closes the HDF5 file."""
291
+ if hasattr(self, 'h5pydata') and self.h5pydata is not None:
292
+ self.h5pydata.close()
293
+ return False
294
+
295
+ def close(self):
296
+ """Explicitly close the HDF5 file."""
297
+ if hasattr(self, 'h5pydata') and self.h5pydata is not None:
298
+ self.h5pydata.close()
299
+
300
+ def get_mass_spectrum_from_scan(self, scan_number):
301
+ """Return mass spectrum data object from scan number."""
302
+ if scan_number in self.scan_number_list:
303
+ mass_spec = self.get_mass_spectrum(scan_number)
304
+ return mass_spec
305
+ else:
306
+ raise Exception("Scan number not found in HDF5 file.")
307
+
308
+ def get_mass_spectra_from_scan_list(
309
+ self, scan_list, spectrum_mode, auto_process=True
310
+ ):
311
+ """Return a list of mass spectrum data objects from a list of scan numbers.
312
+
313
+ Parameters
314
+ ----------
315
+ scan_list : list
316
+ A list of scan numbers to retrieve mass spectra for.
317
+ spectrum_mode : str
318
+ The spectrum mode to use when retrieving the mass spectra.
319
+ Note that this parameter is not used for CoreMS HDF5 files, as the spectra are already processed and only
320
+ centroided spectra are saved.
321
+ auto_process : bool
322
+ If True, automatically process the mass spectra when retrieving them.
323
+ Note that this parameter is not used for CoreMS HDF5 files, as the spectra are already processed and only
324
+ centroided spectra are saved.
325
+
326
+ Returns
327
+ -------
328
+ list
329
+ A list of mass spectrum data objects corresponding to the provided scan numbers.
330
+ """
331
+ mass_spectra_list = []
332
+ for scan_number in scan_list:
333
+ if scan_number in self.scan_number_list:
334
+ mass_spec = self.get_mass_spectrum_from_scan(scan_number)
335
+ mass_spectra_list.append(mass_spec)
336
+ else:
337
+ warnings.warn(f"Scan number {scan_number} not found in HDF5 file.")
338
+ return mass_spectra_list
339
+
340
+ def load(self) -> None:
341
+ """ """
342
+ pass
343
+
344
+ def get_ms_raw(self, spectra=None, scan_df=None, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None) -> dict:
345
+ """ """
346
+ # Warn if spectra or scan_df are not None that they are not used for CoreMS HDF5 files and should be rerun after instantiation
347
+ if spectra is not None or scan_df is not None:
348
+ SyntaxWarning(
349
+ "get_ms_raw method for CoreMS HDF5 files can only access saved data, consider rerunning after instantiation."
350
+ )
351
+ ms_unprocessed = {}
352
+ dict_group_load = self.h5pydata["ms_unprocessed"]
353
+ dict_group_keys = dict_group_load.keys()
354
+ for k in dict_group_keys:
355
+ ms_up_int = dict_group_load[k][:]
356
+ ms_unprocessed[int(k)] = pd.DataFrame(
357
+ ms_up_int, columns=["scan", "mz", "intensity"]
358
+ )
359
+ return ms_unprocessed
360
+
361
+ def get_scan_df(self, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None) -> pd.DataFrame:
362
+ scan_info = {}
363
+ dict_group_load = self.h5pydata["scan_info"]
364
+ dict_group_keys = dict_group_load.keys()
365
+ for k in dict_group_keys:
366
+ scan_info[k] = dict_group_load[k][:]
367
+ scan_df = pd.DataFrame(scan_info)
368
+ scan_df.set_index("scan", inplace=True, drop=False)
369
+ str_df = scan_df.select_dtypes([object])
370
+ str_df = str_df.stack().str.decode("utf-8").unstack()
371
+ for col in str_df:
372
+ scan_df[col] = str_df[col]
373
+
374
+ # Apply time range filtering if specified
375
+ if time_range is not None:
376
+ time_ranges = self._normalize_time_range(time_range)
377
+ mask = pd.Series([False] * len(scan_df), index=scan_df.index)
378
+ for start_time, end_time in time_ranges:
379
+ mask |= (scan_df.scan_time >= start_time) & (scan_df.scan_time <= end_time)
380
+ scan_df = scan_df[mask]
381
+
382
+ return scan_df
383
+
384
+ def run(self, mass_spectra, load_raw=True, load_light=False) -> None:
385
+ """Runs the importer functions to populate a LCMS or MassSpectraBase object.
386
+
387
+ Notes
388
+ -----
389
+ The following functions are run in order, if the HDF5 file contains the necessary data:
390
+ 1. import_parameters(), which populates the parameters attribute on the LCMS or MassSpectraBase object.
391
+ 2. import_mass_spectra(), which populates the _ms list on the LCMS or MassSpectraBase object.
392
+ 3. import_scan_info(), which populates the _scan_info on the LCMS or MassSpectraBase object.
393
+ 4. import_ms_unprocessed(), which populates the _ms_unprocessed attribute on the LCMS or MassSpectraBase object.
394
+ 5. import_mass_features(), which populates the mass_features attribute on the LCMS or MassSpectraBase object.
395
+ 6. import_eics(), which populates the eics attribute on the LCMS or MassSpectraBase object.
396
+ 7. import_spectral_search_results(), which populates the spectral_search_results attribute on the LCMS or MassSpectraBase object.
397
+
398
+ Parameters
399
+ ----------
400
+ mass_spectra : LCMSBase or MassSpectraBase
401
+ The LCMS or MassSpectraBase object to populate with mass spectra, generally instantiated with only the file_location, analyzer, and instrument_label attributes.
402
+ load_raw : bool
403
+ If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default is True.
404
+ load_light : bool
405
+ If True, only load the parameters, mass features, and scan info. Default is False.
406
+
407
+ Returns
408
+ -------
409
+ None, but populates several attributes on the LCMS or MassSpectraBase object.
410
+
411
+ """
412
+ if self.parameters_location is not None:
413
+ # Populate the parameters attribute on the LCMS object
414
+ self.import_parameters(mass_spectra)
415
+
416
+ if "mass_spectra" in self.h5pydata and not load_light:
417
+ # Populate the _ms list on the LCMS object
418
+ self.import_mass_spectra(mass_spectra, load_raw=load_raw)
419
+
420
+ if "scan_info" in self.h5pydata:
421
+ # Populate the _scan_info attribute on the LCMS object
422
+ self.import_scan_info(mass_spectra)
423
+
424
+ if "ms_unprocessed" in self.h5pydata and load_raw and not load_light:
425
+ # Populate the _ms_unprocessed attribute on the LCMS object
426
+ self.import_ms_unprocessed(mass_spectra)
427
+
428
+ if "mass_features" in self.h5pydata:
429
+ # Populate the mass_features attribute on the LCMS object
430
+ self.import_mass_features(mass_spectra)
431
+
432
+ if "eics" in self.h5pydata and not load_light:
433
+ # Populate the eics attribute on the LCMS object
434
+ self.import_eics(mass_spectra)
435
+
436
+ if "spectral_search_results" in self.h5pydata and not load_light:
437
+ # Populate the spectral_search_results attribute on the LCMS object
438
+ self.import_spectral_search_results(mass_spectra)
439
+
440
+ def import_mass_spectra(self, mass_spectra, load_raw=True) -> None:
441
+ """Imports all mass spectra from the HDF5 file.
442
+
443
+ Parameters
444
+ ----------
445
+ mass_spectra : LCMSBase | MassSpectraBase
446
+ The MassSpectraBase or LCMSBase object to populate with mass spectra.
447
+ load_raw : bool
448
+ If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default
449
+
450
+ Returns
451
+ -------
452
+ None, but populates the '_ms' list on the LCMSBase or MassSpectraBase
453
+ object with mass spectra from the HDF5 file.
454
+ """
455
+ for scan_number in self.scan_number_list:
456
+ mass_spec = self.get_mass_spectrum(scan_number, load_raw=load_raw)
457
+ mass_spec.scan_number = scan_number
458
+ mass_spectra.add_mass_spectrum(mass_spec)
459
+
460
+ def import_scan_info(self, mass_spectra) -> None:
461
+ """Imports the scan info from the HDF5 file.
462
+
463
+ Parameters
464
+ ----------
465
+ lcms : LCMSBase | MassSpectraBase
466
+ The MassSpectraBase or LCMSBase objects
467
+
468
+ Returns
469
+ -------
470
+ None, but populates the 'scan_df' attribute on the LCMSBase or MassSpectraBase
471
+ object with a pandas DataFrame of the 'scan_info' from the HDF5 file.
472
+
473
+ """
474
+ scan_df = self.get_scan_df()
475
+ mass_spectra.scan_df = scan_df
476
+
477
+ def import_ms_unprocessed(self, mass_spectra) -> None:
478
+ """Imports the unprocessed mass spectra from the HDF5 file.
479
+
480
+ Parameters
481
+ ----------
482
+ lcms : LCMSBase | MassSpectraBase
483
+ The MassSpectraBase or LCMSBase objects
484
+
485
+ Returns
486
+ -------
487
+ None, but populates the '_ms_unprocessed' attribute on the LCMSBase or MassSpectraBase
488
+ object with a dictionary of the 'ms_unprocessed' from the HDF5 file.
489
+
490
+ """
491
+ ms_unprocessed = self.get_ms_raw()
492
+ mass_spectra._ms_unprocessed = ms_unprocessed
493
+
494
+ def import_parameters(self, mass_spectra) -> None:
495
+ """Imports the parameters from the HDF5 file.
496
+
497
+ Parameters
498
+ ----------
499
+ mass_spectra : LCMSBase | MassSpectraBase
500
+ The MassSpectraBase or LCMSBase object to populate with parameters.
501
+
502
+ Returns
503
+ -------
504
+ None, but populates the 'parameters' attribute on the LCMS or MassSpectraBase
505
+ object with a dictionary of the 'parameters' from the HDF5 file.
506
+
507
+ """
508
+ if ".json" == self.parameters_location.suffix:
509
+ load_and_set_json_parameters_lcms(mass_spectra, self.parameters_location)
510
+ if ".toml" == self.parameters_location.suffix:
511
+ load_and_set_toml_parameters_lcms(mass_spectra, self.parameters_location)
512
+ else:
513
+ raise Exception(
514
+ "Parameters file must be in JSON format, TOML format is not yet supported."
515
+ )
516
+
517
+ def import_mass_features(self, mass_spectra, mf_ids=None) -> None:
518
+ """Imports the mass features from the HDF5 file.
519
+
520
+ Parameters
521
+ ----------
522
+ mass_spectra : LCMSBase | MassSpectraBase
523
+ The MassSpectraBase or LCMSBase object to populate with mass features.
524
+ mf_ids : list, optional
525
+ A list of mass feature IDs to import. If None, all mass features are imported.
526
+
527
+ Returns
528
+ -------
529
+ None, but populates the 'mass_features' attribute on the LCMSBase or MassSpectraBase
530
+ object with a dictionary of the 'mass_features' from the HDF5 file.
531
+
532
+ """
533
+ dict_group_load = self.h5pydata["mass_features"]
534
+ dict_group_keys = dict_group_load.keys()
535
+ for k in dict_group_keys:
536
+ if mf_ids is not None and int(k) not in mf_ids:
537
+ continue
538
+ # Instantiate the MassFeature object
539
+ mass_feature = LCMSMassFeature(
540
+ mass_spectra,
541
+ mz=dict_group_load[k].attrs["_mz_exp"],
542
+ retention_time=dict_group_load[k].attrs["_retention_time"],
543
+ intensity=dict_group_load[k].attrs["_intensity"],
544
+ apex_scan=dict_group_load[k].attrs["_apex_scan"],
545
+ persistence=dict_group_load[k].attrs["_persistence"],
546
+ id=int(k),
547
+ )
548
+
549
+ # Populate additional attributes on the MassFeature object
550
+ for key in dict_group_load[k].attrs.keys() - {
551
+ "_mz_exp",
552
+ "_mz_cal",
553
+ "_retention_time",
554
+ "_intensity",
555
+ "_apex_scan",
556
+ "_persistence",
557
+ }:
558
+ setattr(mass_feature, key, dict_group_load[k].attrs[key])
559
+
560
+ # Populate attributes on MassFeature object that are lists
561
+ for key in dict_group_load[k].keys():
562
+ setattr(mass_feature, key, dict_group_load[k][key][:])
563
+ # Convert _noise_score from array to tuple
564
+ if key == "_noise_score":
565
+ mass_feature._noise_score = tuple(mass_feature._noise_score)
566
+ mass_spectra.mass_features[int(k)] = mass_feature
567
+
568
+ # Associate mass features with ms1 and ms2 spectra, if available
569
+ for mf_id in mass_spectra.mass_features.keys():
570
+ if mass_spectra.mass_features[mf_id].apex_scan in mass_spectra._ms.keys():
571
+ mass_spectra.mass_features[mf_id].mass_spectrum = mass_spectra._ms[
572
+ mass_spectra.mass_features[mf_id].apex_scan
573
+ ]
574
+ if mass_spectra.mass_features[mf_id].ms2_scan_numbers is not None:
575
+ for ms2_scan in mass_spectra.mass_features[mf_id].ms2_scan_numbers:
576
+ if ms2_scan in mass_spectra._ms.keys():
577
+ mass_spectra.mass_features[mf_id].ms2_mass_spectra[ms2_scan] = (
578
+ mass_spectra._ms[ms2_scan]
579
+ )
580
+
581
+ def import_eics(self, mass_spectra, mz_list=None, mz_tolerance=0.0001):
582
+ """Imports the extracted ion chromatograms from the HDF5 file.
583
+
584
+ Parameters
585
+ ----------
586
+ mass_spectra : LCMSBase | MassSpectraBase
587
+ The MassSpectraBase or LCMSBase object to populate with extracted ion chromatograms.
588
+ mz_list : list of float, optional
589
+ List of m/z values to load EICs for. If None, loads all EICs. Default is None.
590
+ mz_tolerance : float, optional
591
+ Tolerance in Daltons for matching m/z values when mz_list is provided.
592
+ Default is 0.0001 Da.
593
+
594
+ Returns
595
+ -------
596
+ None, but populates the 'eics' attribute on the LCMSBase or MassSpectraBase
597
+ object with a dictionary of the 'eics' from the HDF5 file.
598
+
599
+ """
600
+ dict_group_load = self.h5pydata["eics"]
601
+ dict_group_keys = dict_group_load.keys()
602
+
603
+ # Prefilter dict_group_keys if mz_list is provided to EICs within tolerance
604
+ if mz_list is not None:
605
+ target_mz_array = np.array(sorted(mz_list))
606
+ mzs = [float(k) for k in dict_group_keys if np.abs(float(k)-target_mz_array).min() < mz_tolerance]
607
+ dict_group_keys = [str(mz) for mz in mzs]
608
+
609
+ for k in dict_group_keys:
610
+ # Check if we should load this EIC (filter by m/z if list provided)
611
+ eic_mz = dict_group_load[k].attrs["mz"]
612
+
613
+ my_eic = EIC_Data(
614
+ scans=dict_group_load[k]["scans"][:],
615
+ time=dict_group_load[k]["time"][:],
616
+ eic=dict_group_load[k]["eic"][:],
617
+ )
618
+ for key in dict_group_load[k].keys():
619
+ if key not in ["scans", "time", "eic"]:
620
+ setattr(my_eic, key, dict_group_load[k][key][:])
621
+ # if key is apexes, convert to a tuple of a list
622
+ if key == "apexes" and len(my_eic.apexes) > 0:
623
+ my_eic.apexes = [tuple(x) for x in my_eic.apexes]
624
+ # Add to mass_spectra object
625
+ mass_spectra.eics[eic_mz] = my_eic
626
+
627
+ # Associate EICs with mass features using tolerance-based matching
628
+ mass_spectra.associate_eics_with_mass_features()
629
+
630
+ @staticmethod
631
+ def _load_eics_from_hdf5_group(eics_group, lcms_obj, mz_filter=None):
632
+ """Load EICs from an HDF5 group.
633
+
634
+ This is a static helper method that can be reused to load EIC data
635
+ from any HDF5 group in a consistent format.
636
+
637
+ Parameters
638
+ ----------
639
+ eics_group : h5py.Group
640
+ The HDF5 group containing EIC data.
641
+ lcms_obj : LCMSBase
642
+ The LCMS object to associate EICs with (for reference, not modified).
643
+ mz_filter : list, optional
644
+ List of m/z values to load. If None, loads all EICs. Default is None.
645
+ Uses tolerance-based matching (0.0001).
646
+
647
+ Returns
648
+ -------
649
+ dict
650
+ Dictionary of EIC_Data objects keyed by m/z value.
651
+ """
652
+ from corems.mass_spectra.factory.chromat_data import EIC_Data
653
+
654
+ loaded_eics = {}
655
+ tolerance = 0.0001
656
+
657
+ for eic_key_str in eics_group.keys():
658
+ eic_mz = float(eic_key_str) if eic_key_str.replace('.', '', 1).replace('-', '', 1).isdigit() else eics_group[eic_key_str].attrs.get("mz")
659
+
660
+ # If mz_filter is provided, check if this EIC matches any requested m/z
661
+ if mz_filter is not None:
662
+ if not any(abs(eic_mz - mz) < tolerance for mz in mz_filter):
663
+ continue
664
+
665
+ eic_data = eics_group[eic_key_str]
666
+
667
+ # Create EIC_Data object from datasets
668
+ eic = EIC_Data(
669
+ scans=list(eic_data["scans"][:]) if "scans" in eic_data else [],
670
+ time=list(eic_data["time"][:]) if "time" in eic_data else [],
671
+ eic=list(eic_data["eic"][:]) if "eic" in eic_data else [],
672
+ apexes=list(eic_data["apexes"][:]) if "apexes" in eic_data else [],
673
+ )
674
+
675
+ # Load any additional datasets
676
+ for key in eic_data.keys():
677
+ if key not in ["scans", "time", "eic", "apexes"]:
678
+ setattr(eic, key, eic_data[key][:])
679
+
680
+ loaded_eics[eic_mz] = eic
681
+
682
+ return loaded_eics
683
+
684
+ def import_spectral_search_results(self, mass_spectra):
685
+ """Imports the spectral search results from the HDF5 file.
686
+
687
+ Parameters
688
+ ----------
689
+ mass_spectra : LCMSBase | MassSpectraBase
690
+ The MassSpectraBase or LCMSBase object to populate with spectral search results.
691
+
692
+ Returns
693
+ -------
694
+ None, but populates the 'spectral_search_results' attribute on the LCMSBase or MassSpectraBase
695
+ object with a dictionary of the 'spectral_search_results' from the HDF5 file.
696
+
697
+ """
698
+ overall_results_dict = {}
699
+ ms2_results_load = self.h5pydata["spectral_search_results"]
700
+ for k in ms2_results_load.keys():
701
+ overall_results_dict[int(k)] = {}
702
+ for k2 in ms2_results_load[k].keys():
703
+ ms2_search_res = SpectrumSearchResults(
704
+ query_spectrum=mass_spectra._ms[int(k)],
705
+ precursor_mz=ms2_results_load[k][k2].attrs["precursor_mz"],
706
+ spectral_similarity_search_results={},
707
+ )
708
+
709
+ for key in ms2_results_load[k][k2].keys() - {"precursor_mz"}:
710
+ data = list(ms2_results_load[k][k2][key][:])
711
+ if data and isinstance(data[0], bytes):
712
+ data = [x.decode("utf-8") for x in data]
713
+ setattr(ms2_search_res, key, data)
714
+
715
+ overall_results_dict[int(k)][
716
+ ms2_results_load[k][k2].attrs["precursor_mz"]
717
+ ] = ms2_search_res
718
+
719
+ # add to mass_spectra
720
+ mass_spectra.spectral_search_results.update(overall_results_dict)
721
+
722
+ # If there are mass features, associate the results with each mass feature
723
+ if len(mass_spectra.mass_features) > 0:
724
+ for mass_feature_id, mass_feature in mass_spectra.mass_features.items():
725
+ scan_ids = mass_feature.ms2_scan_numbers
726
+ for ms2_scan_id in scan_ids:
727
+ precursor_mz = mass_feature.mz
728
+ try:
729
+ mass_spectra.spectral_search_results[ms2_scan_id][precursor_mz]
730
+ except KeyError:
731
+ pass
732
+ else:
733
+ mass_spectra.mass_features[
734
+ mass_feature_id
735
+ ].ms2_similarity_results.append(
736
+ mass_spectra.spectral_search_results[ms2_scan_id][
737
+ precursor_mz
738
+ ]
739
+ )
740
+
741
+ def get_mass_spectra_obj(self, load_raw=True, load_light=False, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None) -> MassSpectraBase:
742
+ """
743
+ Return mass spectra data object, populating the _ms list on MassSpectraBase object from the HDF5 file.
744
+
745
+ Parameters
746
+ ----------
747
+ load_raw : bool
748
+ If True, load raw data (unprocessed) from HDF5 files for overall spectra object and individual mass spectra. Default is True.
749
+ load_light : bool
750
+ If True, only load the parameters, mass features, and scan info. Default is False.
751
+ time_range : tuple or list of tuples, optional
752
+ Retention time range(s) to load. Can be:
753
+ - Single range: (start_time, end_time) in minutes
754
+ - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes
755
+ If None, loads all scans. Note: For HDF5 files, this parameter is accepted for
756
+ interface consistency but not currently used in filtering.
757
+
758
+ """
759
+ # Instantiate the LCMS object
760
+ spectra_obj = MassSpectraBase(
761
+ file_location=self.file_location,
762
+ analyzer=self.analyzer,
763
+ instrument_label=self.instrument_label,
764
+ sample_name=self.sample_name,
765
+ )
766
+
767
+ # This will populate the _ms list on the LCMS or MassSpectraBase object
768
+ self.run(spectra_obj, load_raw=load_raw, load_light=load_light)
769
+
770
+ return spectra_obj
771
+
772
+ def get_lcms_obj(
773
+ self, load_raw=True, load_light=False, use_original_parser=True, raw_file_path=None, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None
774
+ ) -> LCMSBase:
775
+ """
776
+ Return LCMSBase object, populating attributes on the LCMSBase object from the HDF5 file.
777
+
778
+ Parameters
779
+ ----------
780
+ load_raw : bool
781
+ If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default is True.
782
+ load_light : bool
783
+ If True, only load the parameters, mass features, and scan info. Default is False.
784
+ use_original_parser : bool
785
+ If True, use the original parser to populate the LCMS object. Default is True.
786
+ raw_file_path : str
787
+ The location of the raw file to parse if attempting to use original parser.
788
+ Default is None, which attempts to get the raw file path from the HDF5 file.
789
+ If the original file path has moved, this parameter can be used to specify the new location.
790
+ time_range : tuple or list of tuples, optional
791
+ Retention time range(s) to load. Can be:
792
+ - Single range: (start_time, end_time) in minutes
793
+ - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes
794
+ If None, loads all scans. Note: For HDF5 files, this parameter is accepted for
795
+ interface consistency. If use_original_parser=True, time_range can be passed to the
796
+ original parser for filtering.
797
+ """
798
+ # Instantiate the LCMS object
799
+ lcms_obj = LCMSBase(
800
+ file_location=self.file_location,
801
+ analyzer=self.analyzer,
802
+ instrument_label=self.instrument_label,
803
+ sample_name=self.sample_name,
804
+ )
805
+
806
+ # This will populate the majority of the attributes on the LCMS object
807
+ self.run(lcms_obj, load_raw=load_raw, load_light=load_light)
808
+
809
+ # Set final attributes of the LCMS object
810
+ lcms_obj.polarity = self.h5pydata.attrs["polarity"]
811
+ lcms_obj._scans_number_list = list(lcms_obj.scan_df.scan)
812
+ lcms_obj._retention_time_list = list(lcms_obj.scan_df.scan_time)
813
+ lcms_obj._tic_list = list(lcms_obj.scan_df.tic)
814
+
815
+ # If use_original_parser is True, instantiate the original parser and populate the LCMS object
816
+ if use_original_parser:
817
+ lcms_obj = self.add_original_parser(lcms_obj, raw_file_path=raw_file_path)
818
+ else:
819
+ lcms_obj.spectra_parser_class = self.__class__
820
+
821
+ return lcms_obj
822
+
823
+ def get_raw_file_location(self):
824
+ """
825
+ Get the raw file location from the HDF5 file attributes.
826
+
827
+ Returns
828
+ -------
829
+ str
830
+ The raw file location.
831
+ """
832
+ if "original_file_location" in self.h5pydata.attrs:
833
+ return self.h5pydata.attrs["original_file_location"]
834
+ else:
835
+ return None
836
+
837
+ def add_original_parser(self, mass_spectra, raw_file_path=None):
838
+ """
839
+ Add the original parser to the mass spectra object.
840
+
841
+ Parameters
842
+ ----------
843
+ mass_spectra : MassSpectraBase | LCMSBase
844
+ The MassSpectraBase or LCMSBase object to add the original parser to.
845
+ raw_file_path : str
846
+ The location of the raw file to parse. Default is None, which attempts to get the raw file path from the HDF5 file.
847
+ """
848
+ # Get the original parser type
849
+ og_parser_type = self.h5pydata.attrs["parser_type"]
850
+
851
+ # If raw_file_path is None, get it from the HDF5 file attributes
852
+ if raw_file_path is None:
853
+ raw_file_path = self.get_raw_file_location()
854
+ if raw_file_path is None:
855
+ raise ValueError(
856
+ "Raw file path not found in HDF5 file attributes, cannot instantiate original parser."
857
+ )
858
+
859
+ # Set the raw file path on the mass_spectra object so the parser knows where to find the raw file
860
+ mass_spectra.raw_file_location = raw_file_path
861
+
862
+ if og_parser_type == "ImportMassSpectraThermoMSFileReader":
863
+ # Check that the parser can be instantiated with the raw file path
864
+ parser_class = ImportMassSpectraThermoMSFileReader
865
+ elif og_parser_type == "MZMLSpectraParser":
866
+ # Check that the parser can be instantiated with the raw file path
867
+ parser_class = MZMLSpectraParser
868
+
869
+ # Set the spectra parser class on the mass_spectra object so the spectra_parser property can be used with the original parser
870
+ mass_spectra.spectra_parser_class = parser_class
871
+
872
+ return mass_spectra
873
+
874
+ def get_original_creation_time(self):
875
+ """
876
+ Get the creation time of the original raw data file.
877
+
878
+ First checks if creation_time is saved in the HDF5 file attributes.
879
+ If not found, attempts to instantiate the original parser and get the creation time.
880
+
881
+ Returns
882
+ -------
883
+ datetime
884
+ The creation time of the original raw data file, or None if not available.
885
+ """
886
+ # Check if creation_time is saved in HDF5 attributes
887
+ if "creation_time" in self.h5pydata.attrs:
888
+ from datetime import datetime
889
+ return datetime.fromisoformat(self.h5pydata.attrs["creation_time"])
890
+
891
+ # Fall back to using original parser to get creation time
892
+ try:
893
+ # Get the original parser type and raw file path
894
+ og_parser_type = self.h5pydata.attrs.get("parser_type")
895
+ raw_file_path = self.get_raw_file_location()
896
+
897
+ if og_parser_type is None or raw_file_path is None:
898
+ warnings.warn(
899
+ "Cannot retrieve creation time: parser_type or original_file_location not found in HDF5 attributes."
900
+ )
901
+ return None
902
+
903
+ # Check if raw file exists
904
+ from pathlib import Path
905
+ if not Path(raw_file_path).exists():
906
+ warnings.warn(
907
+ f"Cannot retrieve creation time: original raw file not found at {raw_file_path}"
908
+ )
909
+ return None
910
+
911
+ # Instantiate the original parser
912
+ if og_parser_type == "ImportMassSpectraThermoMSFileReader":
913
+ parser = ImportMassSpectraThermoMSFileReader(raw_file_path)
914
+ elif og_parser_type == "MZMLSpectraParser":
915
+ parser = MZMLSpectraParser(raw_file_path)
916
+ else:
917
+ warnings.warn(
918
+ f"Unknown parser type: {og_parser_type}, cannot retrieve creation time."
919
+ )
920
+ return None
921
+
922
+ # Get creation time from parser
923
+ return parser.get_creation_time()
924
+
925
+ except Exception as e:
926
+ warnings.warn(
927
+ f"Failed to retrieve creation time from original parser: {e}"
928
+ )
929
+ return None
930
+
931
+ def get_creation_time(self):
932
+ """
933
+ Get the creation time of the original raw data file.
934
+
935
+ This is an alias for get_original_creation_time() for backward compatibility.
936
+
937
+ Returns
938
+ -------
939
+ datetime
940
+ The creation time of the original raw data file, or None if not available.
941
+ """
942
+ return self.get_original_creation_time()
943
+
944
+ def get_instrument_info(self):
945
+ """
946
+ Raise a NotImplemented Warning, as instrument info is not available in CoreMS HDF5 files and returning None.
947
+ """
948
+ warnings.warn(
949
+ "Instrument info is not available in CoreMS HDF5 files, returning None."
950
+ "This should be accessed through the original parser.",
951
+ )
952
+ return None
953
+
954
+ def get_scans_in_time_range(
955
+ self,
956
+ time_range: Union[Tuple[float, float], List[Tuple[float, float]]],
957
+ ms_level: Optional[int] = None
958
+ ) -> List[int]:
959
+ """Return scan numbers within specified retention time range(s).
960
+
961
+ Parameters
962
+ ----------
963
+ time_range : tuple or list of tuples
964
+ Retention time range(s) in minutes. Can be:
965
+ - Single range: (start_time, end_time)
966
+ - Multiple ranges: [(start1, end1), (start2, end2), ...]
967
+ ms_level : int, optional
968
+ If specified, only return scans of this MS level (e.g., 1 for MS1, 2 for MS2).
969
+ If None, returns scans of all MS levels.
970
+
971
+ Returns
972
+ -------
973
+ list of int
974
+ List of scan numbers within the specified time range(s) and MS level.
975
+ """
976
+ # Normalize time range to list of tuples
977
+ time_ranges = self._normalize_time_range(time_range)
978
+
979
+ # Get all scan data
980
+ scan_df = self.get_scan_df()
981
+
982
+ # Filter by time range
983
+ mask = pd.Series([False] * len(scan_df), index=scan_df.index)
984
+ for start_time, end_time in time_ranges:
985
+ mask |= (scan_df.scan_time >= start_time) & (scan_df.scan_time <= end_time)
986
+
987
+ filtered_df = scan_df[mask]
988
+
989
+ # Filter by MS level if specified
990
+ if ms_level is not None:
991
+ filtered_df = filtered_df[filtered_df.ms_level == ms_level]
992
+
993
+ return filtered_df.scan.tolist()
994
+
995
+
996
+ class ReadCoreMSHDFMassSpectraCollection:
997
+ """Read a collection of CoreMS HDF5 files and populate an LCMSCollection object.
998
+
999
+ Parameters
1000
+ ----------
1001
+ folder_location : Path
1002
+ Folder containing .corems subdirectories with HDF5 files.
1003
+ manifest_file : Path, optional
1004
+ Manifest CSV with columns: sample_name, order, batch, center, time.
1005
+ One sample must have center='TRUE' for RT alignment.
1006
+ If None, checks if auto-generated manifest_auto.csv exists in the folder. If not,
1007
+ auto-generates from folder contents. Default: None.
1008
+ cores : int, optional
1009
+ Number of cores for multiprocessing. Default: 1.
1010
+ auto_manifest_batch_threshold_hours : float, optional
1011
+ Time gap (hours) for auto-generated batch separation. Default: 12.0.
1012
+ auto_manifest_center_name : str, optional
1013
+ Sample name for RT alignment center when auto-generating.
1014
+ Must match a discovered sample. If None, uses middle sample. Default: None.
1015
+
1016
+ Attributes
1017
+ ----------
1018
+ folder_location : Path
1019
+ Folder containing CoreMS HDF5 files.
1020
+ manifest_filepath : Path
1021
+ Path to manifest file.
1022
+ manifest : dict
1023
+ Manifest data indexed by sample_name.
1024
+ """
1025
+ def __init__(
1026
+ self,
1027
+ folder_location: Path,
1028
+ manifest_file: Path = None,
1029
+ cores: int = 1,
1030
+ auto_manifest_batch_threshold_hours: float = 12.0,
1031
+ auto_manifest_center_name: str = None
1032
+ ):
1033
+ # Check for folder location
1034
+ folder_location = Path(folder_location)
1035
+ if not folder_location.exists():
1036
+ raise FileNotFoundError(f"Folder location {folder_location} not found.")
1037
+
1038
+ # Auto-generate manifest if not provided
1039
+ if manifest_file is None:
1040
+ # Check if manifest_auto.csv already exists
1041
+ auto_manifest_path = folder_location / "manifest_auto.csv"
1042
+ if auto_manifest_path.exists():
1043
+ print(f"No manifest file provided. Using existing manifest_auto.csv from {folder_location}")
1044
+ manifest_file = auto_manifest_path
1045
+ else:
1046
+ print(f"No manifest file provided. Auto-generating manifest from {folder_location}")
1047
+ manifest_file = create_manifest_from_folder(
1048
+ folder_path=folder_location,
1049
+ output_path=auto_manifest_path,
1050
+ batch_time_threshold_hours=auto_manifest_batch_threshold_hours,
1051
+ center_name=auto_manifest_center_name,
1052
+ overwrite=True
1053
+ )
1054
+ else:
1055
+ manifest_file = Path(manifest_file)
1056
+ if not manifest_file.exists():
1057
+ raise FileNotFoundError(f"Manifest file {manifest_file} not found.")
1058
+
1059
+ # Check if the manifest file is a CSV
1060
+ if manifest_file.suffix != ".csv":
1061
+ raise ValueError("Manifest file must be a CSV.")
1062
+
1063
+ self.folder_location = folder_location
1064
+ self._manifest_dict = None
1065
+ self._parse_manifest(manifest_file)
1066
+ self._validate_manifest()
1067
+ self._validate_parameters()
1068
+ self._validate_cores(cores)
1069
+
1070
+ def _validate_cores(self, cores):
1071
+ # Check if the cores parameter is an integer greater than 0 and less than the number of cores available
1072
+ if not isinstance(cores, int) or cores < 1:
1073
+ raise ValueError("Cores must be an integer greater than 0.")
1074
+ if cores > multiprocessing.cpu_count():
1075
+ raise ValueError(
1076
+ f"Cores must be less than or equal to the number of cores available ({multiprocessing.cpu_count()})."
1077
+ )
1078
+ self._cores = cores
1079
+
1080
+ def _parse_manifest(self, manifest_file):
1081
+ """Parse the manifest file and set the manifest dictionary."""
1082
+ self.manifest_filepath = manifest_file
1083
+ manifest = pd.read_csv(manifest_file)
1084
+ # Check if the following columns exisit in the manifest file
1085
+ if not all(
1086
+ col in manifest.columns for col in ["sample_name", "order", "batch"]
1087
+ ):
1088
+ raise ValueError(
1089
+ "Manifest file must contain the following columns: 'sample_name', 'order', 'batch'."
1090
+ )
1091
+ # Set index to the 'sample_name' column
1092
+ manifest.set_index("sample_name", inplace=True)
1093
+ self._manifest_dict = manifest.to_dict(orient="index")
1094
+
1095
+ def _validate_manifest(self):
1096
+ """Validate the manifest dictionary against the CoreMS folder location."""
1097
+ # Check if the folder location contains HDF5 files for each sample
1098
+ for sample_name in self._manifest_dict.keys():
1099
+ corems_dir = self.folder_location / f"{sample_name}.corems"
1100
+ if not corems_dir.exists():
1101
+ raise FileNotFoundError(f"CoreMS folder for {sample_name} not found.")
1102
+ hdf5_file = corems_dir / f"{sample_name}.hdf5"
1103
+ if not hdf5_file.exists():
1104
+ raise FileNotFoundError(f"HDF5 file for {sample_name} not found.")
1105
+
1106
+ # Check that at least one sample has center='TRUE' for retention time alignment
1107
+ center_values = [sample_data.get('center') for sample_data in self._manifest_dict.values()]
1108
+ if not any(center_val == 'TRUE' or center_val == True for center_val in center_values):
1109
+ raise ValueError(
1110
+ "Manifest must contain at least one sample with center='TRUE' for retention time alignment. "
1111
+ "None of the samples in the manifest have center='TRUE'."
1112
+ )
1113
+
1114
+ def _validate_parameters(self):
1115
+ """Validate that the parameters used for all samples within a batch are the same."""
1116
+ # Check if parameters files are saved as JSON or TOML
1117
+ if self.parameters_files[0].suffix == ".json":
1118
+ importer = json
1119
+ suffix = ".json"
1120
+
1121
+ elif self.parameters_files[0].suffix == ".toml":
1122
+ importer = toml
1123
+ suffix = ".toml"
1124
+
1125
+ manfiest_df = self.manifest_dataframe
1126
+
1127
+ # Split up samples by batch
1128
+ batches = manfiest_df["batch"].unique()
1129
+
1130
+ for batch in batches:
1131
+ samples = manfiest_df[manfiest_df["batch"] == batch].index
1132
+ # check if self.parameters_files end with .json or .toml
1133
+ batch_param_files = [
1134
+ self.folder_location / f"{sample_name}.corems/{sample_name}{suffix}"
1135
+ for sample_name in self._manifest_dict.keys()
1136
+ if sample_name in samples
1137
+ ]
1138
+ with open(
1139
+ batch_param_files[0],
1140
+ "r",
1141
+ encoding="utf8",
1142
+ ) as stream:
1143
+ first_parameters = importer.load(stream)
1144
+ for parameters_file in batch_param_files[1:]:
1145
+ with open(
1146
+ parameters_file,
1147
+ "r",
1148
+ encoding="utf8",
1149
+ ) as stream:
1150
+ parameters = importer.load(stream)
1151
+ if parameters != first_parameters:
1152
+ raise ValueError(
1153
+ f"Parameters files for samples in batch {batch} are not equal."
1154
+ )
1155
+
1156
+ def get_lcms_obj(self, sample_name: str, load_raw=False, load_light=True, use_original_parser=True, raw_file_path=None) -> LCMSBase:
1157
+ """Return a LCMSBase object for a given sample name within the collection.
1158
+
1159
+ Parameters
1160
+ ----------
1161
+ sample_name : str
1162
+ The sample name to retrieve the LCMS object for.
1163
+ load_raw : bool
1164
+ If True, load raw data from HDF5 files. Default is False.
1165
+ load_light : bool
1166
+ If True, only load the parameters, mass features, and scan info are initially loaded for each lcms object. Default is True.
1167
+ """
1168
+ hdf5_file = self.folder_location / f"{sample_name}.corems/{sample_name}.hdf5"
1169
+ with ReadCoreMSHDFMassSpectra(hdf5_file) as parser:
1170
+ lcms_obj = parser.get_lcms_obj(load_raw=load_raw, load_light=load_light, use_original_parser=use_original_parser, raw_file_path=raw_file_path)
1171
+ if load_light:
1172
+ mf_df = lcms_obj.mass_features_to_df()
1173
+ # Add ._eic_mz to mf_df for each mass_feature
1174
+ eic_mz_list = []
1175
+ for mf_id, mf in lcms_obj.mass_features.items():
1176
+ if hasattr(mf, "_eic_mz"):
1177
+ eic_mz_list.append(mf._eic_mz)
1178
+ else:
1179
+ eic_mz_list.append(None)
1180
+ mf_df["_eic_mz"] = eic_mz_list
1181
+ lcms_obj.mass_features = {}
1182
+ lcms_obj.light_mf_df = mf_df
1183
+ return lcms_obj
1184
+
1185
+ def get_lcms_collection(self, load_raw = False, load_light = True, use_original_parser = True) -> LCMSCollection:
1186
+ """Return a LCMSCollection object
1187
+
1188
+ Parameters
1189
+ ----------
1190
+ load_raw : bool
1191
+ If True, load raw data from HDF5 files. Default is False.
1192
+ load_light : bool
1193
+ If True, only load the parameters, mass features, and scan info are initially loaded for each lcms object.
1194
+ After concatenating the mass_features, remove the mass_features attribute from the individual LCMS objects for memory efficiency. Default is True.
1195
+ Default is True.
1196
+ """
1197
+ # Instantiate the LCMSCollection object
1198
+ lcms_coll = LCMSCollection(
1199
+ collection_location=self.folder_location,
1200
+ manifest=self.manifest,
1201
+ collection_parser=self
1202
+ )
1203
+
1204
+ # Set the number of cores on the LCMSCollection object from the ReadCoreMSHDFMassSpectraCollection object
1205
+ lcms_coll.parameters.lcms_collection.cores = self._cores
1206
+
1207
+ # Add LCMS objects to the collection
1208
+ samples = self._manifest_dict.keys()
1209
+
1210
+ # Initialize the LCMS object dictionary
1211
+ if self._cores > 1:
1212
+ if self._cores > len(samples):
1213
+ ncores = len(samples)
1214
+ else:
1215
+ ncores = self._cores
1216
+ # Create a pool of workers (one for each core or sample, whichever is smaller)
1217
+ pool = multiprocessing.Pool(ncores)
1218
+ args = [(sample, load_raw, load_light, use_original_parser) for sample in samples]
1219
+ lcms_objs = pool.starmap(self.get_lcms_obj, args)
1220
+ for sample_name, lcms_obj in zip(samples, lcms_objs):
1221
+ lcms_coll._lcms[sample_name] = lcms_obj
1222
+
1223
+ elif self._cores == 1:
1224
+ # Load the LCMS objects sequentially
1225
+ for sample_name in samples:
1226
+ lcms_coll._lcms[sample_name] = self.get_lcms_obj(sample_name, load_raw=load_raw, load_light=load_light, use_original_parser=use_original_parser)
1227
+
1228
+ else:
1229
+ raise ValueError("Number of cores must be greater than 0 and set on the ReadCoreMSHDFMassSpectraCollection object.")
1230
+
1231
+ # Check that all LCMS objects have the same polarity
1232
+ if len(set([x.polarity for k, x in lcms_coll._lcms.items()])) != 1:
1233
+ raise ValueError("All samples must have the same polarity.")
1234
+
1235
+ # Set ids on the LCMS objects in the manifest
1236
+ i = 0
1237
+ for sample in lcms_coll.samples:
1238
+ lcms_coll._manifest_dict[sample]["collection_id"] = i
1239
+ i += 1
1240
+
1241
+ # Reorder the LCMS objects
1242
+ lcms_coll._reorder_lcms_objects()
1243
+
1244
+ # Collect the mass features from the LCMS objects and combine them into a single dataframe for the collection
1245
+ lcms_coll._combine_mass_features()
1246
+
1247
+ # If load_light, remove the mass_feature attribute from the individual LCMS objects
1248
+ if load_light:
1249
+ for sample_name in lcms_coll.samples:
1250
+ lcms_coll._lcms[sample_name].mass_features = {}
1251
+ # Remove the light_mf_df attribute from the individual LCMS objects
1252
+ del lcms_coll._lcms[sample_name].light_mf_df
1253
+
1254
+
1255
+ return lcms_coll
1256
+
1257
+ @property
1258
+ def manifest(self):
1259
+ return self._manifest_dict
1260
+
1261
+ @property
1262
+ def manifest_dataframe(self):
1263
+ return pd.DataFrame(self._manifest_dict).T
1264
+
1265
+ @property
1266
+ def hdf5_files(self):
1267
+ return [
1268
+ self.folder_location / f"{sample_name}.corems/{sample_name}.hdf5"
1269
+ for sample_name in self._manifest_dict.keys()
1270
+ ]
1271
+
1272
+ @property
1273
+ def parameters_files(self):
1274
+ # Check if parameters files are saved as JSON or TOML
1275
+ json_files = [
1276
+ self.folder_location / f"{sample_name}.corems/{sample_name}.json"
1277
+ for sample_name in self._manifest_dict.keys()
1278
+ ]
1279
+ toml_files = [
1280
+ self.folder_location / f"{sample_name}.corems/{sample_name}.toml"
1281
+ for sample_name in self._manifest_dict.keys()
1282
+ ]
1283
+ if all([x.exists() for x in json_files]):
1284
+ return json_files
1285
+ elif all([x.exists() for x in toml_files]):
1286
+ return toml_files
1287
+ else:
1288
+ raise ValueError("Parameters files are not saved for all samples.")
1289
+
1290
+ class ReadSavedLCMSCollection(ReadCoreMSHDFMassSpectraCollection):
1291
+ """
1292
+ Subclass to read and re-instantiate a LCMSCollection from a saved HDF5 file.
1293
+
1294
+
1295
+ Parameters
1296
+ ----------
1297
+ collection_hdf5_path : str or Path
1298
+ Path to the saved LCMSCollection HDF5 file.
1299
+ cores : int, optional
1300
+ Number of cores for processing. Default is 1.
1301
+ """
1302
+
1303
+ def __init__(
1304
+ self,
1305
+ collection_hdf5_path: str,
1306
+ cores: int = 1
1307
+ ):
1308
+ # Convert to Path objects
1309
+ self.collection_hdf5_path = Path(collection_hdf5_path)
1310
+
1311
+ # Validate the collection file exists
1312
+ if not self.collection_hdf5_path.exists():
1313
+ raise FileNotFoundError(f"Collection HDF5 file {self.collection_hdf5_path} not found.")
1314
+
1315
+ # Validate cores
1316
+ self._validate_cores(cores)
1317
+
1318
+ # Load metadata from saved collection
1319
+ self._load_collection_metadata()
1320
+
1321
+ if not self.folder_location.exists():
1322
+ raise FileNotFoundError(f"Folder location {self.folder_location} not found.")
1323
+
1324
+ # Load the mass spectra data
1325
+ self._validate_manifest()
1326
+
1327
+ # Set the parameters file location
1328
+ self.parameters_location = self._get_parameters_location()
1329
+
1330
+ def _get_parameters_location(self):
1331
+ """Find the parameters file (JSON or TOML) associated with the collection HDF5 file."""
1332
+ # Check for TOML file first (preferred)
1333
+ toml_path = self.collection_hdf5_path.with_suffix('.toml')
1334
+ if toml_path.exists():
1335
+ return toml_path
1336
+
1337
+ # Check for JSON file
1338
+ json_path = self.collection_hdf5_path.with_suffix('.json')
1339
+ if json_path.exists():
1340
+ return json_path
1341
+
1342
+ # No parameters file found
1343
+ return None
1344
+
1345
+ def _load_collection_metadata(self):
1346
+ """Load metadata and manifest from the saved collection HDF5 file."""
1347
+ with h5py.File(self.collection_hdf5_path, 'r') as f:
1348
+ self.folder_location = Path(f.attrs.get('lcms_objects_folder', ''))
1349
+ self.missing_mass_features_searched = f.attrs.get('missing_mass_features_searched', False)
1350
+
1351
+ # Call the _load_manifest function to process the manifest
1352
+ self._manifest_dict = self._load_manifest(f)
1353
+
1354
+ def _load_manifest(self, hdf_handle):
1355
+ """Load and clean the manifest from the HDF5 file."""
1356
+ manifest_json = hdf_handle.attrs.get('manifest', '{}')
1357
+ if isinstance(manifest_json, bytes):
1358
+ manifest_json = manifest_json.decode('utf-8')
1359
+ loaded_manifest = json.loads(manifest_json)
1360
+
1361
+ # Convert integer values for 'use_rt_alignment' back to booleans
1362
+ def convert_back_to_bool(data):
1363
+ if isinstance(data, dict):
1364
+ # Process each key-value pair recursively
1365
+ return {k: (bool(v) if k == 'use_rt_alignment' and isinstance(v, int) else convert_back_to_bool(v)) for k, v in data.items()}
1366
+ elif isinstance(data, list):
1367
+ # Recursively process lists
1368
+ return [convert_back_to_bool(item) for item in data]
1369
+ else:
1370
+ # Return non-dict/list types unchanged
1371
+ return data
1372
+
1373
+ # Clean the loaded manifest
1374
+ return convert_back_to_bool(loaded_manifest)
1375
+
1376
+ def _load_rt_alignments(self, lcms_collection):
1377
+ """Load retention time alignments from the saved collection HDF5 file."""
1378
+ # First Set the rt_aligned flag from the collection-level attribute saved directly
1379
+ with h5py.File(self.collection_hdf5_path, 'r') as f:
1380
+ lcms_collection.rt_aligned = f.attrs.get('rt_aligned', False)
1381
+ lcms_collection.rt_alignment_attempted = f.attrs.get('rt_alignment_attempted', False)
1382
+
1383
+ if lcms_collection.rt_aligned:
1384
+ with h5py.File(self.collection_hdf5_path, 'r') as f:
1385
+ if "rt_alignments" in f:
1386
+ # Iterate over the group `rt_alignments` containing datasets and add to the corresponding lcms object
1387
+ rt_alignments_group = f["rt_alignments"]
1388
+ for sample_idx, lcms_obj in zip(rt_alignments_group.keys(), lcms_collection):
1389
+ alignment_data = rt_alignments_group[sample_idx][:]
1390
+ scan_df = lcms_obj.scan_df
1391
+ scan_df["scan_time_aligned"] = alignment_data
1392
+ lcms_obj.scan_df = scan_df
1393
+ elif lcms_collection.rt_alignment_attempted:
1394
+ # This means it was attempted and not used, so we populate the "scan_time_aligned"
1395
+ for lcms_obj in lcms_collection:
1396
+ scan_df = lcms_obj.scan_df
1397
+ scan_df["scan_time_aligned"] = scan_df["scan_time"]
1398
+ lcms_obj.scan_df = scan_df
1399
+
1400
+ def _load_cluster_assignments(self, lcms_collection):
1401
+ """Load cluster assignments from the saved collection HDF5 file."""
1402
+ with h5py.File(self.collection_hdf5_path, 'r') as f:
1403
+ if "cluster_assignments" in f:
1404
+ # Access the group containing cluster assignments
1405
+ cluster_grp = f["cluster_assignments"]
1406
+
1407
+ # Reload index and cluster data
1408
+ index = cluster_grp["index"][:] # Extract index
1409
+ index = [idx.decode('utf-8') for idx in index] # Convert byte strings back to regular strings
1410
+ cluster_data = cluster_grp["cluster"][:] # Extract cluster column
1411
+
1412
+ # Reassemble the DataFrame
1413
+ cluster_df = pd.DataFrame({"cluster": cluster_data}, index=index)
1414
+
1415
+ # Assign cluster data back to lcms_collection.mass_features_dataframe
1416
+ lcms_collection.mass_features_dataframe = lcms_collection.mass_features_dataframe.join(cluster_df, how='left')
1417
+
1418
+ # Drop rows with NaN cluster values
1419
+ lcms_collection.mass_features_dataframe.dropna(subset=['cluster'], inplace=True)
1420
+
1421
+ def get_lcms_collection(self, load_raw=False, load_light=False, load_representatives=False, load_eics=False, load_ms1=False, load_ms2=False):
1422
+ """Get the LCMS collection from the saved HDF5 file.
1423
+
1424
+ Parameters
1425
+ ----------
1426
+ load_raw : bool, optional
1427
+ If True, load raw data. Default is False.
1428
+ load_light : bool, optional
1429
+ If True, load light data (minimal). Default is False.
1430
+ load_representatives : bool, optional
1431
+ If True, load representative mass features from clusters. Default is False.
1432
+ load_eics : bool, optional
1433
+ If True, load EIC data for clustered mass features. Default is False.
1434
+ load_ms1 : bool, optional
1435
+ If True, load MS1 spectra for loaded mass features. Default is False.
1436
+ load_ms2 : bool, optional
1437
+ If True, load MS2 spectra for loaded mass features. Default is False.
1438
+
1439
+ Returns
1440
+ -------
1441
+ LCMSCollection
1442
+ The loaded LCMS collection object.
1443
+ """
1444
+ # First load the LCMSCollection object exactly as in the parent class
1445
+ lcms_collection = super().get_lcms_collection(load_raw=load_raw, load_light=load_light)
1446
+
1447
+ # Set the missing_mass_features_searched flag from saved metadata
1448
+ lcms_collection.missing_mass_features_searched = self.missing_mass_features_searched
1449
+
1450
+ # Load parameters if a parameters file exists
1451
+ if self.parameters_location:
1452
+ self._load_parameters(lcms_collection)
1453
+
1454
+ # Add retention time alignments if they exist
1455
+ self._load_rt_alignments(lcms_collection)
1456
+
1457
+ # Add cluster assignments if they exist
1458
+ self._load_cluster_assignments(lcms_collection)
1459
+
1460
+ # Load induced mass features if they exist
1461
+ self._load_induced_mass_features(lcms_collection)
1462
+
1463
+ # Load EICs for induced mass features from collection HDF5
1464
+ if lcms_collection.missing_mass_features_searched and load_eics:
1465
+ self._load_induced_eics_from_collection(lcms_collection)
1466
+
1467
+ # Combine induced mass features into the collection-level dataframe if any were loaded
1468
+ if lcms_collection.missing_mass_features_searched:
1469
+ lcms_collection._combine_mass_features(induced_features=True)
1470
+
1471
+ # Load representative mass features if requested
1472
+ if load_representatives:
1473
+ self._load_representative_mass_features(lcms_collection)
1474
+
1475
+ # Load MS1 and/or MS2 spectra for loaded mass features if requested
1476
+ if load_ms1 or load_ms2:
1477
+ # Reuse the existing ReloadFeaturesOperation from the pipeline system
1478
+ from corems.mass_spectra.calc.lc_calc_operations import ReloadFeaturesOperation
1479
+
1480
+ operations = [ReloadFeaturesOperation('reload_spectra', add_ms1=load_ms1, add_ms2=load_ms2)]
1481
+ lcms_collection.process_samples_pipeline(operations, keep_raw_data=False, show_progress=False)
1482
+
1483
+ # Load EICs for clustered features if requested
1484
+ if load_eics:
1485
+ # Reuse the existing LoadEICsOperation from the pipeline system
1486
+ from corems.mass_spectra.calc.lc_calc_operations import LoadEICsOperation
1487
+
1488
+ operations = [LoadEICsOperation('load_eics')]
1489
+ lcms_collection.process_samples_pipeline(operations, keep_raw_data=False, show_progress=False)
1490
+
1491
+ # Associate EICs with mass features (same as in process_consensus_features)
1492
+ for sample_id in range(len(lcms_collection.samples)):
1493
+ sample = lcms_collection[sample_id]
1494
+ if sample.eics: # Only if EICs were loaded
1495
+ # Associate EICs with regular mass features
1496
+ sample.associate_eics_with_mass_features(induced=False)
1497
+ # Associate EICs with induced mass features
1498
+ sample.associate_eics_with_mass_features(induced=True)
1499
+
1500
+ return lcms_collection
1501
+
1502
+ def _load_parameters(self, lcms_collection):
1503
+ """Load collection-level parameters from the saved parameters file."""
1504
+ from corems.encapsulation.input.parameter_from_json import (
1505
+ load_and_set_json_parameters_lcms_collection,
1506
+ load_and_set_toml_parameters_lcms_collection,
1507
+ )
1508
+
1509
+ if self.parameters_location.suffix == ".json":
1510
+ load_and_set_json_parameters_lcms_collection(lcms_collection, self.parameters_location)
1511
+ elif self.parameters_location.suffix == ".toml":
1512
+ load_and_set_toml_parameters_lcms_collection(lcms_collection, self.parameters_location)
1513
+ else:
1514
+ warnings.warn(f"Unknown parameter file format: {self.parameters_location.suffix}. Skipping parameter loading.")
1515
+
1516
+ def _load_induced_mass_features(self, lcms_collection):
1517
+ """Load induced mass features from the saved collection HDF5 file.
1518
+
1519
+ Induced mass features are gap-filled features that exist at the collection level.
1520
+ This method loads them from the collection HDF5 file with all their attributes
1521
+ and datasets, and distributes them to individual LCMS objects.
1522
+
1523
+ Parameters
1524
+ ----------
1525
+ lcms_collection : LCMSCollection
1526
+ The LCMS collection object to populate with induced mass features.
1527
+ """
1528
+ with h5py.File(self.collection_hdf5_path, 'r') as f:
1529
+ if "induced_mass_features" not in f:
1530
+ return
1531
+
1532
+ # Access the top-level induced mass features group
1533
+ imf_group = f["induced_mass_features"]
1534
+
1535
+ # Iterate through each sample's induced mass features
1536
+ for sample_idx in imf_group.keys():
1537
+ lcms_obj = lcms_collection[int(sample_idx)]
1538
+ sample_group = imf_group[sample_idx]
1539
+
1540
+ # Load each mass feature for this sample
1541
+ for mf_id_str in sample_group.keys():
1542
+ mf_group = sample_group[mf_id_str]
1543
+
1544
+ # Note: Induced mass feature IDs are strings like 'c2923_0_i', not integers
1545
+ # Keep them as strings since that's how they're stored
1546
+ mf_id = mf_id_str
1547
+
1548
+ # Instantiate the LCMSMassFeature object with required attributes
1549
+ mass_feature = LCMSMassFeature(
1550
+ lcms_obj,
1551
+ mz=mf_group.attrs["_mz_exp"],
1552
+ retention_time=mf_group.attrs["_retention_time"],
1553
+ intensity=mf_group.attrs["_intensity"],
1554
+ apex_scan=mf_group.attrs["_apex_scan"],
1555
+ persistence=mf_group.attrs.get("_persistence", 0),
1556
+ id=mf_id,
1557
+ )
1558
+
1559
+ # Populate additional attributes from HDF5 attributes
1560
+ for key in mf_group.attrs.keys() - {
1561
+ "_mz_exp",
1562
+ "_mz_cal",
1563
+ "_retention_time",
1564
+ "_intensity",
1565
+ "_apex_scan",
1566
+ "_persistence",
1567
+ }:
1568
+ setattr(mass_feature, key, mf_group.attrs[key])
1569
+
1570
+ # Populate attributes from HDF5 datasets (arrays)
1571
+ for key in mf_group.keys():
1572
+ setattr(mass_feature, key, mf_group[key][:])
1573
+ # Convert _noise_score from array to tuple
1574
+ if key == "_noise_score":
1575
+ mass_feature._noise_score = tuple(mass_feature._noise_score)
1576
+
1577
+ # Add to the LCMS object's induced_mass_features dictionary
1578
+ lcms_obj.induced_mass_features[mf_id] = mass_feature
1579
+
1580
+ def _load_induced_eics_from_collection(self, lcms_collection):
1581
+ """Load EICs for induced mass features from the collection HDF5 file.
1582
+
1583
+ Induced mass features are gap-filled features. Their EICs are saved at the
1584
+ collection level and need to be loaded and associated with the induced mass features.
1585
+
1586
+ Parameters
1587
+ ----------
1588
+ lcms_collection : LCMSCollection
1589
+ The LCMS collection object with induced mass features to associate EICs with.
1590
+ """
1591
+ with h5py.File(self.collection_hdf5_path, 'r') as f:
1592
+ if "induced_eics" not in f:
1593
+ return
1594
+
1595
+ # Access the top-level induced EICs group
1596
+ induced_eics_group = f["induced_eics"]
1597
+
1598
+ # Iterate through each sample's induced EICs
1599
+ for sample_idx in induced_eics_group.keys():
1600
+ lcms_obj = lcms_collection[int(sample_idx)]
1601
+ sample_group = induced_eics_group[sample_idx]
1602
+
1603
+ # Use the static helper to load EICs
1604
+ loaded_eics = ReadCoreMSHDFMassSpectra._load_eics_from_hdf5_group(sample_group, lcms_obj)
1605
+
1606
+ # Ensure eics dictionary exists (should already be initialized in __init__)
1607
+ if not hasattr(lcms_obj, 'eics') or lcms_obj.eics is None:
1608
+ lcms_obj.eics = {}
1609
+
1610
+ # Add to lcms_obj.eics dictionary
1611
+ for eic_mz, eic in loaded_eics.items():
1612
+ lcms_obj.eics[eic_mz] = eic
1613
+
1614
+ # Associate EICs with induced mass features after all samples processed
1615
+ # This is done outside the loop to handle all samples at once
1616
+ for lcms_obj in lcms_collection:
1617
+ if len(lcms_obj.induced_mass_features) > 0:
1618
+ lcms_obj.associate_eics_with_mass_features(induced=True)
1619
+
1620
+ def _load_representative_mass_features(self, lcms_collection):
1621
+ """Load representative mass features for all clusters from HDF5 files.
1622
+
1623
+ This method uses the same logic as process_consensus_features() when loading
1624
+ representatives, calling get_sample_mf_map_for_representatives() (DRY helper)
1625
+ to determine which features to load.
1626
+
1627
+ Parameters
1628
+ ----------
1629
+ lcms_collection : LCMSCollection
1630
+ The LCMS collection object to populate with representative mass features.
1631
+ """
1632
+ # Get cluster assignments from the mass_features_dataframe
1633
+ if "cluster" not in lcms_collection.mass_features_dataframe.columns:
1634
+ return
1635
+
1636
+ # Use DRY helper method to build sample_mf_map with cluster IDs
1637
+ sample_mf_map = lcms_collection.get_sample_mf_map_for_representatives(include_cluster_id=True)
1638
+
1639
+ # Load mass features for each sample
1640
+ for sample_id, mf_list in sample_mf_map.items():
1641
+ lcms_obj = lcms_collection[sample_id]
1642
+
1643
+ # Load each mass feature
1644
+ for mf_id, cluster_id in mf_list:
1645
+ self._load_single_mass_feature(lcms_obj, mf_id, cluster_id)
1646
+
1647
+ def _load_single_mass_feature(self, lcms_obj, feature_id, cluster_index=None):
1648
+ """Load a single mass feature from an LCMS object's HDF5 file.
1649
+
1650
+ Parameters
1651
+ ----------
1652
+ lcms_obj : LCMSBase
1653
+ The LCMS object to add the mass feature to.
1654
+ feature_id : int
1655
+ The ID of the mass feature to load.
1656
+ cluster_index : int, optional
1657
+ The cluster index to assign to the loaded mass feature.
1658
+ """
1659
+ hdf5_path = lcms_obj.file_location.with_suffix('.hdf5')
1660
+
1661
+ if not hdf5_path.exists():
1662
+ return
1663
+
1664
+ with h5py.File(hdf5_path, 'r') as f:
1665
+ if 'mass_features' not in f:
1666
+ return
1667
+
1668
+ mf_group = f['mass_features']
1669
+ feature_id_str = str(feature_id)
1670
+
1671
+ if feature_id_str not in mf_group:
1672
+ return
1673
+
1674
+ mf_data = mf_group[feature_id_str]
1675
+
1676
+ # Create LCMSMassFeature object
1677
+ mass_feature = LCMSMassFeature(
1678
+ lcms_obj,
1679
+ mz=mf_data.attrs["_mz_exp"],
1680
+ retention_time=mf_data.attrs["_retention_time"],
1681
+ intensity=mf_data.attrs["_intensity"],
1682
+ apex_scan=mf_data.attrs["_apex_scan"],
1683
+ persistence=mf_data.attrs.get("_persistence", 0),
1684
+ id=feature_id,
1685
+ )
1686
+
1687
+ # Set cluster_index if provided
1688
+ if cluster_index is not None:
1689
+ mass_feature.cluster_index = cluster_index
1690
+
1691
+ # Populate additional attributes from HDF5 attributes
1692
+ for key in mf_data.attrs.keys() - {
1693
+ "_mz_exp",
1694
+ "_mz_cal",
1695
+ "_retention_time",
1696
+ "_intensity",
1697
+ "_apex_scan",
1698
+ "_persistence",
1699
+ }:
1700
+ setattr(mass_feature, key, mf_data.attrs[key])
1701
+
1702
+ # Populate attributes from HDF5 datasets (arrays)
1703
+ for key in mf_data.keys():
1704
+ setattr(mass_feature, key, mf_data[key][:])
1705
+ # Convert _noise_score from array to tuple
1706
+ if key == "_noise_score":
1707
+ mass_feature._noise_score = tuple(mass_feature._noise_score)
1708
+
1709
+ # Add to the LCMS object's mass_features dictionary
1710
+ lcms_obj.mass_features[feature_id] = mass_feature