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