Skip to content

Commit a809e84

Browse files
authored
Merge pull request #252 from mwang87/mgf-refactor
bug fix refactoring
2 parents b878761 + f3ccf58 commit a809e84

6 files changed

Lines changed: 112 additions & 68 deletions

File tree

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,8 @@ deploy_clean:
3333

3434
deploy_pypi:
3535
python -m build --sdist --wheel .
36-
twine upload dist/*
36+
twine upload dist/*
37+
38+
specific_pytest:
39+
#pytest -vv --cov=massql ./tests/test_extraction.py::test_extract_MGF
40+
python ./tests/test_extraction.py

massql/msql_extract.py

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
import pandas as pd
77
import numpy as np
88
import pymzml
9-
from pyteomics import mzxml
10-
from matchms.importing import load_from_mgf
9+
from pyteomics import mzxml, mgf
1110
from psims.mzml.writer import MzMLWriter
1211

1312
def main():
@@ -139,31 +138,38 @@ def _extract_mzXML_scan(input_filename, spectrum_identifier_list):
139138

140139
def _extract_mgf_scan(input_filename, spectrum_identifier_list):
141140
output_list = []
142-
spectrum_identifier_set = set([str(spectrum_scan) for spectrum_scan in spectrum_identifier_list])
143-
144-
file = load_from_mgf(input_filename)
145-
146-
spec = None
147-
for spectrum in file:
148-
scan_number = spectrum.metadata["scans"]
149-
if str(scan_number) in spectrum_identifier_set:
150-
spec = spectrum
151-
152-
mz_list = list(spec.peaks.mz)
153-
i_list = list(spec.peaks.intensities)
154-
155-
peaks_list = []
156-
for i in range(len(mz_list)):
157-
peaks_list.append([mz_list[i], i_list[i]])
141+
142+
# Convert identifiers to a set
143+
spectrum_identifier_set = set(str(scan) for scan in spectrum_identifier_list)
158144

159-
# Loading Data
160-
spectrum_obj = {}
161-
spectrum_obj["peaks"] = peaks_list
162-
spectrum_obj["mslevel"] = 2
163-
spectrum_obj["scan"] = scan_number
164-
spectrum_obj["precursor_mz"] = float(spec.metadata["pepmass"][0])
145+
# Open the MGF file using pyteomics
146+
with mgf.read(input_filename) as reader:
147+
for spectrum in reader:
148+
# Pyteomics stores metadata in the 'params' dictionary
149+
# We convert to string to ensure matching works against the set
150+
scan_number = str(spectrum['params'].get('scans'))
151+
152+
if scan_number in spectrum_identifier_set:
153+
# pyteomics returns numpy arrays for 'm/z array' and 'intensity array'
154+
# We zip them to create the [[mz, int], [mz, int]] structure
155+
mz_array = spectrum['m/z array']
156+
int_array = spectrum['intensity array']
157+
158+
# Create the list of lists
159+
peaks_list = [[float(mz), float(i)] for mz, i in zip(mz_array, int_array)]
160+
161+
# Extract precursor m/z (pepmass is usually a tuple: (mz, intensity))
162+
precursor_mz = float(spectrum['params']['pepmass'][0])
163+
164+
# Construct the object
165+
spectrum_obj = {
166+
"peaks": peaks_list,
167+
"mslevel": 2,
168+
"scan": scan_number,
169+
"precursor_mz": precursor_mz
170+
}
165171

166-
output_list.append(spectrum_obj)
172+
output_list.append(spectrum_obj)
167173

168174
return output_list
169175

@@ -227,6 +233,8 @@ def _extract_spectra(results_df, input_spectra_folder,
227233
elif input_spectra_filename[-5:] == ".json":
228234
spectrum_obj_list = _extract_json_scan(input_spectra_filename, list(set(results_by_file_df["scan"])))
229235

236+
print(spectrum_obj_list)
237+
230238
for spectrum_obj in spectrum_obj_list:
231239
# These are a new scan number in the file, not sure if we need this
232240
spectrum_obj["new_scan"] = current_scan

massql/msql_fileloading.py

Lines changed: 69 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55
import numpy as np
66

77
from tqdm import tqdm
8-
from matchms.importing import load_from_mgf
98
import pymzml
10-
from pyteomics import mzxml, mzml
9+
from pyteomics import mzxml, mzml, mgf
1110

1211
import logging
1312
logger = logging.getLogger('msql_fileloading')
@@ -143,51 +142,83 @@ def load_data(input_filename, cache=None, cache_dir=None, cache_file=None):
143142
return ms1_df, ms2_df
144143

145144
def _load_data_mgf(input_filename):
146-
file = load_from_mgf(input_filename)
145+
ms2_data_list = []
147146

148-
ms2mz_list = []
149-
for i, spectrum in enumerate(file):
150-
if len(spectrum.peaks.mz) == 0:
151-
continue
152-
153-
mz_list = list(spectrum.peaks.mz)
154-
i_list = list(spectrum.peaks.intensities)
155-
i_max = max(i_list)
156-
i_sum = sum(i_list)
147+
# Use 'with' context manager for safe file handling
148+
with mgf.read(input_filename) as reader:
149+
for index, spectrum in enumerate(reader):
150+
151+
# Pyteomics returns numpy arrays
152+
mz_array = spectrum['m/z array']
153+
int_array = spectrum['intensity array']
157154

158-
for i in range(len(mz_list)):
159-
if i_list[i] == 0:
155+
# Skip empty spectra
156+
if len(mz_array) == 0:
160157
continue
161158

162-
peak_dict = {}
163-
peak_dict["i"] = i_list[i]
164-
peak_dict["i_norm"] = i_list[i] / i_max
165-
peak_dict["i_tic_norm"] = i_list[i] / i_sum
166-
peak_dict["mz"] = mz_list[i]
159+
# Calculate spectrum-wide statistics
160+
i_max = int_array.max()
161+
i_sum = int_array.sum()
162+
163+
# --- Metadata Extraction ---
164+
params = spectrum.get('params', {})
167165

168-
# Handling malformed mgf files
166+
# Scan: Use 'scans' or fallback to index
167+
scan = params.get('scans', index + 1)
168+
169+
# RT: Parse 'rtinseconds', default 0, convert to minutes
169170
try:
170-
peak_dict["scan"] = spectrum.metadata["scans"]
171-
except:
172-
peak_dict["scan"] = i + 1
171+
rt = float(params.get('rtinseconds', 0)) / 60.0
172+
except (ValueError, TypeError):
173+
rt = 0.0
174+
175+
# Precursor m/z: 'pepmass' is usually a tuple (mz, intensity)
173176
try:
174-
peak_dict["rt"] = float(spectrum.metadata["rtinseconds"]) / 60
175-
except:
176-
peak_dict["rt"] = 0
177+
precmz = float(params.get('pepmass', [0])[0])
178+
except (IndexError, ValueError, TypeError):
179+
precmz = 0.0
180+
181+
# Charge: Parse 'CHARGE=2+' format
182+
# Pyteomics often returns charge as a list or integer depending on config
177183
try:
178-
peak_dict["precmz"] = float(spectrum.metadata["pepmass"][0])
184+
charge_val = params.get('charge', [1])
185+
# Handle cases where it is a list e.g., [2+] or [2]
186+
if isinstance(charge_val, list):
187+
charge_str = str(charge_val[0])
188+
else:
189+
charge_str = str(charge_val)
190+
# Strip '+' and convert to int
191+
charge = int(charge_str.strip('+'))
179192
except:
180-
peak_dict["precmz"] = 0
181-
182-
peak_dict["ms1scan"] = 0
183-
peak_dict["charge"] = 1 # TODO: Add Charge Correctly here
184-
peak_dict["polarity"] = 1 # TODO: Add Polarity Correctly here
185-
186-
ms2mz_list.append(peak_dict)
187-
188-
# Turning into pandas data frames
189-
ms1_df = pd.DataFrame([peak_dict])
190-
ms2_df = pd.DataFrame(ms2mz_list)
193+
charge = 1
194+
195+
# --- Peak Extraction ---
196+
# Zip arrays to iterate pairs
197+
for mz, intensity in zip(mz_array, int_array):
198+
if intensity == 0:
199+
continue
200+
201+
peak_dict = {
202+
"i": intensity,
203+
"i_norm": intensity / i_max,
204+
"i_tic_norm": intensity / i_sum,
205+
"mz": mz,
206+
"scan": scan,
207+
"rt": rt,
208+
"precmz": precmz,
209+
"ms1scan": 0,
210+
"charge": charge, # Implemented
211+
"polarity": 1 # Default
212+
}
213+
214+
ms2_data_list.append(peak_dict)
215+
216+
# Convert to DataFrames
217+
ms2_df = pd.DataFrame(ms2_data_list)
218+
219+
# Original code assigned the last single peak to ms1_df.
220+
# Initializing empty to prevent bugs.
221+
ms1_df = pd.DataFrame()
191222

192223
return ms1_df, ms2_df
193224

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ pandas
44
pyarrow
55
tqdm
66
py_expression_eval
7-
matchms
87
pyteomics
98
psims
109
plotly

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
"pyarrow",
3535
"tqdm",
3636
"py_expression_eval",
37-
"matchms",
3837
"pyteomics",
3938
"psims",
4039
"plotly",

tests/test_extraction.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ def test_extract_MGF():
6161
results_df["filename"] = "specs_ms.mgf"
6262

6363
print("Extracting", len(results_df))
64+
print(results_df)
65+
66+
# Extracting now
6467
merged_summary_df = msql_extract._extract_spectra(results_df, "tests/data/", output_json_filename="test.json")
6568
assert(len(merged_summary_df) == 5)
6669

@@ -90,9 +93,9 @@ def test_waters_uv_extract():
9093
def main():
9194
#test_extract_mzML()
9295
#test_extract_mzXML()
93-
#test_extract_MGF()
96+
test_extract_MGF()
9497
#test_gnps_library_extract()
95-
test_waters_uv_extract()
98+
#test_waters_uv_extract()
9699

97100

98101
if __name__ == "__main__":

0 commit comments

Comments
 (0)