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.
- corems/__init__.py +63 -0
- corems/chroma_peak/__init__.py +0 -0
- corems/chroma_peak/calc/ChromaPeakCalc.py +480 -0
- corems/chroma_peak/calc/__init__.py +0 -0
- corems/chroma_peak/calc/subset.py +196 -0
- corems/chroma_peak/factory/__init__.py +0 -0
- corems/chroma_peak/factory/chroma_peak_classes.py +1178 -0
- corems/encapsulation/__init__.py +0 -0
- corems/encapsulation/constant.py +1283 -0
- corems/encapsulation/factory/__init__.py +0 -0
- corems/encapsulation/factory/parameters.py +392 -0
- corems/encapsulation/factory/processingSetting.py +1244 -0
- corems/encapsulation/input/__init__.py +0 -0
- corems/encapsulation/input/parameter_from_json.py +579 -0
- corems/encapsulation/output/__init__.py +0 -0
- corems/encapsulation/output/parameter_to_dict.py +142 -0
- corems/encapsulation/output/parameter_to_json.py +332 -0
- corems/mass_spectra/__init__.py +0 -0
- corems/mass_spectra/calc/GC_Calc.py +84 -0
- corems/mass_spectra/calc/GC_Deconvolution.py +558 -0
- corems/mass_spectra/calc/GC_RI_Calibration.py +44 -0
- corems/mass_spectra/calc/MZSearch.py +249 -0
- corems/mass_spectra/calc/SignalProcessing.py +656 -0
- corems/mass_spectra/calc/__init__.py +0 -0
- corems/mass_spectra/calc/lc_calc.py +5790 -0
- corems/mass_spectra/calc/lc_calc_operations.py +1127 -0
- corems/mass_spectra/factory/GC_Class.py +791 -0
- corems/mass_spectra/factory/__init__.py +0 -0
- corems/mass_spectra/factory/chromat_data.py +51 -0
- corems/mass_spectra/factory/lc_class.py +2721 -0
- corems/mass_spectra/input/__init__.py +0 -0
- corems/mass_spectra/input/andiNetCDF.py +200 -0
- corems/mass_spectra/input/boosterHDF5.py +216 -0
- corems/mass_spectra/input/brukerSolarix.py +153 -0
- corems/mass_spectra/input/brukerSolarix_utils.py +73 -0
- corems/mass_spectra/input/corems_hdf5.py +1710 -0
- corems/mass_spectra/input/massList.py +133 -0
- corems/mass_spectra/input/mzml.py +668 -0
- corems/mass_spectra/input/parserbase.py +239 -0
- corems/mass_spectra/input/rawFileReader.py +1839 -0
- corems/mass_spectra/output/__init__.py +0 -0
- corems/mass_spectra/output/export.py +2800 -0
- corems/mass_spectrum/__init__.py +0 -0
- corems/mass_spectrum/calc/AutoRecalibration.py +237 -0
- corems/mass_spectrum/calc/Calibration.py +602 -0
- corems/mass_spectrum/calc/CalibrationCalc.py +253 -0
- corems/mass_spectrum/calc/KendrickGroup.py +239 -0
- corems/mass_spectrum/calc/MassErrorPrediction.py +436 -0
- corems/mass_spectrum/calc/MassSpectrumCalc.py +303 -0
- corems/mass_spectrum/calc/MeanResolvingPowerFilter.py +212 -0
- corems/mass_spectrum/calc/NoiseCalc.py +371 -0
- corems/mass_spectrum/calc/NoiseCalc_Bayes.py +93 -0
- corems/mass_spectrum/calc/PeakPicking.py +994 -0
- corems/mass_spectrum/calc/__init__.py +0 -0
- corems/mass_spectrum/factory/MassSpectrumClasses.py +1753 -0
- corems/mass_spectrum/factory/__init__.py +0 -0
- corems/mass_spectrum/input/__init__.py +0 -0
- corems/mass_spectrum/input/baseClass.py +531 -0
- corems/mass_spectrum/input/boosterHDF5.py +161 -0
- corems/mass_spectrum/input/coremsHDF5.py +475 -0
- corems/mass_spectrum/input/massList.py +402 -0
- corems/mass_spectrum/input/numpyArray.py +133 -0
- corems/mass_spectrum/output/__init__.py +0 -0
- corems/mass_spectrum/output/export.py +841 -0
- corems/molecular_formula/__init__.py +0 -0
- corems/molecular_formula/calc/MolecularFormulaCalc.py +885 -0
- corems/molecular_formula/calc/__init__.py +0 -0
- corems/molecular_formula/factory/MolecularFormulaFactory.py +895 -0
- corems/molecular_formula/factory/__init__.py +0 -0
- corems/molecular_formula/input/__init__.py +0 -0
- corems/molecular_formula/input/masslist_ref.py +355 -0
- corems/molecular_id/__init__.py +0 -0
- corems/molecular_id/calc/ClusterFilter.py +251 -0
- corems/molecular_id/calc/MolecularFilter.py +122 -0
- corems/molecular_id/calc/SpectralSimilarity.py +632 -0
- corems/molecular_id/calc/__init__.py +0 -0
- corems/molecular_id/calc/math_distance.py +1637 -0
- corems/molecular_id/data/FAMES_REF.msp +980 -0
- corems/molecular_id/data/PNNLMetV20191015.msp +157267 -0
- corems/molecular_id/factory/EI_SQL.py +650 -0
- corems/molecular_id/factory/MolecularLookupTable.py +914 -0
- corems/molecular_id/factory/__init__.py +0 -0
- corems/molecular_id/factory/classification.py +884 -0
- corems/molecular_id/factory/lipid_molecular_metadata.py +50 -0
- corems/molecular_id/factory/molecularSQL.py +827 -0
- corems/molecular_id/factory/spectrum_search_results.py +119 -0
- corems/molecular_id/input/__init__.py +0 -0
- corems/molecular_id/input/nistMSI.py +148 -0
- corems/molecular_id/search/__init__.py +0 -0
- corems/molecular_id/search/compoundSearch.py +214 -0
- corems/molecular_id/search/database_interfaces.py +1527 -0
- corems/molecular_id/search/findOxygenPeaks.py +330 -0
- corems/molecular_id/search/lcms_spectral_search.py +348 -0
- corems/molecular_id/search/molecularFormulaSearch.py +1117 -0
- corems/molecular_id/search/priorityAssignment.py +723 -0
- corems/ms_peak/__init__.py +0 -0
- corems/ms_peak/calc/MSPeakCalc.py +1010 -0
- corems/ms_peak/calc/__init__.py +0 -0
- corems/ms_peak/factory/MSPeakClasses.py +542 -0
- corems/ms_peak/factory/__init__.py +0 -0
- corems/transient/__init__.py +0 -0
- corems/transient/calc/TransientCalc.py +362 -0
- corems/transient/calc/__init__.py +0 -0
- corems/transient/factory/TransientClasses.py +457 -0
- corems/transient/factory/__init__.py +0 -0
- corems/transient/input/__init__.py +0 -0
- corems/transient/input/brukerSolarix.py +461 -0
- corems/transient/input/midasDatFile.py +172 -0
- corems-4.0.0.dist-info/METADATA +475 -0
- corems-4.0.0.dist-info/RECORD +171 -0
- corems-4.0.0.dist-info/WHEEL +5 -0
- corems-4.0.0.dist-info/licenses/LICENSE +22 -0
- corems-4.0.0.dist-info/top_level.txt +4 -0
- examples/archive/scripts/CoreMS_tutorial.py +94 -0
- examples/archive/scripts/DI HR-MS Halogens Bruker.py +196 -0
- examples/archive/scripts/DI HR-MS MassList.py +385 -0
- examples/archive/scripts/GC-MS MetabRef.py +213 -0
- examples/archive/scripts/GC-MS NetCDF.py +217 -0
- examples/archive/scripts/HR-MS Thermo Raw 21T.py +136 -0
- examples/archive/scripts/LC-ICPMS_metal_peaks.py +297 -0
- examples/archive/scripts/LCMS-Thermo.py +460 -0
- examples/archive/scripts/LCMS_isotopes.py +283 -0
- examples/archive/scripts/MSParams_example.py +21 -0
- examples/archive/scripts/Molecular Formula Data Aggreation.py +84 -0
- examples/archive/scripts/Single Mz Search.py +69 -0
- examples/test_notebooks.py +145 -0
- ext_lib/ChemstationMSFileReader.dll +0 -0
- ext_lib/ChemstationMSFileReader.xml +126 -0
- ext_lib/RawFileReaderLicense.doc +0 -0
- ext_lib/ThermoFisher.CommonCore.BackgroundSubtraction.dll +0 -0
- ext_lib/ThermoFisher.CommonCore.BackgroundSubtraction.xml +2307 -0
- ext_lib/ThermoFisher.CommonCore.Data.dll +0 -0
- ext_lib/ThermoFisher.CommonCore.Data.xml +28974 -0
- ext_lib/ThermoFisher.CommonCore.MassPrecisionEstimator.dll +0 -0
- ext_lib/ThermoFisher.CommonCore.MassPrecisionEstimator.xml +241 -0
- ext_lib/ThermoFisher.CommonCore.RawFileReader.dll +0 -0
- ext_lib/ThermoFisher.CommonCore.RawFileReader.xml +31174 -0
- ext_lib/__init__.py +0 -0
- ext_lib/dotnet/OpenMcdf.Extensions.dll +0 -0
- ext_lib/dotnet/OpenMcdf.dll +0 -0
- ext_lib/dotnet/OpenMcdf.xml +1154 -0
- ext_lib/dotnet/System.IO.FileSystem.AccessControl.dll +0 -0
- ext_lib/dotnet/System.IO.FileSystem.AccessControl.xml +506 -0
- ext_lib/dotnet/System.Security.AccessControl.dll +0 -0
- ext_lib/dotnet/System.Security.AccessControl.xml +2043 -0
- ext_lib/dotnet/System.Security.Principal.Windows.dll +0 -0
- ext_lib/dotnet/System.Security.Principal.Windows.xml +1011 -0
- ext_lib/dotnet/ThermoFisher.CommonCore.BackgroundSubtraction.dll +0 -0
- ext_lib/dotnet/ThermoFisher.CommonCore.BackgroundSubtraction.xml +2307 -0
- ext_lib/dotnet/ThermoFisher.CommonCore.Data.dll +0 -0
- ext_lib/dotnet/ThermoFisher.CommonCore.Data.xml +29148 -0
- ext_lib/dotnet/ThermoFisher.CommonCore.MassPrecisionEstimator.dll +0 -0
- ext_lib/dotnet/ThermoFisher.CommonCore.MassPrecisionEstimator.xml +241 -0
- ext_lib/dotnet/ThermoFisher.CommonCore.RawFileReader.dll +0 -0
- ext_lib/dotnet/ThermoFisher.CommonCore.RawFileReader.xml +31492 -0
- ext_lib/version +18 -0
- support_code/atom_parsers/AtomsDescription.py +359 -0
- support_code/atom_parsers/CreateAtomsDescription.py +56 -0
- support_code/nmdc/filefinder.py +90 -0
- support_code/nmdc/lipidomics/lipidomics_workflow.py +748 -0
- support_code/nmdc/lipidomics/manifest_examples.py +49 -0
- support_code/nmdc/metabolomics/gcms_workflow.py +158 -0
- support_code/nmdc/metabolomics/lcms_metabolomics_targeted_search.py +59 -0
- support_code/nmdc/metabolomics/lcms_metabolomics_workflow.py +248 -0
- support_code/nmdc/metabolomics/metabolomics_collection.py +628 -0
- support_code/nmdc/metadata/dms_api.py +42 -0
- support_code/nmdc/nom/archived_scripts/nmdc_metadata_gen.py +288 -0
- support_code/nmdc/nom/archived_scripts/nom_grow_workflow.py +209 -0
- support_code/nmdc/nom/nom_workflow.py +312 -0
- support_code/windows_only_importers/BrukerCompassXtract.py +180 -0
- 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
|