corems.mass_spectra.input.rawFileReader
1__author__ = "Yuri E. Corilo" 2__date__ = "Jun 09, 2021" 3 4 5from warnings import warn 6import warnings 7from collections import defaultdict 8 9from matplotlib import axes 10from corems.encapsulation.factory.processingSetting import LiquidChromatographSetting 11 12import numpy as np 13import sys 14from pathlib import Path 15import datetime 16import importlib.util 17import os 18 19import clr 20import pandas as pd 21from s3path import S3Path 22 23 24from typing import Any, Dict, List, Optional, Tuple, Union 25from corems.encapsulation.constant import Labels 26from corems.mass_spectra.factory.lc_class import MassSpectraBase, LCMSBase 27from corems.mass_spectra.factory.chromat_data import EIC_Data, TIC_Data 28from corems.mass_spectrum.factory.MassSpectrumClasses import ( 29 MassSpecProfile, 30 MassSpecCentroid, 31) 32from corems.encapsulation.factory.parameters import LCMSParameters, default_parameters 33from corems.mass_spectra.input.parserbase import SpectraParserInterface 34 35# Add the path of the Thermo .NET libraries to the system path 36spec = importlib.util.find_spec("corems") 37sys.path.append(str(Path(os.path.dirname(spec.origin)).parent) + "/ext_lib/dotnet/") 38 39clr.AddReference("ThermoFisher.CommonCore.RawFileReader") 40clr.AddReference("ThermoFisher.CommonCore.Data") 41clr.AddReference("ThermoFisher.CommonCore.MassPrecisionEstimator") 42 43from System.Collections.Generic import List as DotNetList 44from ThermoFisher.CommonCore.RawFileReader import RawFileReaderAdapter 45from ThermoFisher.CommonCore.Data import ToleranceUnits, Extensions 46from ThermoFisher.CommonCore.Data.Business import ( 47 ChromatogramTraceSettings, 48 TraceType, 49 MassOptions, 50) 51from ThermoFisher.CommonCore.Data.Business import ChromatogramSignal, Range 52from ThermoFisher.CommonCore.Data.Business import Device 53from ThermoFisher.CommonCore.Data.Interfaces import IChromatogramSettings 54from ThermoFisher.CommonCore.Data.Business import FileHeaderReaderFactory 55from ThermoFisher.CommonCore.Data.FilterEnums import MSOrderType 56 57 58class ThermoBaseClass: 59 """Class for parsing Thermo Raw files and extracting information from them. 60 61 Parameters: 62 ----------- 63 file_location : str or pathlib.Path or s3path.S3Path 64 Thermo Raw file path or S3 path. 65 66 Attributes: 67 ----------- 68 file_path : str or pathlib.Path or s3path.S3Path 69 The file path of the Thermo Raw file. 70 parameters : LCMSParameters 71 The LCMS parameters for the Thermo Raw file. 72 chromatogram_settings : LiquidChromatographSetting 73 The chromatogram settings for the Thermo Raw file. 74 scans : list or tuple 75 The selected scans for the Thermo Raw file. 76 start_scan : int 77 The starting scan number for the Thermo Raw file. 78 end_scan : int 79 The ending scan number for the Thermo Raw file. 80 81 Methods: 82 -------- 83 * set_msordertype(scanFilter, mstype: str = 'ms1') -> scanFilter 84 Convert the user-passed MS Type string to a Thermo MSOrderType object. 85 * get_instrument_info() -> dict 86 Get the instrument information from the Thermo Raw file. 87 * get_creation_time() -> datetime.datetime 88 Extract the creation date stamp from the .RAW file and return it as a formatted datetime object. 89 * remove_temp_file() 90 Remove the temporary file if the path is from S3Path. 91 * get_polarity_mode(scan_number: int) -> int 92 Get the polarity mode for the given scan number. 93 * get_filter_for_scan_num(scan_number: int) -> List[str] 94 Get the filter for the given scan number. 95 * check_full_scan(scan_number: int) -> bool 96 Check if the given scan number is a full scan. 97 * get_all_filters() -> Tuple[Dict[int, str], List[str]] 98 Get all scan filters for the Thermo Raw file. 99 * get_scan_header(scan: int) -> Dict[str, Any] 100 Get the full dictionary of scan header metadata for the given scan number. 101 * get_rt_time_from_trace(trace) -> Tuple[List[float], List[float], List[int]] 102 Get the retention time, intensity, and scan number from the given trace. 103 * get_eics(target_mzs: List[float], tic_data: Dict[str, Any], ms_type: str = 'MS !d', 104 peak_detection: bool = True, smooth: bool = True, plot: bool = False, 105 ax: Optional[matplotlib.axes.Axes] = None, legend: bool = False) -> Tuple[Dict[float, EIC_Data], matplotlib.axes.Axes] 106 Get the extracted ion chromatograms (EICs) for the target m/z values. 107 108 """ 109 110 def __init__(self, file_location): 111 """file_location: srt pathlib.Path or s3path.S3Path 112 Thermo Raw file path 113 """ 114 # Thread.__init__(self) 115 if isinstance(file_location, str): 116 file_path = Path(file_location) 117 118 elif isinstance(file_location, S3Path): 119 temp_dir = Path("tmp/") 120 temp_dir.mkdir(exist_ok=True) 121 122 file_path = temp_dir / file_location.name 123 with open(file_path, "wb") as fh: 124 fh.write(file_location.read_bytes()) 125 126 else: 127 file_path = file_location 128 129 self.iRawDataPlus = RawFileReaderAdapter.FileFactory(str(file_path)) 130 self.iFileHeader = None 131 132 if not self.iRawDataPlus.IsOpen: 133 self.close_file() 134 raise FileNotFoundError( 135 "Unable to access the RAW file using the RawFileReader class!" 136 ) 137 138 # Check for any errors in the RAW file 139 if self.iRawDataPlus.IsError: 140 file_error = self.iRawDataPlus.FileError 141 self.close_file() 142 raise IOError("Error opening ({}) - {}".format(file_error, file_path)) 143 144 self.res = self.iRawDataPlus.SelectInstrument(Device.MS, 1) 145 146 self.file_path = file_location 147 self.iFileHeader = FileHeaderReaderFactory.ReadFile(str(file_path)) 148 149 # removing tmp file 150 151 self._init_settings() 152 153 def _init_settings(self): 154 """ 155 Initialize the LCMSParameters object. 156 """ 157 self._parameters = LCMSParameters() 158 159 @property 160 def parameters(self) -> LCMSParameters: 161 """ 162 Get or set the LCMSParameters object. 163 """ 164 return self._parameters 165 166 @parameters.setter 167 def parameters(self, instance_LCMSParameters: LCMSParameters): 168 self._parameters = instance_LCMSParameters 169 170 @property 171 def chromatogram_settings(self) -> LiquidChromatographSetting: 172 """ 173 Get or set the LiquidChromatographSetting object. 174 """ 175 return self.parameters.lc_ms 176 177 @chromatogram_settings.setter 178 def chromatogram_settings( 179 self, instance_LiquidChromatographSetting: LiquidChromatographSetting 180 ): 181 self.parameters.lc_ms = instance_LiquidChromatographSetting 182 183 @property 184 def scans(self) -> list | tuple: 185 """scans : list or tuple 186 If list uses Thermo AverageScansInScanRange for selected scans, ortherwise uses Thermo AverageScans for a scan range 187 """ 188 return self.chromatogram_settings.scans 189 190 @property 191 def start_scan(self) -> int: 192 """ 193 Get the starting scan number for the Thermo Raw file. 194 """ 195 if self.scans[0] == -1: 196 return self.iRawDataPlus.RunHeaderEx.FirstSpectrum 197 else: 198 return self.scans[0] 199 200 @property 201 def end_scan(self) -> int: 202 """ 203 Get the ending scan number for the Thermo Raw file. 204 """ 205 if self.scans[-1] == -1: 206 return self.iRawDataPlus.RunHeaderEx.LastSpectrum 207 else: 208 return self.scans[-1] 209 210 def set_msordertype(self, scanFilter, mstype: str = "ms1"): 211 """ 212 Function to convert user passed string MS Type to Thermo MSOrderType object 213 Limited to MS1 through MS10. 214 215 Parameters: 216 ----------- 217 scanFilter : Thermo.ScanFilter 218 The scan filter object. 219 mstype : str, optional 220 The MS Type string, by default 'ms1' 221 222 """ 223 mstype = mstype.upper() 224 # Check that a valid mstype is passed 225 if (int(mstype.split("MS")[1]) > 10) or (int(mstype.split("MS")[1]) < 1): 226 warn("MS Type not valid, must be between MS1 and MS10") 227 228 msordertypedict = { 229 "MS1": MSOrderType.Ms, 230 "MS2": MSOrderType.Ms2, 231 "MS3": MSOrderType.Ms3, 232 "MS4": MSOrderType.Ms4, 233 "MS5": MSOrderType.Ms5, 234 "MS6": MSOrderType.Ms6, 235 "MS7": MSOrderType.Ms7, 236 "MS8": MSOrderType.Ms8, 237 "MS9": MSOrderType.Ms9, 238 "MS10": MSOrderType.Ms10, 239 } 240 scanFilter.MSOrder = msordertypedict[mstype] 241 return scanFilter 242 243 def get_instrument_info(self) -> dict: 244 """ 245 Get the instrument information from the Thermo Raw file. 246 247 Returns: 248 -------- 249 dict 250 A dictionary with the keys 'model', and 'serial_number'. 251 """ 252 instrumentData = self.iRawDataPlus.GetInstrumentData() 253 return { 254 "model": instrumentData.Model, 255 "serial_number": instrumentData.SerialNumber, 256 } 257 258 def get_creation_time(self) -> datetime.datetime: 259 """ 260 Extract the creation date stamp from the .RAW file 261 Return formatted creation date stamp. 262 263 """ 264 credate = self.iRawDataPlus.CreationDate.get_Ticks() 265 credate = datetime.datetime(1, 1, 1) + datetime.timedelta( 266 microseconds=credate / 10 267 ) 268 return credate 269 270 def remove_temp_file(self) -> None: 271 """if the path is from S3Path data cannot be serialized to io.ByteStream and 272 a temporary copy is stored at the temp dir 273 use this function only at the end of your execution scrip 274 some LCMS class methods depend on this file 275 """ 276 277 self.file_path.unlink() 278 279 def close_file(self) -> None: 280 """Dispose the Thermo file handles. 281 282 A second call is a no-op. This does not delete an S3 temp copy; 283 ``remove_temp_file`` does that. 284 """ 285 for attr in ("iRawDataPlus", "iFileHeader"): 286 handle = getattr(self, attr, None) 287 if handle is None: 288 continue 289 dispose = getattr(handle, "Dispose", None) 290 setattr(self, attr, None) 291 if callable(dispose): 292 dispose() 293 294 def __del__(self): 295 try: 296 self.close_file() 297 except Exception: 298 return 299 300 def get_polarity_mode(self, scan_number: int) -> int: 301 """ 302 Get the polarity mode for the given scan number. 303 304 Parameters: 305 ----------- 306 scan_number : int 307 The scan number. 308 309 Raises: 310 ------- 311 Exception 312 If the polarity mode is unknown. 313 314 """ 315 polarity_symbol = self.get_filter_for_scan_num(scan_number)[1] 316 317 if polarity_symbol == "+": 318 return 1 319 # return 'POSITIVE_ION_MODE' 320 321 elif polarity_symbol == "-": 322 return -1 323 324 else: 325 raise Exception("Polarity Mode Unknown, please set it manually") 326 327 def get_filter_for_scan_num(self, scan_number: int) -> List[str]: 328 """ 329 Returns the closest matching run time that corresponds to scan_number for the current 330 controller. This function is only supported for MS device controllers. 331 e.g. ['FTMS', '-', 'p', 'NSI', 'Full', 'ms', '[200.00-1000.00]'] 332 333 Parameters: 334 ----------- 335 scan_number : int 336 The scan number. 337 338 """ 339 scan_label = self.iRawDataPlus.GetScanEventStringForScanNumber(scan_number) 340 341 return str(scan_label).split() 342 343 def get_ms_level_for_scan_num(self, scan_number: int) -> str: 344 """ 345 Get the MS order for the given scan number. 346 347 Parameters: 348 ----------- 349 scan_number : int 350 The scan number 351 352 Returns: 353 -------- 354 int 355 The MS order type (1 for MS, 2 for MS2, etc.) 356 """ 357 scan_filter = self.iRawDataPlus.GetFilterForScanNumber(scan_number) 358 359 msordertype = { 360 MSOrderType.Ms: 1, 361 MSOrderType.Ms2: 2, 362 MSOrderType.Ms3: 3, 363 MSOrderType.Ms4: 4, 364 MSOrderType.Ms5: 5, 365 MSOrderType.Ms6: 6, 366 MSOrderType.Ms7: 7, 367 MSOrderType.Ms8: 8, 368 MSOrderType.Ms9: 9, 369 MSOrderType.Ms10: 10, 370 } 371 372 if scan_filter.MSOrder in msordertype: 373 return msordertype[scan_filter.MSOrder] 374 else: 375 raise Exception("MS Order Type not found") 376 377 def check_full_scan(self, scan_number: int) -> bool: 378 # scan_filter.ScanMode 0 = FULL 379 scan_filter = self.iRawDataPlus.GetFilterForScanNumber(scan_number) 380 381 return scan_filter.ScanMode == MSOrderType.Ms 382 383 def get_all_filters(self) -> Tuple[Dict[int, str], List[str]]: 384 """ 385 Get all scan filters. 386 This function is only supported for MS device controllers. 387 e.g. ['FTMS', '-', 'p', 'NSI', 'Full', 'ms', '[200.00-1000.00]'] 388 389 """ 390 391 scanrange = range(self.start_scan, self.end_scan + 1) 392 scanfiltersdic = {} 393 scanfilterslist = [] 394 for scan_number in scanrange: 395 scan_label = self.iRawDataPlus.GetScanEventStringForScanNumber(scan_number) 396 scanfiltersdic[scan_number] = scan_label 397 scanfilterslist.append(scan_label) 398 scanfilterset = list(set(scanfilterslist)) 399 return scanfiltersdic, scanfilterset 400 401 def get_scan_header(self, scan: int) -> Dict[str, Any]: 402 """ 403 Get full dictionary of scan header meta data, i.e. AGC status, ion injection time, etc. 404 405 Parameters: 406 ----------- 407 scan : int 408 The scan number. 409 410 """ 411 header = self.iRawDataPlus.GetTrailerExtraInformation(scan) 412 413 header_dic = {} 414 for i in range(header.Length): 415 header_dic.update({header.Labels[i]: header.Values[i]}) 416 return header_dic 417 418 @staticmethod 419 def get_rt_time_from_trace(trace) -> Tuple[List[float], List[float], List[int]]: 420 """trace: ThermoFisher.CommonCore.Data.Business.ChromatogramSignal""" 421 return list(trace.Times), list(trace.Intensities), list(trace.Scans) 422 423 def get_eics( 424 self, 425 target_mzs: List[float], 426 tic_data: Dict[str, Any], 427 ms_type="MS !d", 428 peak_detection=False, 429 smooth=False, 430 plot=False, 431 ax: Optional[axes.Axes] = None, 432 legend=False, 433 ) -> Tuple[Dict[float, EIC_Data], axes.Axes]: 434 """ms_type: str ('MS', MS2') 435 start_scan: int default -1 will select the lowest available 436 end_scan: int default -1 will select the highest available 437 438 returns: 439 440 chroma: dict{target_mz: EIC_Data( 441 Scans: [int] 442 original thermo scan numbers 443 Time: [floats] 444 list of retention times 445 TIC: [floats] 446 total ion chromatogram 447 Apexes: [int] 448 original thermo apex scan number after peak picking 449 ) 450 451 """ 452 # If peak_detection or smooth is True, raise exception 453 if peak_detection or smooth: 454 raise Exception( 455 "Peak detection and smoothing are no longer implemented in this function" 456 ) 457 458 options = MassOptions() 459 options.ToleranceUnits = ToleranceUnits.ppm 460 options.Tolerance = self.chromatogram_settings.eic_tolerance_ppm 461 462 all_chroma_settings = [] 463 464 for target_mz in target_mzs: 465 settings = ChromatogramTraceSettings(TraceType.MassRange) 466 settings.Filter = ms_type 467 settings.MassRanges = [Range(target_mz, target_mz)] 468 469 chroma_settings = IChromatogramSettings(settings) 470 471 all_chroma_settings.append(chroma_settings) 472 473 # chroma_settings2 = IChromatogramSettings(settings) 474 # print(chroma_settings.FragmentMass) 475 # print(chroma_settings.FragmentMass) 476 # print(chroma_settings) 477 # print(chroma_settings) 478 479 data = self.iRawDataPlus.GetChromatogramData( 480 all_chroma_settings, self.start_scan, self.end_scan, options 481 ) 482 483 traces = ChromatogramSignal.FromChromatogramData(data) 484 485 chroma = {} 486 487 if plot: 488 from matplotlib.transforms import Bbox 489 import matplotlib.pyplot as plt 490 491 if not ax: 492 # ax = plt.gca() 493 # ax.clear() 494 fig, ax = plt.subplots() 495 496 else: 497 fig = plt.gcf() 498 499 # plt.show() 500 501 for i, trace in enumerate(traces): 502 if trace.Length > 0: 503 rt, eic, scans = self.get_rt_time_from_trace(trace) 504 if smooth: 505 eic = self.smooth_tic(eic) 506 507 chroma[target_mzs[i]] = EIC_Data(scans=scans, time=rt, eic=eic) 508 if plot: 509 ax.plot(rt, eic, label="{:.5f}".format(target_mzs[i])) 510 511 if peak_detection: 512 # max_eic = self.get_max_eic(chroma) 513 max_signal = max(tic_data.tic) 514 515 for eic_data in chroma.values(): 516 eic = eic_data.eic 517 time = eic_data.time 518 519 if len(eic) != len(tic_data.tic): 520 warn( 521 "The software assumes same lenth of TIC and EIC, this does not seems to be the case and the results mass spectrum selected by the scan number might not be correct" 522 ) 523 524 if eic.max() > 0: 525 centroid_eics = self.eic_centroid_detector(time, eic, max_signal) 526 eic_data.apexes = [i for i in centroid_eics] 527 528 if plot: 529 for peak_indexes in eic_data.apexes: 530 apex_index = peak_indexes[1] 531 ax.plot( 532 time[apex_index], 533 eic[apex_index], 534 marker="x", 535 linewidth=0, 536 ) 537 538 if plot: 539 ax.set_xlabel("Time (min)") 540 ax.set_ylabel("a.u.") 541 ax.set_title(ms_type + " EIC") 542 ax.tick_params(axis="both", which="major", labelsize=12) 543 ax.axes.spines["top"].set_visible(False) 544 ax.axes.spines["right"].set_visible(False) 545 546 if legend: 547 legend = ax.legend(loc="upper left", bbox_to_anchor=(1.02, 0, 0.07, 1)) 548 fig.subplots_adjust(right=0.76) 549 # ax.set_prop_cycle(color=plt.cm.gist_rainbow(np.linspace(0, 1, len(traces)))) 550 551 d = {"down": 30, "up": -30} 552 553 def func(evt): 554 if legend.contains(evt): 555 bbox = legend.get_bbox_to_anchor() 556 bbox = Bbox.from_bounds( 557 bbox.x0, bbox.y0 + d[evt.button], bbox.width, bbox.height 558 ) 559 tr = legend.axes.transAxes.inverted() 560 legend.set_bbox_to_anchor(bbox.transformed(tr)) 561 fig.canvas.draw_idle() 562 563 fig.canvas.mpl_connect("scroll_event", func) 564 return chroma, ax 565 else: 566 return chroma, None 567 rt = [] 568 tic = [] 569 scans = [] 570 for i in range(traces[0].Length): 571 # print(trace[0].HasBasePeakData,trace[0].EndTime ) 572 573 # print(" {} - {}, {}".format( i, trace[0].Times[i], trace[0].Intensities[i] )) 574 rt.append(traces[0].Times[i]) 575 tic.append(traces[0].Intensities[i]) 576 scans.append(traces[0].Scans[i]) 577 578 return traces 579 # plot_chroma(rt, tic) 580 # plt.show() 581 582 def get_tic( 583 self, 584 ms_type="MS !d", 585 peak_detection=False, # This wont work right now 586 smooth=False, # This wont work right now 587 plot=False, 588 ax=None, 589 trace_type="TIC", 590 ) -> Tuple[TIC_Data, axes.Axes]: 591 """ms_type: str ('MS !d', 'MS2', None) 592 if you use None you get all scans. 593 peak_detection: bool 594 smooth: bool 595 plot: bool 596 ax: matplotlib axis object 597 trace_type: str ('TIC','BPC') 598 599 returns: 600 chroma: dict 601 { 602 Scan: [int] 603 original thermo scan numberMS 604 Time: [floats] 605 list of retention times 606 TIC: [floats] 607 total ion chromatogram 608 Apexes: [int] 609 original thermo apex scan number after peak picking 610 } 611 """ 612 # If peak_detection or smooth is True, raise exception 613 if peak_detection or smooth: 614 raise Exception( 615 "Peak detection and smoothing are no longer implemented in this function" 616 ) 617 618 if trace_type == "TIC": 619 settings = ChromatogramTraceSettings(TraceType.TIC) 620 elif trace_type == "BPC": 621 settings = ChromatogramTraceSettings(TraceType.BasePeak) 622 else: 623 raise ValueError(f"{trace_type} undefined") 624 if ms_type == "all": 625 settings.Filter = None 626 else: 627 settings.Filter = ms_type 628 629 chroma_settings = IChromatogramSettings(settings) 630 631 data = self.iRawDataPlus.GetChromatogramData( 632 [chroma_settings], self.start_scan, self.end_scan 633 ) 634 635 trace = ChromatogramSignal.FromChromatogramData(data) 636 637 data = TIC_Data(time=[], scans=[], tic=[], bpc=[], apexes=[]) 638 639 if trace[0].Length > 0: 640 for i in range(trace[0].Length): 641 # print(trace[0].HasBasePeakData,trace[0].EndTime ) 642 643 # print(" {} - {}, {}".format( i, trace[0].Times[i], trace[0].Intensities[i] )) 644 data.time.append(trace[0].Times[i]) 645 data.tic.append(trace[0].Intensities[i]) 646 data.scans.append(trace[0].Scans[i]) 647 648 # print(trace[0].Scans[i]) 649 if smooth: 650 data.tic = self.smooth_tic(data.tic) 651 652 else: 653 data.tic = np.array(data.tic) 654 655 if peak_detection: 656 centroid_peak_indexes = [ 657 i for i in self.centroid_detector(data.time, data.tic) 658 ] 659 660 data.apexes = centroid_peak_indexes 661 662 if plot: 663 if not ax: 664 import matplotlib.pyplot as plt 665 666 ax = plt.gca() 667 # fig, ax = plt.subplots(figsize=(6, 3)) 668 669 ax.plot(data.time, data.tic, label=trace_type) 670 ax.set_xlabel("Time (min)") 671 ax.set_ylabel("a.u.") 672 if peak_detection: 673 for peak_indexes in data.apexes: 674 apex_index = peak_indexes[1] 675 ax.plot( 676 data.time[apex_index], 677 data.tic[apex_index], 678 marker="x", 679 linewidth=0, 680 ) 681 682 # plt.show() 683 if trace_type == "BPC": 684 data.bpc = data.tic 685 data.tic = [] 686 return data, ax 687 if trace_type == "BPC": 688 data.bpc = data.tic 689 data.tic = [] 690 return data, None 691 692 else: 693 return None, None 694 695 def get_average_mass_spectrum( 696 self, 697 spectrum_mode: str = "profile", 698 auto_process: bool = True, 699 ppm_tolerance: float = 5.0, 700 ms_type: str = "MS1", 701 ) -> MassSpecProfile | MassSpecCentroid: 702 """ 703 Averages mass spectra over a scan range using Thermo's AverageScansInScanRange method 704 or a scan list using Thermo's AverageScans method 705 spectrum_mode: str 706 centroid or profile mass spectrum 707 auto_process: bool 708 If true performs peak picking, and noise threshold calculation after creation of mass spectrum object 709 ms_type: str 710 String of form 'ms1' or 'ms2' or 'MS3' etc. Valid up to MS10. 711 Internal function converts to Thermo MSOrderType class. 712 713 """ 714 715 def get_profile_mass_spec(averageScan, d_params: dict, auto_process: bool): 716 mz_list = list(averageScan.SegmentedScan.Positions) 717 abund_list = list(averageScan.SegmentedScan.Intensities) 718 719 data_dict = { 720 Labels.mz: mz_list, 721 Labels.abundance: abund_list, 722 } 723 724 return MassSpecProfile(data_dict, d_params, auto_process=auto_process) 725 726 def get_centroid_mass_spec(averageScan, d_params: dict): 727 noise = list(averageScan.centroidScan.Noises) 728 729 baselines = list(averageScan.centroidScan.Baselines) 730 731 rp = list(averageScan.centroidScan.Resolutions) 732 733 magnitude = list(averageScan.centroidScan.Intensities) 734 735 mz = list(averageScan.centroidScan.Masses) 736 737 array_noise_std = (np.array(noise) - np.array(baselines)) / 3 738 l_signal_to_noise = np.array(magnitude) / array_noise_std 739 740 d_params["baseline_noise"] = np.average(array_noise_std) 741 742 d_params["baseline_noise_std"] = np.std(array_noise_std) 743 744 data_dict = { 745 Labels.mz: mz, 746 Labels.abundance: magnitude, 747 Labels.rp: rp, 748 Labels.s2n: list(l_signal_to_noise), 749 } 750 751 mass_spec = MassSpecCentroid(data_dict, d_params, auto_process=False) 752 753 return mass_spec 754 755 d_params = self.set_metadata( 756 firstScanNumber=self.start_scan, lastScanNumber=self.end_scan 757 ) 758 759 # Create the mass options object that will be used when averaging the scans 760 options = MassOptions() 761 options.ToleranceUnits = ToleranceUnits.ppm 762 options.Tolerance = ppm_tolerance 763 764 # Get the scan filter for the first scan. This scan filter will be used to located 765 # scans within the given scan range of the same type 766 scanFilter = self.iRawDataPlus.GetFilterForScanNumber(self.start_scan) 767 768 # force it to only look for the MSType 769 scanFilter = self.set_msordertype(scanFilter, ms_type) 770 771 if isinstance(self.scans, tuple): 772 averageScan = Extensions.AverageScansInScanRange( 773 self.iRawDataPlus, self.start_scan, self.end_scan, scanFilter, options 774 ) 775 776 if averageScan: 777 if spectrum_mode == "profile": 778 mass_spec = get_profile_mass_spec( 779 averageScan, d_params, auto_process 780 ) 781 782 return mass_spec 783 784 elif spectrum_mode == "centroid": 785 if averageScan.HasCentroidStream: 786 mass_spec = get_centroid_mass_spec(averageScan, d_params) 787 788 return mass_spec 789 790 else: 791 raise ValueError( 792 "No Centroind data available for the selected scans" 793 ) 794 else: 795 raise ValueError("spectrum_mode must be 'profile' or centroid") 796 else: 797 raise ValueError("No data found for the selected scans") 798 799 elif isinstance(self.scans, list): 800 d_params = self.set_metadata(scans_list=self.scans) 801 802 scans = DotNetList[int]() 803 for scan in self.scans: 804 scans.Add(scan) 805 806 averageScan = Extensions.AverageScans(self.iRawDataPlus, scans, options) 807 808 if averageScan: 809 if spectrum_mode == "profile": 810 mass_spec = get_profile_mass_spec( 811 averageScan, d_params, auto_process 812 ) 813 814 return mass_spec 815 816 elif spectrum_mode == "centroid": 817 if averageScan.HasCentroidStream: 818 mass_spec = get_centroid_mass_spec(averageScan, d_params) 819 820 return mass_spec 821 822 else: 823 raise ValueError( 824 "No Centroind data available for the selected scans" 825 ) 826 827 else: 828 raise ValueError("spectrum_mode must be 'profile' or centroid") 829 830 else: 831 raise ValueError("No data found for the selected scans") 832 833 else: 834 raise ValueError("scans must be a list intergers or a tuple if integers") 835 836 def set_metadata( 837 self, 838 firstScanNumber=0, 839 lastScanNumber=0, 840 scans_list=False, 841 label=Labels.thermo_profile, 842 ): 843 """ 844 Collect metadata to be ingested in the mass spectrum object 845 846 scans_list: list[int] or false 847 lastScanNumber: int 848 firstScanNumber: int 849 """ 850 851 d_params = default_parameters(self.file_path) 852 853 # assumes scans is full scan or reduced profile scan 854 855 d_params["label"] = label 856 857 if scans_list: 858 d_params["scan_number"] = scans_list 859 860 d_params["polarity"] = self.get_polarity_mode(scans_list[0]) 861 862 else: 863 d_params["scan_number"] = "{}-{}".format(firstScanNumber, lastScanNumber) 864 865 d_params["polarity"] = self.get_polarity_mode(firstScanNumber) 866 867 d_params["analyzer"] = self.iRawDataPlus.GetInstrumentData().Model 868 869 d_params["acquisition_time"] = self.get_creation_time() 870 871 d_params["instrument_label"] = self.iRawDataPlus.GetInstrumentData().Name 872 873 return d_params 874 875 def get_instrument_methods(self, parse_strings: bool = True): 876 """ 877 This function will extract the instrument methods embedded in the raw file 878 879 First it will check if there are any instrument methods, if not returning None 880 Then it will get the total number of instrument methods. 881 For each method, it will extract the plaintext string of the method and attempt to parse it into a dictionary 882 If this fails, it will return just the string object. 883 884 This has been tested on data from an Orbitrap ID-X with embedded MS and LC methods, but other instrument types may fail. 885 886 Parameters: 887 ----------- 888 parse_strings: bool 889 If True, will attempt to parse the instrument methods into a dictionary. If False, will return the raw string. 890 891 Returns: 892 -------- 893 List[Dict[str, Any]] or List 894 A list of dictionaries containing the instrument methods, or a list of strings if parsing fails. 895 """ 896 897 if not self.iRawDataPlus.HasInstrumentMethod: 898 raise ValueError( 899 "Raw Data file does not have any instrument methods attached" 900 ) 901 return None 902 else: 903 904 def parse_instrument_method(data): 905 lines = data.split("\r\n") 906 method = {} 907 current_section = None 908 sub_section = None 909 910 for line in lines: 911 if not line.strip(): # Skip empty lines 912 continue 913 if ( 914 line.startswith("----") 915 or line.endswith("Settings") 916 or line.endswith("Summary") 917 or line.startswith("Experiment") 918 or line.startswith("Scan Event") 919 ): 920 current_section = line.replace("-", "").strip() 921 method[current_section] = {} 922 sub_section = None 923 elif line.startswith("\t"): 924 if "\t\t" in line: 925 indent_level = line.count("\t") 926 key_value = line.strip() 927 928 if indent_level == 2: 929 if sub_section: 930 key, value = ( 931 key_value.split("=", 1) 932 if "=" in key_value 933 else (key_value, None) 934 ) 935 method[current_section][sub_section][ 936 key.strip() 937 ] = value.strip() if value else None 938 elif indent_level == 3: 939 scan_type, key_value = ( 940 key_value.split(" ", 1) 941 if " " in key_value 942 else (key_value, None) 943 ) 944 method.setdefault(current_section, {}).setdefault( 945 sub_section, {} 946 ).setdefault(scan_type, {}) 947 948 if key_value: 949 key, value = ( 950 key_value.split("=", 1) 951 if "=" in key_value 952 else (key_value, None) 953 ) 954 method[current_section][sub_section][scan_type][ 955 key.strip() 956 ] = value.strip() if value else None 957 else: 958 key_value = line.strip() 959 if "=" in key_value: 960 key, value = key_value.split("=", 1) 961 method.setdefault(current_section, {})[key.strip()] = ( 962 value.strip() 963 ) 964 else: 965 sub_section = key_value 966 else: 967 if ":" in line: 968 key, value = line.split(":", 1) 969 method[current_section][key.strip()] = value.strip() 970 else: 971 method[current_section][line] = {} 972 973 return method 974 975 count_instrument_methods = self.iRawDataPlus.InstrumentMethodsCount 976 # TODO make this code better... 977 instrument_methods = [] 978 for i in range(count_instrument_methods): 979 instrument_method_string = self.iRawDataPlus.GetInstrumentMethod(i) 980 if parse_strings: 981 try: 982 instrument_method_dict = parse_instrument_method( 983 instrument_method_string 984 ) 985 except: # if it fails for any reason 986 instrument_method_dict = instrument_method_string 987 else: 988 instrument_method_dict = instrument_method_string 989 instrument_methods.append(instrument_method_dict) 990 return instrument_methods 991 992 def get_tune_method(self): 993 """ 994 This code will extract the tune method from the raw file 995 It has been tested on data from a Thermo Orbitrap ID-X, Astral and Q-Exactive, but may fail on other instrument types. 996 It attempts to parse out section headers and sub-sections, but may not work for all instrument types. 997 It will also not return Labels (keys) where the value is blank 998 999 Returns: 1000 -------- 1001 Dict[str, Any] 1002 A dictionary containing the tune method information 1003 1004 Raises: 1005 ------- 1006 ValueError 1007 If no tune methods are found in the raw file 1008 1009 """ 1010 tunemethodcount = self.iRawDataPlus.GetTuneDataCount() 1011 if tunemethodcount == 0: 1012 raise ValueError("No tune methods found in the raw data file") 1013 return None 1014 elif tunemethodcount > 1: 1015 warnings.warn( 1016 "Multiple tune methods found in the raw data file, returning the 1st" 1017 ) 1018 1019 header = self.iRawDataPlus.GetTuneData(0) 1020 1021 header_dic = {} 1022 current_section = None 1023 1024 for i in range(header.Length): 1025 label = header.Labels[i] 1026 value = header.Values[i] 1027 1028 # Check for section headers 1029 if "===" in label or ( 1030 (value == "" or value is None) and not label.endswith(":") 1031 ): 1032 # This is a section header 1033 section_name = ( 1034 label.replace("=", "").replace(":", "").strip() 1035 ) # Clean the label if it contains '=' 1036 header_dic[section_name] = {} 1037 current_section = section_name 1038 else: 1039 if current_section: 1040 header_dic[current_section][label] = value 1041 else: 1042 header_dic[label] = value 1043 return header_dic 1044 1045 def get_status_log(self, retention_time: float = 0): 1046 """ 1047 This code will extract the status logs from the raw file 1048 It has been tested on data from a Thermo Orbitrap ID-X, Astral and Q-Exactive, but may fail on other instrument types. 1049 It attempts to parse out section headers and sub-sections, but may not work for all instrument types. 1050 It will also not return Labels (keys) where the value is blank 1051 1052 Parameters: 1053 ----------- 1054 retention_time: float 1055 The retention time in minutes to extract the status log data from. 1056 Will use the closest retention time found. Default 0. 1057 1058 Returns: 1059 -------- 1060 Dict[str, Any] 1061 A dictionary containing the status log information 1062 1063 Raises: 1064 ------- 1065 ValueError 1066 If no status logs are found in the raw file 1067 1068 """ 1069 tunemethodcount = self.iRawDataPlus.GetStatusLogEntriesCount() 1070 if tunemethodcount == 0: 1071 raise ValueError("No status logs found in the raw data file") 1072 return None 1073 1074 header = self.iRawDataPlus.GetStatusLogForRetentionTime(retention_time) 1075 1076 header_dic = {} 1077 current_section = None 1078 1079 for i in range(header.Length): 1080 label = header.Labels[i] 1081 value = header.Values[i] 1082 1083 # Check for section headers 1084 if "===" in label or ( 1085 (value == "" or value is None) and not label.endswith(":") 1086 ): 1087 # This is a section header 1088 section_name = ( 1089 label.replace("=", "").replace(":", "").strip() 1090 ) # Clean the label if it contains '=' 1091 header_dic[section_name] = {} 1092 current_section = section_name 1093 else: 1094 if current_section: 1095 header_dic[current_section][label] = value 1096 else: 1097 header_dic[label] = value 1098 return header_dic 1099 1100 def get_error_logs(self): 1101 """ 1102 This code will extract the error logs from the raw file 1103 1104 Returns: 1105 -------- 1106 Dict[float, str] 1107 A dictionary containing the error log information with the retention time as the key 1108 1109 Raises: 1110 ------- 1111 ValueError 1112 If no error logs are found in the raw file 1113 """ 1114 1115 error_log_count = self.iRawDataPlus.RunHeaderEx.ErrorLogCount 1116 if error_log_count == 0: 1117 raise ValueError("No error logs found in the raw data file") 1118 return None 1119 1120 error_logs = {} 1121 1122 for i in range(error_log_count): 1123 error_log_item = self.iRawDataPlus.GetErrorLogItem(i) 1124 rt = error_log_item.RetentionTime 1125 message = error_log_item.Message 1126 # Use the index `i` as the unique ID key 1127 error_logs[i] = {"rt": rt, "message": message} 1128 return error_logs 1129 1130 def get_sample_information(self): 1131 """ 1132 This code will extract the sample information from the raw file 1133 1134 Returns: 1135 -------- 1136 Dict[str, Any] 1137 A dictionary containing the sample information 1138 Note that UserText field may not be handled properly and may need further processing 1139 """ 1140 sminfo = self.iRawDataPlus.SampleInformation 1141 smdict = {} 1142 smdict["Comment"] = sminfo.Comment 1143 smdict["SampleId"] = sminfo.SampleId 1144 smdict["SampleName"] = sminfo.SampleName 1145 smdict["Vial"] = sminfo.Vial 1146 smdict["InjectionVolume"] = sminfo.InjectionVolume 1147 smdict["Barcode"] = sminfo.Barcode 1148 smdict["BarcodeStatus"] = str(sminfo.BarcodeStatus) 1149 smdict["CalibrationLevel"] = sminfo.CalibrationLevel 1150 smdict["DilutionFactor"] = sminfo.DilutionFactor 1151 smdict["InstrumentMethodFile"] = sminfo.InstrumentMethodFile 1152 smdict["RawFileName"] = sminfo.RawFileName 1153 smdict["CalibrationFile"] = sminfo.CalibrationFile 1154 smdict["IstdAmount"] = sminfo.IstdAmount 1155 smdict["RowNumber"] = sminfo.RowNumber 1156 smdict["Path"] = sminfo.Path 1157 smdict["ProcessingMethodFile"] = sminfo.ProcessingMethodFile 1158 smdict["SampleType"] = str(sminfo.SampleType) 1159 smdict["SampleWeight"] = sminfo.SampleWeight 1160 smdict["UserText"] = { 1161 "UserText": [x for x in sminfo.UserText] 1162 } # [0] #This may not work - needs debugging with 1163 return smdict 1164 1165 def get_instrument_data(self): 1166 """ 1167 This code will extract the instrument data from the raw file 1168 1169 Returns: 1170 -------- 1171 Dict[str, Any] 1172 A dictionary containing the instrument data 1173 """ 1174 instrument_data = self.iRawDataPlus.GetInstrumentData() 1175 id_dict = {} 1176 id_dict["Name"] = instrument_data.Name 1177 id_dict["Model"] = instrument_data.Model 1178 id_dict["SerialNumber"] = instrument_data.SerialNumber 1179 id_dict["SoftwareVersion"] = instrument_data.SoftwareVersion 1180 id_dict["HardwareVersion"] = instrument_data.HardwareVersion 1181 id_dict["ChannelLabels"] = { 1182 "ChannelLabels": [x for x in instrument_data.ChannelLabels] 1183 } 1184 id_dict["Flags"] = instrument_data.Flags 1185 id_dict["AxisLabelY"] = instrument_data.AxisLabelY 1186 id_dict["AxisLabelX"] = instrument_data.AxisLabelX 1187 return id_dict 1188 1189 def get_centroid_msms_data(self, scan): 1190 """ 1191 .. deprecated:: 2.0 1192 This function will be removed in CoreMS 2.0. Please use `get_average_mass_spectrum()` instead for similar functionality. 1193 """ 1194 1195 warnings.warn( 1196 "The `get_centroid_msms_data()` is deprecated as of CoreMS 2.0 and will be removed in a future version. " 1197 "Please use `get_average_mass_spectrum()` instead.", 1198 DeprecationWarning, 1199 ) 1200 1201 d_params = self.set_metadata(scans_list=[scan], label=Labels.thermo_centroid) 1202 1203 centroidStream = self.iRawDataPlus.GetCentroidStream(scan, False) 1204 1205 noise = list(centroidStream.Noises) 1206 1207 baselines = list(centroidStream.Baselines) 1208 1209 rp = list(centroidStream.Resolutions) 1210 1211 magnitude = list(centroidStream.Intensities) 1212 1213 mz = list(centroidStream.Masses) 1214 1215 # charge = scans_labels[5] 1216 array_noise_std = (np.array(noise) - np.array(baselines)) / 3 1217 l_signal_to_noise = np.array(magnitude) / array_noise_std 1218 1219 d_params["baseline_noise"] = np.average(array_noise_std) 1220 1221 d_params["baseline_noise_std"] = np.std(array_noise_std) 1222 1223 data_dict = { 1224 Labels.mz: mz, 1225 Labels.abundance: magnitude, 1226 Labels.rp: rp, 1227 Labels.s2n: list(l_signal_to_noise), 1228 } 1229 1230 mass_spec = MassSpecCentroid(data_dict, d_params, auto_process=False) 1231 mass_spec.settings.noise_threshold_method = "relative_abundance" 1232 mass_spec.settings.noise_threshold_min_relative_abundance = 1 1233 mass_spec.process_mass_spec() 1234 return mass_spec 1235 1236 def get_average_mass_spectrum_by_scanlist( 1237 self, 1238 scans_list: List[int], 1239 auto_process: bool = True, 1240 ppm_tolerance: float = 5.0, 1241 ) -> MassSpecProfile: 1242 """ 1243 Averages selected scans mass spectra using Thermo's AverageScans method 1244 scans_list: list[int] 1245 auto_process: bool 1246 If true performs peak picking, and noise threshold calculation after creation of mass spectrum object 1247 Returns: 1248 MassSpecProfile 1249 1250 .. deprecated:: 2.0 1251 This function will be removed in CoreMS 2.0. Please use `get_average_mass_spectrum()` instead for similar functionality. 1252 """ 1253 1254 warnings.warn( 1255 "The `get_average_mass_spectrum_by_scanlist()` is deprecated as of CoreMS 2.0 and will be removed in a future version. " 1256 "Please use `get_average_mass_spectrum()` instead.", 1257 DeprecationWarning, 1258 ) 1259 1260 d_params = self.set_metadata(scans_list=scans_list) 1261 1262 # assumes scans is full scan or reduced profile scan 1263 1264 scans = DotNetList[int]() 1265 for scan in scans_list: 1266 scans.Add(scan) 1267 1268 # Create the mass options object that will be used when averaging the scans 1269 options = MassOptions() 1270 options.ToleranceUnits = ToleranceUnits.ppm 1271 options.Tolerance = ppm_tolerance 1272 1273 # Get the scan filter for the first scan. This scan filter will be used to located 1274 # scans within the given scan range of the same type 1275 1276 averageScan = Extensions.AverageScans(self.iRawDataPlus, scans, options) 1277 1278 len_data = averageScan.SegmentedScan.Positions.Length 1279 1280 mz_list = list(averageScan.SegmentedScan.Positions) 1281 abund_list = list(averageScan.SegmentedScan.Intensities) 1282 1283 data_dict = { 1284 Labels.mz: mz_list, 1285 Labels.abundance: abund_list, 1286 } 1287 1288 mass_spec = MassSpecProfile(data_dict, d_params, auto_process=auto_process) 1289 1290 return mass_spec 1291 1292 1293class ImportMassSpectraThermoMSFileReader(ThermoBaseClass, SpectraParserInterface): 1294 """A class for parsing Thermo RAW mass spectrometry data files and instatiating MassSpectraBase or LCMSBase objects 1295 1296 Parameters 1297 ---------- 1298 file_location : str or Path 1299 The path to the RAW file to be parsed. 1300 analyzer : str, optional 1301 The type of mass analyzer used in the instrument. Default is "Unknown". 1302 instrument_label : str, optional 1303 The name of the instrument used to acquire the data. Default is "Unknown". 1304 sample_name : str, optional 1305 The name of the sample being analyzed. If not provided, the stem of the file_location path will be used. 1306 1307 Attributes 1308 ---------- 1309 file_location : Path 1310 The path to the RAW file being parsed. 1311 analyzer : str 1312 The type of mass analyzer used in the instrument. 1313 instrument_label : str 1314 The name of the instrument used to acquire the data. 1315 sample_name : str 1316 The name of the sample being analyzed. 1317 1318 Methods 1319 ------- 1320 * run(spectra=True). 1321 Parses the RAW file and returns a dictionary of mass spectra dataframes and a scan metadata dataframe. 1322 * get_mass_spectrum_from_scan(scan_number, polarity, auto_process=True) 1323 Parses the RAW file and returns a MassSpecBase object from a single scan. 1324 * get_mass_spectra_obj(). 1325 Parses the RAW file and instantiates a MassSpectraBase object. 1326 * get_lcms_obj(). 1327 Parses the RAW file and instantiates an LCMSBase object. 1328 * get_icr_transient_times(). 1329 Return a list for transient time targets for all scans, or selected scans range 1330 1331 Inherits from ThermoBaseClass and SpectraParserInterface 1332 """ 1333 1334 def __init__( 1335 self, 1336 file_location, 1337 analyzer="Unknown", 1338 instrument_label="Unknown", 1339 sample_name=None, 1340 ): 1341 super().__init__(file_location) 1342 if isinstance(file_location, str): 1343 # if obj is a string it defaults to create a Path obj, pass the S3Path if needed 1344 file_location = Path(file_location) 1345 if not file_location.exists(): 1346 raise FileExistsError("File does not exist: " + str(file_location)) 1347 1348 self.file_location = file_location 1349 self.analyzer = analyzer 1350 self.instrument_label = instrument_label 1351 1352 if sample_name: 1353 self.sample_name = sample_name 1354 else: 1355 self.sample_name = file_location.stem 1356 1357 def load(self): 1358 pass 1359 1360 def get_scans_in_time_range( 1361 self, 1362 time_range: Union[Tuple[float, float], List[Tuple[float, float]]], 1363 ms_level: Optional[int] = None, 1364 ) -> List[int]: 1365 """Return scan numbers within specified retention time range(s). 1366 1367 Parameters 1368 ---------- 1369 time_range : tuple or list of tuples 1370 Retention time range(s) in minutes. Can be: 1371 - Single range: (start_time, end_time) 1372 - Multiple ranges: [(start1, end1), (start2, end2), ...] 1373 ms_level : int, optional 1374 If specified, only return scans of this MS level (e.g., 1 for MS1, 2 for MS2). 1375 If None, returns scans of all MS levels. 1376 1377 Returns 1378 ------- 1379 list of int 1380 List of scan numbers within the specified time range(s) and MS level. 1381 """ 1382 # Normalize time range to list of tuples 1383 time_ranges = self._normalize_time_range(time_range) 1384 1385 # Get all scan data 1386 scan_df = self.get_scan_df() 1387 1388 # Filter by time range 1389 mask = pd.Series([False] * len(scan_df), index=scan_df.index) 1390 for start_time, end_time in time_ranges: 1391 mask |= (scan_df.scan_time >= start_time) & (scan_df.scan_time <= end_time) 1392 1393 filtered_df = scan_df[mask] 1394 1395 # Filter by MS level if specified 1396 if ms_level is not None: 1397 filtered_df = filtered_df[filtered_df.ms_level == ms_level] 1398 1399 return filtered_df.scan.tolist() 1400 1401 def get_scan_df( 1402 self, 1403 time_range: Optional[ 1404 Union[Tuple[float, float], List[Tuple[float, float]]] 1405 ] = None, 1406 ): 1407 """Return scan data as a pandas DataFrame. 1408 1409 Parameters 1410 ---------- 1411 time_range : tuple or list of tuples, optional 1412 Retention time range(s) to filter scans. Can be: 1413 - Single range: (start_time, end_time) in minutes 1414 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 1415 If None, returns all scans. 1416 1417 Returns 1418 ------- 1419 pd.DataFrame 1420 DataFrame containing scan information, optionally filtered by time range. 1421 """ 1422 # This automatically brings in all the data 1423 self.chromatogram_settings.scans = (-1, -1) 1424 1425 # Get scan df info; starting with TIC data 1426 tic_data, _ = self.get_tic(ms_type="all", peak_detection=False, smooth=False) 1427 tic_data = { 1428 "scan": tic_data.scans, 1429 "scan_time": tic_data.time, 1430 "tic": tic_data.tic, 1431 } 1432 scan_df = pd.DataFrame.from_dict(tic_data) 1433 scan_df["ms_level"] = None 1434 1435 # get scan text 1436 scan_filter_df = pd.DataFrame.from_dict( 1437 self.get_all_filters()[0], orient="index" 1438 ) 1439 scan_filter_df.reset_index(inplace=True) 1440 scan_filter_df.rename(columns={"index": "scan", 0: "scan_text"}, inplace=True) 1441 1442 scan_df = scan_df.merge(scan_filter_df, on="scan", how="left") 1443 scan_df["scan_window_lower"] = scan_df.scan_text.str.extract( 1444 r"\[(\d+\.\d+)-\d+\.\d+\]" 1445 ) 1446 scan_df["scan_window_upper"] = scan_df.scan_text.str.extract( 1447 r"\[\d+\.\d+-(\d+\.\d+)\]" 1448 ) 1449 scan_df["polarity"] = np.where( 1450 scan_df.scan_text.str.contains(" - "), "negative", "positive" 1451 ) 1452 scan_df["precursor_mz"] = scan_df.scan_text.str.extract(r"(\d+\.\d+)@") 1453 scan_df["precursor_mz"] = scan_df["precursor_mz"].astype(float) 1454 1455 # Assign each scan as centroid or profile and add ms_level 1456 scan_df["ms_format"] = None 1457 for i in scan_df.scan.to_list(): 1458 scan_df.loc[scan_df.scan == i, "ms_level"] = self.get_ms_level_for_scan_num( 1459 i 1460 ) 1461 if self.iRawDataPlus.IsCentroidScanFromScanNumber(i): 1462 scan_df.loc[scan_df.scan == i, "ms_format"] = "centroid" 1463 else: 1464 scan_df.loc[scan_df.scan == i, "ms_format"] = "profile" 1465 1466 # Remove any non-mass spectra scans (e.g., MS level 0 or None) 1467 scan_df = scan_df[ 1468 scan_df.ms_level.notnull() & (scan_df.ms_level > 0) 1469 ].reset_index(drop=True) 1470 1471 # Filter by time range if specified 1472 if time_range is not None: 1473 time_ranges = self._normalize_time_range(time_range) 1474 mask = pd.Series([False] * len(scan_df), index=scan_df.index) 1475 for start_time, end_time in time_ranges: 1476 mask |= (scan_df.scan_time >= start_time) & ( 1477 scan_df.scan_time <= end_time 1478 ) 1479 scan_df = scan_df[mask].reset_index(drop=True) 1480 1481 return scan_df 1482 1483 def get_ms_raw( 1484 self, 1485 spectra, 1486 scan_df, 1487 time_range: Optional[ 1488 Union[Tuple[float, float], List[Tuple[float, float]]] 1489 ] = None, 1490 ): 1491 """Return a dictionary of mass spectra data as pandas DataFrames. 1492 1493 Parameters 1494 ---------- 1495 spectra : str 1496 Specifies which spectra to load (e.g., 'all', 'ms1', 'ms2') 1497 scan_df : pd.DataFrame 1498 Scan information DataFrame 1499 time_range : tuple or list of tuples, optional 1500 Retention time range(s) to filter scans. Can be: 1501 - Single range: (start_time, end_time) in minutes 1502 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 1503 If None, returns all scans. Note: filtering is typically done at scan_df level. 1504 1505 Returns 1506 ------- 1507 dict 1508 Dictionary of raw mass spectra data, optionally filtered by time range. 1509 """ 1510 # Note: time_range filtering is handled at the scan_df level before calling this method 1511 # The parameter is here for interface consistency with SpectraParserInterface 1512 1513 if spectra == "all": 1514 scan_df_forspec = scan_df 1515 elif spectra == "ms1": 1516 scan_df_forspec = scan_df[scan_df.ms_level == 1] 1517 elif spectra == "ms2": 1518 scan_df_forspec = scan_df[scan_df.ms_level == 2] 1519 else: 1520 raise ValueError("spectra must be 'none', 'all', 'ms1', or 'ms2'") 1521 1522 # Result container 1523 res = {} 1524 1525 # Row count container 1526 counter = {} 1527 1528 # Column name container 1529 cols = {} 1530 1531 # set at float32 1532 dtype = np.float32 1533 1534 # First pass: get nrows 1535 N = defaultdict(lambda: 0) 1536 for i in scan_df_forspec.scan.to_list(): 1537 level = scan_df_forspec.loc[scan_df_forspec.scan == i, "ms_level"].values[0] 1538 scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber(i) 1539 profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber( 1540 i, scanStatistics 1541 ) 1542 abun = list(profileStream.Intensities) 1543 abun = np.array(abun)[np.where(np.array(abun) > 0)[0]] 1544 1545 N[level] += len(abun) 1546 1547 # Second pass: parse 1548 for i in scan_df_forspec.scan.to_list(): 1549 scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber(i) 1550 profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber( 1551 i, scanStatistics 1552 ) 1553 abun = list(profileStream.Intensities) 1554 mz = list(profileStream.Positions) 1555 1556 # Get index of abun that are > 0 1557 inx = np.where(np.array(abun) > 0)[0] 1558 mz = np.array(mz)[inx] 1559 mz = np.float32(mz) 1560 abun = np.array(abun)[inx] 1561 abun = np.float32(abun) 1562 1563 level = scan_df_forspec.loc[scan_df_forspec.scan == i, "ms_level"].values[0] 1564 1565 # Number of rows 1566 n = len(mz) 1567 1568 # No measurements 1569 if n == 0: 1570 continue 1571 1572 # Dimension check 1573 if len(mz) != len(abun): 1574 warnings.warn("m/z and intensity array dimension mismatch") 1575 continue 1576 1577 # Scan/frame info 1578 id_dict = i 1579 1580 # Columns 1581 cols[level] = ["scan", "mz", "intensity"] 1582 m = len(cols[level]) 1583 1584 # Subarray init 1585 arr = np.empty((n, m), dtype=dtype) 1586 inx = 0 1587 1588 # Populate scan/frame info 1589 arr[:, inx] = i 1590 inx += 1 1591 1592 # Populate m/z 1593 arr[:, inx] = mz 1594 inx += 1 1595 1596 # Populate intensity 1597 arr[:, inx] = abun 1598 inx += 1 1599 1600 # Initialize output container 1601 if level not in res: 1602 res[level] = np.empty((N[level], m), dtype=dtype) 1603 counter[level] = 0 1604 1605 # Insert subarray 1606 res[level][counter[level] : counter[level] + n, :] = arr 1607 counter[level] += n 1608 1609 # Construct ms1 and ms2 mz dataframes 1610 for level in res.keys(): 1611 res[level] = pd.DataFrame(res[level]) 1612 res[level].columns = cols[level] 1613 # rename keys in res to add 'ms' prefix 1614 res = {f"ms{key}": value for key, value in res.items()} 1615 1616 return res 1617 1618 def run( 1619 self, 1620 spectra="all", 1621 scan_df=None, 1622 time_range: Optional[ 1623 Union[Tuple[float, float], List[Tuple[float, float]]] 1624 ] = None, 1625 ): 1626 """ 1627 Extracts mass spectra data from a raw file. 1628 1629 Parameters 1630 ---------- 1631 spectra : str, optional 1632 Which mass spectra data to include in the output. Default is all. Other options: none, ms1, ms2. 1633 scan_df : pandas.DataFrame, optional 1634 Scan dataframe. If not provided, the scan dataframe is created from the mzML file. 1635 time_range : tuple or list of tuples, optional 1636 Retention time range(s) to filter scans. Can be: 1637 - Single range: (start_time, end_time) in minutes 1638 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 1639 If None, returns all scans. 1640 1641 Returns 1642 ------- 1643 tuple 1644 A tuple containing two elements: 1645 - A dictionary containing mass spectra data, separated by MS level. 1646 - A pandas DataFrame containing scan information, including scan number, scan time, TIC, MS level, 1647 scan text, scan window lower and upper bounds, polarity, and precursor m/z (if applicable). 1648 """ 1649 # Prepare scan_df 1650 if scan_df is None: 1651 scan_df = self.get_scan_df(time_range=time_range) 1652 1653 # Prepare mass spectra data 1654 if spectra != "none": 1655 res = self.get_ms_raw( 1656 spectra=spectra, scan_df=scan_df, time_range=time_range 1657 ) 1658 else: 1659 res = None 1660 1661 return res, scan_df 1662 1663 def get_mass_spectra_from_scan_list( 1664 self, scan_list, spectrum_mode, auto_process=True 1665 ): 1666 """Instantiate multiple MassSpecBase objects from a list of scan numbers from the binary file. 1667 1668 Parameters 1669 ---------- 1670 scan_list : list[int] 1671 A list of scan numbers to extract the mass spectra from. 1672 spectrum_mode : str 1673 The type of mass spectrum to extract. Must be 'profile' or 'centroid'. 1674 All scans in the list must be of the same type. 1675 auto_process : bool, optional 1676 If True, perform peak picking and noise threshold calculation after creating the mass spectrum object. Default is True. 1677 """ 1678 mass_spectra = [] 1679 for scan in scan_list: 1680 mass_spectrum = self.get_mass_spectrum_from_scan( 1681 scan, spectrum_mode, auto_process=auto_process 1682 ) 1683 mass_spectra.append(mass_spectrum) 1684 1685 return mass_spectra 1686 1687 def get_mass_spectrum_from_scan( 1688 self, scan_number, spectrum_mode, auto_process=True 1689 ): 1690 """Instantiate a MassSpecBase object from a single scan number from the binary file. 1691 1692 Parameters 1693 ---------- 1694 scan_number : int 1695 The scan number to extract the mass spectrum from. 1696 polarity : int 1697 The polarity of the scan. 1 for positive mode, -1 for negative mode. 1698 spectrum_mode : str 1699 The type of mass spectrum to extract. Must be 'profile' or 'centroid'. 1700 auto_process : bool, optional 1701 If True, perform peak picking and noise threshold calculation after creating the mass spectrum object. Default is True. 1702 1703 Returns 1704 ------- 1705 MassSpecProfile | MassSpecCentroid 1706 The MassSpecProfile or MassSpecCentroid object containing the parsed mass spectrum. 1707 """ 1708 1709 if spectrum_mode == "profile": 1710 scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber(scan_number) 1711 profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber( 1712 scan_number, scanStatistics 1713 ) 1714 abun = list(profileStream.Intensities) 1715 mz = list(profileStream.Positions) 1716 data_dict = { 1717 Labels.mz: mz, 1718 Labels.abundance: abun, 1719 } 1720 d_params = self.set_metadata( 1721 firstScanNumber=scan_number, 1722 lastScanNumber=scan_number, 1723 scans_list=False, 1724 label=Labels.thermo_profile, 1725 ) 1726 mass_spectrum_obj = MassSpecProfile( 1727 data_dict, d_params, auto_process=auto_process 1728 ) 1729 1730 elif spectrum_mode == "centroid": 1731 centroid_scan = self.iRawDataPlus.GetCentroidStream(scan_number, False) 1732 if centroid_scan.Masses is not None: 1733 mz = list(centroid_scan.Masses) 1734 abun = list(centroid_scan.Intensities) 1735 rp = list(centroid_scan.Resolutions) 1736 magnitude = list(centroid_scan.Intensities) 1737 noise = list(centroid_scan.Noises) 1738 baselines = list(centroid_scan.Baselines) 1739 array_noise_std = (np.array(noise) - np.array(baselines)) / 3 1740 l_signal_to_noise = np.array(magnitude) / array_noise_std 1741 data_dict = { 1742 Labels.mz: mz, 1743 Labels.abundance: abun, 1744 Labels.rp: rp, 1745 Labels.s2n: list(l_signal_to_noise), 1746 } 1747 else: # For CID MS2, the centroid data are stored in the profile data location, they do not have any associated rp or baseline data, but they should be treated as centroid data 1748 scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber( 1749 scan_number 1750 ) 1751 profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber( 1752 scan_number, scanStatistics 1753 ) 1754 abun = list(profileStream.Intensities) 1755 mz = list(profileStream.Positions) 1756 data_dict = { 1757 Labels.mz: mz, 1758 Labels.abundance: abun, 1759 Labels.rp: [np.nan] * len(mz), 1760 Labels.s2n: [np.nan] * len(mz), 1761 } 1762 d_params = self.set_metadata( 1763 firstScanNumber=scan_number, 1764 lastScanNumber=scan_number, 1765 scans_list=False, 1766 label=Labels.thermo_centroid, 1767 ) 1768 mass_spectrum_obj = MassSpecCentroid( 1769 data_dict, d_params, auto_process=auto_process 1770 ) 1771 1772 return mass_spectrum_obj 1773 1774 def get_mass_spectra_obj( 1775 self, 1776 time_range: Optional[ 1777 Union[Tuple[float, float], List[Tuple[float, float]]] 1778 ] = None, 1779 ): 1780 """Instatiate a MassSpectraBase object from the binary data file file. 1781 1782 Parameters 1783 ---------- 1784 time_range : tuple or list of tuples, optional 1785 Retention time range(s) to load. Can be: 1786 - Single range: (start_time, end_time) in minutes 1787 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 1788 If None, loads all scans. Useful for targeted workflows to improve performance. 1789 1790 Returns 1791 ------- 1792 MassSpectraBase 1793 The MassSpectra object containing the parsed mass spectra. The object is instatiated with the mzML file, analyzer, instrument, sample name, and scan dataframe. 1794 """ 1795 _, scan_df = self.run(spectra="none", time_range=time_range) 1796 mass_spectra_obj = MassSpectraBase( 1797 self.file_location, 1798 self.analyzer, 1799 self.instrument_label, 1800 self.sample_name, 1801 self, 1802 ) 1803 scan_df = scan_df.set_index("scan", drop=False) 1804 mass_spectra_obj.scan_df = scan_df 1805 1806 return mass_spectra_obj 1807 1808 def get_lcms_obj( 1809 self, 1810 spectra="all", 1811 time_range: Optional[ 1812 Union[Tuple[float, float], List[Tuple[float, float]]] 1813 ] = None, 1814 ): 1815 """Instatiates a LCMSBase object from the mzML file. 1816 1817 Parameters 1818 ---------- 1819 spectra : str, optional 1820 Which mass spectra data to include in the output. Default is "all". Other options: "none", "ms1", "ms2". 1821 time_range : tuple or list of tuples, optional 1822 Retention time range(s) to load. Can be: 1823 - Single range: (start_time, end_time) in minutes 1824 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 1825 If None, loads all scans. Useful for targeted workflows to improve performance. 1826 1827 Returns 1828 ------- 1829 LCMSBase 1830 LCMS object containing mass spectra data. The object is instatiated with the file location, analyzer, instrument, sample name, scan info, mz dataframe (as specifified), polarity, as well as the attributes holding the scans, retention times, and tics. 1831 """ 1832 _, scan_df = self.run( 1833 spectra="none", time_range=time_range 1834 ) # first run it to just get scan info 1835 res, scan_df = self.run( 1836 scan_df=scan_df, spectra=spectra, time_range=time_range 1837 ) # second run to parse data 1838 lcms_obj = LCMSBase( 1839 self.file_location, 1840 self.analyzer, 1841 self.instrument_label, 1842 self.sample_name, 1843 self, 1844 ) 1845 if spectra != "none": 1846 for key in res: 1847 key_int = int(key.replace("ms", "")) 1848 res[key] = res[key][res[key].intensity > 0] 1849 res[key] = ( 1850 res[key].sort_values(by=["scan", "mz"]).reset_index(drop=True) 1851 ) 1852 lcms_obj._ms_unprocessed[key_int] = res[key] 1853 lcms_obj.scan_df = scan_df.set_index("scan", drop=False) 1854 # Check if polarity is mixed 1855 if len(set(scan_df.polarity)) > 1: 1856 raise ValueError("Mixed polarities detected in scan data") 1857 lcms_obj.polarity = scan_df.polarity.iloc[0] 1858 lcms_obj._scans_number_list = list(scan_df.scan) 1859 lcms_obj._retention_time_list = list(scan_df.scan_time) 1860 lcms_obj._tic_list = list(scan_df.tic) 1861 1862 return lcms_obj 1863 1864 def get_icr_transient_times(self): 1865 """Return a list for transient time targets for all scans, or selected scans range 1866 1867 Notes 1868 -------- 1869 Resolving Power and Transient time targets based on 7T FT-ICR MS system 1870 """ 1871 1872 res_trans_time = { 1873 "50": 0.384, 1874 "100000": 0.768, 1875 "200000": 1.536, 1876 "400000": 3.072, 1877 "750000": 6.144, 1878 "1000000": 12.288, 1879 } 1880 1881 firstScanNumber = self.start_scan 1882 1883 lastScanNumber = self.end_scan 1884 1885 transient_time_list = [] 1886 1887 for scan in range(firstScanNumber, lastScanNumber): 1888 scan_header = self.get_scan_header(scan) 1889 1890 rp_target = scan_header["FT Resolution:"] 1891 1892 transient_time = res_trans_time.get(rp_target) 1893 1894 transient_time_list.append(transient_time) 1895 1896 # print(transient_time, rp_target) 1897 1898 return transient_time_list
59class ThermoBaseClass: 60 """Class for parsing Thermo Raw files and extracting information from them. 61 62 Parameters: 63 ----------- 64 file_location : str or pathlib.Path or s3path.S3Path 65 Thermo Raw file path or S3 path. 66 67 Attributes: 68 ----------- 69 file_path : str or pathlib.Path or s3path.S3Path 70 The file path of the Thermo Raw file. 71 parameters : LCMSParameters 72 The LCMS parameters for the Thermo Raw file. 73 chromatogram_settings : LiquidChromatographSetting 74 The chromatogram settings for the Thermo Raw file. 75 scans : list or tuple 76 The selected scans for the Thermo Raw file. 77 start_scan : int 78 The starting scan number for the Thermo Raw file. 79 end_scan : int 80 The ending scan number for the Thermo Raw file. 81 82 Methods: 83 -------- 84 * set_msordertype(scanFilter, mstype: str = 'ms1') -> scanFilter 85 Convert the user-passed MS Type string to a Thermo MSOrderType object. 86 * get_instrument_info() -> dict 87 Get the instrument information from the Thermo Raw file. 88 * get_creation_time() -> datetime.datetime 89 Extract the creation date stamp from the .RAW file and return it as a formatted datetime object. 90 * remove_temp_file() 91 Remove the temporary file if the path is from S3Path. 92 * get_polarity_mode(scan_number: int) -> int 93 Get the polarity mode for the given scan number. 94 * get_filter_for_scan_num(scan_number: int) -> List[str] 95 Get the filter for the given scan number. 96 * check_full_scan(scan_number: int) -> bool 97 Check if the given scan number is a full scan. 98 * get_all_filters() -> Tuple[Dict[int, str], List[str]] 99 Get all scan filters for the Thermo Raw file. 100 * get_scan_header(scan: int) -> Dict[str, Any] 101 Get the full dictionary of scan header metadata for the given scan number. 102 * get_rt_time_from_trace(trace) -> Tuple[List[float], List[float], List[int]] 103 Get the retention time, intensity, and scan number from the given trace. 104 * get_eics(target_mzs: List[float], tic_data: Dict[str, Any], ms_type: str = 'MS !d', 105 peak_detection: bool = True, smooth: bool = True, plot: bool = False, 106 ax: Optional[matplotlib.axes.Axes] = None, legend: bool = False) -> Tuple[Dict[float, EIC_Data], matplotlib.axes.Axes] 107 Get the extracted ion chromatograms (EICs) for the target m/z values. 108 109 """ 110 111 def __init__(self, file_location): 112 """file_location: srt pathlib.Path or s3path.S3Path 113 Thermo Raw file path 114 """ 115 # Thread.__init__(self) 116 if isinstance(file_location, str): 117 file_path = Path(file_location) 118 119 elif isinstance(file_location, S3Path): 120 temp_dir = Path("tmp/") 121 temp_dir.mkdir(exist_ok=True) 122 123 file_path = temp_dir / file_location.name 124 with open(file_path, "wb") as fh: 125 fh.write(file_location.read_bytes()) 126 127 else: 128 file_path = file_location 129 130 self.iRawDataPlus = RawFileReaderAdapter.FileFactory(str(file_path)) 131 self.iFileHeader = None 132 133 if not self.iRawDataPlus.IsOpen: 134 self.close_file() 135 raise FileNotFoundError( 136 "Unable to access the RAW file using the RawFileReader class!" 137 ) 138 139 # Check for any errors in the RAW file 140 if self.iRawDataPlus.IsError: 141 file_error = self.iRawDataPlus.FileError 142 self.close_file() 143 raise IOError("Error opening ({}) - {}".format(file_error, file_path)) 144 145 self.res = self.iRawDataPlus.SelectInstrument(Device.MS, 1) 146 147 self.file_path = file_location 148 self.iFileHeader = FileHeaderReaderFactory.ReadFile(str(file_path)) 149 150 # removing tmp file 151 152 self._init_settings() 153 154 def _init_settings(self): 155 """ 156 Initialize the LCMSParameters object. 157 """ 158 self._parameters = LCMSParameters() 159 160 @property 161 def parameters(self) -> LCMSParameters: 162 """ 163 Get or set the LCMSParameters object. 164 """ 165 return self._parameters 166 167 @parameters.setter 168 def parameters(self, instance_LCMSParameters: LCMSParameters): 169 self._parameters = instance_LCMSParameters 170 171 @property 172 def chromatogram_settings(self) -> LiquidChromatographSetting: 173 """ 174 Get or set the LiquidChromatographSetting object. 175 """ 176 return self.parameters.lc_ms 177 178 @chromatogram_settings.setter 179 def chromatogram_settings( 180 self, instance_LiquidChromatographSetting: LiquidChromatographSetting 181 ): 182 self.parameters.lc_ms = instance_LiquidChromatographSetting 183 184 @property 185 def scans(self) -> list | tuple: 186 """scans : list or tuple 187 If list uses Thermo AverageScansInScanRange for selected scans, ortherwise uses Thermo AverageScans for a scan range 188 """ 189 return self.chromatogram_settings.scans 190 191 @property 192 def start_scan(self) -> int: 193 """ 194 Get the starting scan number for the Thermo Raw file. 195 """ 196 if self.scans[0] == -1: 197 return self.iRawDataPlus.RunHeaderEx.FirstSpectrum 198 else: 199 return self.scans[0] 200 201 @property 202 def end_scan(self) -> int: 203 """ 204 Get the ending scan number for the Thermo Raw file. 205 """ 206 if self.scans[-1] == -1: 207 return self.iRawDataPlus.RunHeaderEx.LastSpectrum 208 else: 209 return self.scans[-1] 210 211 def set_msordertype(self, scanFilter, mstype: str = "ms1"): 212 """ 213 Function to convert user passed string MS Type to Thermo MSOrderType object 214 Limited to MS1 through MS10. 215 216 Parameters: 217 ----------- 218 scanFilter : Thermo.ScanFilter 219 The scan filter object. 220 mstype : str, optional 221 The MS Type string, by default 'ms1' 222 223 """ 224 mstype = mstype.upper() 225 # Check that a valid mstype is passed 226 if (int(mstype.split("MS")[1]) > 10) or (int(mstype.split("MS")[1]) < 1): 227 warn("MS Type not valid, must be between MS1 and MS10") 228 229 msordertypedict = { 230 "MS1": MSOrderType.Ms, 231 "MS2": MSOrderType.Ms2, 232 "MS3": MSOrderType.Ms3, 233 "MS4": MSOrderType.Ms4, 234 "MS5": MSOrderType.Ms5, 235 "MS6": MSOrderType.Ms6, 236 "MS7": MSOrderType.Ms7, 237 "MS8": MSOrderType.Ms8, 238 "MS9": MSOrderType.Ms9, 239 "MS10": MSOrderType.Ms10, 240 } 241 scanFilter.MSOrder = msordertypedict[mstype] 242 return scanFilter 243 244 def get_instrument_info(self) -> dict: 245 """ 246 Get the instrument information from the Thermo Raw file. 247 248 Returns: 249 -------- 250 dict 251 A dictionary with the keys 'model', and 'serial_number'. 252 """ 253 instrumentData = self.iRawDataPlus.GetInstrumentData() 254 return { 255 "model": instrumentData.Model, 256 "serial_number": instrumentData.SerialNumber, 257 } 258 259 def get_creation_time(self) -> datetime.datetime: 260 """ 261 Extract the creation date stamp from the .RAW file 262 Return formatted creation date stamp. 263 264 """ 265 credate = self.iRawDataPlus.CreationDate.get_Ticks() 266 credate = datetime.datetime(1, 1, 1) + datetime.timedelta( 267 microseconds=credate / 10 268 ) 269 return credate 270 271 def remove_temp_file(self) -> None: 272 """if the path is from S3Path data cannot be serialized to io.ByteStream and 273 a temporary copy is stored at the temp dir 274 use this function only at the end of your execution scrip 275 some LCMS class methods depend on this file 276 """ 277 278 self.file_path.unlink() 279 280 def close_file(self) -> None: 281 """Dispose the Thermo file handles. 282 283 A second call is a no-op. This does not delete an S3 temp copy; 284 ``remove_temp_file`` does that. 285 """ 286 for attr in ("iRawDataPlus", "iFileHeader"): 287 handle = getattr(self, attr, None) 288 if handle is None: 289 continue 290 dispose = getattr(handle, "Dispose", None) 291 setattr(self, attr, None) 292 if callable(dispose): 293 dispose() 294 295 def __del__(self): 296 try: 297 self.close_file() 298 except Exception: 299 return 300 301 def get_polarity_mode(self, scan_number: int) -> int: 302 """ 303 Get the polarity mode for the given scan number. 304 305 Parameters: 306 ----------- 307 scan_number : int 308 The scan number. 309 310 Raises: 311 ------- 312 Exception 313 If the polarity mode is unknown. 314 315 """ 316 polarity_symbol = self.get_filter_for_scan_num(scan_number)[1] 317 318 if polarity_symbol == "+": 319 return 1 320 # return 'POSITIVE_ION_MODE' 321 322 elif polarity_symbol == "-": 323 return -1 324 325 else: 326 raise Exception("Polarity Mode Unknown, please set it manually") 327 328 def get_filter_for_scan_num(self, scan_number: int) -> List[str]: 329 """ 330 Returns the closest matching run time that corresponds to scan_number for the current 331 controller. This function is only supported for MS device controllers. 332 e.g. ['FTMS', '-', 'p', 'NSI', 'Full', 'ms', '[200.00-1000.00]'] 333 334 Parameters: 335 ----------- 336 scan_number : int 337 The scan number. 338 339 """ 340 scan_label = self.iRawDataPlus.GetScanEventStringForScanNumber(scan_number) 341 342 return str(scan_label).split() 343 344 def get_ms_level_for_scan_num(self, scan_number: int) -> str: 345 """ 346 Get the MS order for the given scan number. 347 348 Parameters: 349 ----------- 350 scan_number : int 351 The scan number 352 353 Returns: 354 -------- 355 int 356 The MS order type (1 for MS, 2 for MS2, etc.) 357 """ 358 scan_filter = self.iRawDataPlus.GetFilterForScanNumber(scan_number) 359 360 msordertype = { 361 MSOrderType.Ms: 1, 362 MSOrderType.Ms2: 2, 363 MSOrderType.Ms3: 3, 364 MSOrderType.Ms4: 4, 365 MSOrderType.Ms5: 5, 366 MSOrderType.Ms6: 6, 367 MSOrderType.Ms7: 7, 368 MSOrderType.Ms8: 8, 369 MSOrderType.Ms9: 9, 370 MSOrderType.Ms10: 10, 371 } 372 373 if scan_filter.MSOrder in msordertype: 374 return msordertype[scan_filter.MSOrder] 375 else: 376 raise Exception("MS Order Type not found") 377 378 def check_full_scan(self, scan_number: int) -> bool: 379 # scan_filter.ScanMode 0 = FULL 380 scan_filter = self.iRawDataPlus.GetFilterForScanNumber(scan_number) 381 382 return scan_filter.ScanMode == MSOrderType.Ms 383 384 def get_all_filters(self) -> Tuple[Dict[int, str], List[str]]: 385 """ 386 Get all scan filters. 387 This function is only supported for MS device controllers. 388 e.g. ['FTMS', '-', 'p', 'NSI', 'Full', 'ms', '[200.00-1000.00]'] 389 390 """ 391 392 scanrange = range(self.start_scan, self.end_scan + 1) 393 scanfiltersdic = {} 394 scanfilterslist = [] 395 for scan_number in scanrange: 396 scan_label = self.iRawDataPlus.GetScanEventStringForScanNumber(scan_number) 397 scanfiltersdic[scan_number] = scan_label 398 scanfilterslist.append(scan_label) 399 scanfilterset = list(set(scanfilterslist)) 400 return scanfiltersdic, scanfilterset 401 402 def get_scan_header(self, scan: int) -> Dict[str, Any]: 403 """ 404 Get full dictionary of scan header meta data, i.e. AGC status, ion injection time, etc. 405 406 Parameters: 407 ----------- 408 scan : int 409 The scan number. 410 411 """ 412 header = self.iRawDataPlus.GetTrailerExtraInformation(scan) 413 414 header_dic = {} 415 for i in range(header.Length): 416 header_dic.update({header.Labels[i]: header.Values[i]}) 417 return header_dic 418 419 @staticmethod 420 def get_rt_time_from_trace(trace) -> Tuple[List[float], List[float], List[int]]: 421 """trace: ThermoFisher.CommonCore.Data.Business.ChromatogramSignal""" 422 return list(trace.Times), list(trace.Intensities), list(trace.Scans) 423 424 def get_eics( 425 self, 426 target_mzs: List[float], 427 tic_data: Dict[str, Any], 428 ms_type="MS !d", 429 peak_detection=False, 430 smooth=False, 431 plot=False, 432 ax: Optional[axes.Axes] = None, 433 legend=False, 434 ) -> Tuple[Dict[float, EIC_Data], axes.Axes]: 435 """ms_type: str ('MS', MS2') 436 start_scan: int default -1 will select the lowest available 437 end_scan: int default -1 will select the highest available 438 439 returns: 440 441 chroma: dict{target_mz: EIC_Data( 442 Scans: [int] 443 original thermo scan numbers 444 Time: [floats] 445 list of retention times 446 TIC: [floats] 447 total ion chromatogram 448 Apexes: [int] 449 original thermo apex scan number after peak picking 450 ) 451 452 """ 453 # If peak_detection or smooth is True, raise exception 454 if peak_detection or smooth: 455 raise Exception( 456 "Peak detection and smoothing are no longer implemented in this function" 457 ) 458 459 options = MassOptions() 460 options.ToleranceUnits = ToleranceUnits.ppm 461 options.Tolerance = self.chromatogram_settings.eic_tolerance_ppm 462 463 all_chroma_settings = [] 464 465 for target_mz in target_mzs: 466 settings = ChromatogramTraceSettings(TraceType.MassRange) 467 settings.Filter = ms_type 468 settings.MassRanges = [Range(target_mz, target_mz)] 469 470 chroma_settings = IChromatogramSettings(settings) 471 472 all_chroma_settings.append(chroma_settings) 473 474 # chroma_settings2 = IChromatogramSettings(settings) 475 # print(chroma_settings.FragmentMass) 476 # print(chroma_settings.FragmentMass) 477 # print(chroma_settings) 478 # print(chroma_settings) 479 480 data = self.iRawDataPlus.GetChromatogramData( 481 all_chroma_settings, self.start_scan, self.end_scan, options 482 ) 483 484 traces = ChromatogramSignal.FromChromatogramData(data) 485 486 chroma = {} 487 488 if plot: 489 from matplotlib.transforms import Bbox 490 import matplotlib.pyplot as plt 491 492 if not ax: 493 # ax = plt.gca() 494 # ax.clear() 495 fig, ax = plt.subplots() 496 497 else: 498 fig = plt.gcf() 499 500 # plt.show() 501 502 for i, trace in enumerate(traces): 503 if trace.Length > 0: 504 rt, eic, scans = self.get_rt_time_from_trace(trace) 505 if smooth: 506 eic = self.smooth_tic(eic) 507 508 chroma[target_mzs[i]] = EIC_Data(scans=scans, time=rt, eic=eic) 509 if plot: 510 ax.plot(rt, eic, label="{:.5f}".format(target_mzs[i])) 511 512 if peak_detection: 513 # max_eic = self.get_max_eic(chroma) 514 max_signal = max(tic_data.tic) 515 516 for eic_data in chroma.values(): 517 eic = eic_data.eic 518 time = eic_data.time 519 520 if len(eic) != len(tic_data.tic): 521 warn( 522 "The software assumes same lenth of TIC and EIC, this does not seems to be the case and the results mass spectrum selected by the scan number might not be correct" 523 ) 524 525 if eic.max() > 0: 526 centroid_eics = self.eic_centroid_detector(time, eic, max_signal) 527 eic_data.apexes = [i for i in centroid_eics] 528 529 if plot: 530 for peak_indexes in eic_data.apexes: 531 apex_index = peak_indexes[1] 532 ax.plot( 533 time[apex_index], 534 eic[apex_index], 535 marker="x", 536 linewidth=0, 537 ) 538 539 if plot: 540 ax.set_xlabel("Time (min)") 541 ax.set_ylabel("a.u.") 542 ax.set_title(ms_type + " EIC") 543 ax.tick_params(axis="both", which="major", labelsize=12) 544 ax.axes.spines["top"].set_visible(False) 545 ax.axes.spines["right"].set_visible(False) 546 547 if legend: 548 legend = ax.legend(loc="upper left", bbox_to_anchor=(1.02, 0, 0.07, 1)) 549 fig.subplots_adjust(right=0.76) 550 # ax.set_prop_cycle(color=plt.cm.gist_rainbow(np.linspace(0, 1, len(traces)))) 551 552 d = {"down": 30, "up": -30} 553 554 def func(evt): 555 if legend.contains(evt): 556 bbox = legend.get_bbox_to_anchor() 557 bbox = Bbox.from_bounds( 558 bbox.x0, bbox.y0 + d[evt.button], bbox.width, bbox.height 559 ) 560 tr = legend.axes.transAxes.inverted() 561 legend.set_bbox_to_anchor(bbox.transformed(tr)) 562 fig.canvas.draw_idle() 563 564 fig.canvas.mpl_connect("scroll_event", func) 565 return chroma, ax 566 else: 567 return chroma, None 568 rt = [] 569 tic = [] 570 scans = [] 571 for i in range(traces[0].Length): 572 # print(trace[0].HasBasePeakData,trace[0].EndTime ) 573 574 # print(" {} - {}, {}".format( i, trace[0].Times[i], trace[0].Intensities[i] )) 575 rt.append(traces[0].Times[i]) 576 tic.append(traces[0].Intensities[i]) 577 scans.append(traces[0].Scans[i]) 578 579 return traces 580 # plot_chroma(rt, tic) 581 # plt.show() 582 583 def get_tic( 584 self, 585 ms_type="MS !d", 586 peak_detection=False, # This wont work right now 587 smooth=False, # This wont work right now 588 plot=False, 589 ax=None, 590 trace_type="TIC", 591 ) -> Tuple[TIC_Data, axes.Axes]: 592 """ms_type: str ('MS !d', 'MS2', None) 593 if you use None you get all scans. 594 peak_detection: bool 595 smooth: bool 596 plot: bool 597 ax: matplotlib axis object 598 trace_type: str ('TIC','BPC') 599 600 returns: 601 chroma: dict 602 { 603 Scan: [int] 604 original thermo scan numberMS 605 Time: [floats] 606 list of retention times 607 TIC: [floats] 608 total ion chromatogram 609 Apexes: [int] 610 original thermo apex scan number after peak picking 611 } 612 """ 613 # If peak_detection or smooth is True, raise exception 614 if peak_detection or smooth: 615 raise Exception( 616 "Peak detection and smoothing are no longer implemented in this function" 617 ) 618 619 if trace_type == "TIC": 620 settings = ChromatogramTraceSettings(TraceType.TIC) 621 elif trace_type == "BPC": 622 settings = ChromatogramTraceSettings(TraceType.BasePeak) 623 else: 624 raise ValueError(f"{trace_type} undefined") 625 if ms_type == "all": 626 settings.Filter = None 627 else: 628 settings.Filter = ms_type 629 630 chroma_settings = IChromatogramSettings(settings) 631 632 data = self.iRawDataPlus.GetChromatogramData( 633 [chroma_settings], self.start_scan, self.end_scan 634 ) 635 636 trace = ChromatogramSignal.FromChromatogramData(data) 637 638 data = TIC_Data(time=[], scans=[], tic=[], bpc=[], apexes=[]) 639 640 if trace[0].Length > 0: 641 for i in range(trace[0].Length): 642 # print(trace[0].HasBasePeakData,trace[0].EndTime ) 643 644 # print(" {} - {}, {}".format( i, trace[0].Times[i], trace[0].Intensities[i] )) 645 data.time.append(trace[0].Times[i]) 646 data.tic.append(trace[0].Intensities[i]) 647 data.scans.append(trace[0].Scans[i]) 648 649 # print(trace[0].Scans[i]) 650 if smooth: 651 data.tic = self.smooth_tic(data.tic) 652 653 else: 654 data.tic = np.array(data.tic) 655 656 if peak_detection: 657 centroid_peak_indexes = [ 658 i for i in self.centroid_detector(data.time, data.tic) 659 ] 660 661 data.apexes = centroid_peak_indexes 662 663 if plot: 664 if not ax: 665 import matplotlib.pyplot as plt 666 667 ax = plt.gca() 668 # fig, ax = plt.subplots(figsize=(6, 3)) 669 670 ax.plot(data.time, data.tic, label=trace_type) 671 ax.set_xlabel("Time (min)") 672 ax.set_ylabel("a.u.") 673 if peak_detection: 674 for peak_indexes in data.apexes: 675 apex_index = peak_indexes[1] 676 ax.plot( 677 data.time[apex_index], 678 data.tic[apex_index], 679 marker="x", 680 linewidth=0, 681 ) 682 683 # plt.show() 684 if trace_type == "BPC": 685 data.bpc = data.tic 686 data.tic = [] 687 return data, ax 688 if trace_type == "BPC": 689 data.bpc = data.tic 690 data.tic = [] 691 return data, None 692 693 else: 694 return None, None 695 696 def get_average_mass_spectrum( 697 self, 698 spectrum_mode: str = "profile", 699 auto_process: bool = True, 700 ppm_tolerance: float = 5.0, 701 ms_type: str = "MS1", 702 ) -> MassSpecProfile | MassSpecCentroid: 703 """ 704 Averages mass spectra over a scan range using Thermo's AverageScansInScanRange method 705 or a scan list using Thermo's AverageScans method 706 spectrum_mode: str 707 centroid or profile mass spectrum 708 auto_process: bool 709 If true performs peak picking, and noise threshold calculation after creation of mass spectrum object 710 ms_type: str 711 String of form 'ms1' or 'ms2' or 'MS3' etc. Valid up to MS10. 712 Internal function converts to Thermo MSOrderType class. 713 714 """ 715 716 def get_profile_mass_spec(averageScan, d_params: dict, auto_process: bool): 717 mz_list = list(averageScan.SegmentedScan.Positions) 718 abund_list = list(averageScan.SegmentedScan.Intensities) 719 720 data_dict = { 721 Labels.mz: mz_list, 722 Labels.abundance: abund_list, 723 } 724 725 return MassSpecProfile(data_dict, d_params, auto_process=auto_process) 726 727 def get_centroid_mass_spec(averageScan, d_params: dict): 728 noise = list(averageScan.centroidScan.Noises) 729 730 baselines = list(averageScan.centroidScan.Baselines) 731 732 rp = list(averageScan.centroidScan.Resolutions) 733 734 magnitude = list(averageScan.centroidScan.Intensities) 735 736 mz = list(averageScan.centroidScan.Masses) 737 738 array_noise_std = (np.array(noise) - np.array(baselines)) / 3 739 l_signal_to_noise = np.array(magnitude) / array_noise_std 740 741 d_params["baseline_noise"] = np.average(array_noise_std) 742 743 d_params["baseline_noise_std"] = np.std(array_noise_std) 744 745 data_dict = { 746 Labels.mz: mz, 747 Labels.abundance: magnitude, 748 Labels.rp: rp, 749 Labels.s2n: list(l_signal_to_noise), 750 } 751 752 mass_spec = MassSpecCentroid(data_dict, d_params, auto_process=False) 753 754 return mass_spec 755 756 d_params = self.set_metadata( 757 firstScanNumber=self.start_scan, lastScanNumber=self.end_scan 758 ) 759 760 # Create the mass options object that will be used when averaging the scans 761 options = MassOptions() 762 options.ToleranceUnits = ToleranceUnits.ppm 763 options.Tolerance = ppm_tolerance 764 765 # Get the scan filter for the first scan. This scan filter will be used to located 766 # scans within the given scan range of the same type 767 scanFilter = self.iRawDataPlus.GetFilterForScanNumber(self.start_scan) 768 769 # force it to only look for the MSType 770 scanFilter = self.set_msordertype(scanFilter, ms_type) 771 772 if isinstance(self.scans, tuple): 773 averageScan = Extensions.AverageScansInScanRange( 774 self.iRawDataPlus, self.start_scan, self.end_scan, scanFilter, options 775 ) 776 777 if averageScan: 778 if spectrum_mode == "profile": 779 mass_spec = get_profile_mass_spec( 780 averageScan, d_params, auto_process 781 ) 782 783 return mass_spec 784 785 elif spectrum_mode == "centroid": 786 if averageScan.HasCentroidStream: 787 mass_spec = get_centroid_mass_spec(averageScan, d_params) 788 789 return mass_spec 790 791 else: 792 raise ValueError( 793 "No Centroind data available for the selected scans" 794 ) 795 else: 796 raise ValueError("spectrum_mode must be 'profile' or centroid") 797 else: 798 raise ValueError("No data found for the selected scans") 799 800 elif isinstance(self.scans, list): 801 d_params = self.set_metadata(scans_list=self.scans) 802 803 scans = DotNetList[int]() 804 for scan in self.scans: 805 scans.Add(scan) 806 807 averageScan = Extensions.AverageScans(self.iRawDataPlus, scans, options) 808 809 if averageScan: 810 if spectrum_mode == "profile": 811 mass_spec = get_profile_mass_spec( 812 averageScan, d_params, auto_process 813 ) 814 815 return mass_spec 816 817 elif spectrum_mode == "centroid": 818 if averageScan.HasCentroidStream: 819 mass_spec = get_centroid_mass_spec(averageScan, d_params) 820 821 return mass_spec 822 823 else: 824 raise ValueError( 825 "No Centroind data available for the selected scans" 826 ) 827 828 else: 829 raise ValueError("spectrum_mode must be 'profile' or centroid") 830 831 else: 832 raise ValueError("No data found for the selected scans") 833 834 else: 835 raise ValueError("scans must be a list intergers or a tuple if integers") 836 837 def set_metadata( 838 self, 839 firstScanNumber=0, 840 lastScanNumber=0, 841 scans_list=False, 842 label=Labels.thermo_profile, 843 ): 844 """ 845 Collect metadata to be ingested in the mass spectrum object 846 847 scans_list: list[int] or false 848 lastScanNumber: int 849 firstScanNumber: int 850 """ 851 852 d_params = default_parameters(self.file_path) 853 854 # assumes scans is full scan or reduced profile scan 855 856 d_params["label"] = label 857 858 if scans_list: 859 d_params["scan_number"] = scans_list 860 861 d_params["polarity"] = self.get_polarity_mode(scans_list[0]) 862 863 else: 864 d_params["scan_number"] = "{}-{}".format(firstScanNumber, lastScanNumber) 865 866 d_params["polarity"] = self.get_polarity_mode(firstScanNumber) 867 868 d_params["analyzer"] = self.iRawDataPlus.GetInstrumentData().Model 869 870 d_params["acquisition_time"] = self.get_creation_time() 871 872 d_params["instrument_label"] = self.iRawDataPlus.GetInstrumentData().Name 873 874 return d_params 875 876 def get_instrument_methods(self, parse_strings: bool = True): 877 """ 878 This function will extract the instrument methods embedded in the raw file 879 880 First it will check if there are any instrument methods, if not returning None 881 Then it will get the total number of instrument methods. 882 For each method, it will extract the plaintext string of the method and attempt to parse it into a dictionary 883 If this fails, it will return just the string object. 884 885 This has been tested on data from an Orbitrap ID-X with embedded MS and LC methods, but other instrument types may fail. 886 887 Parameters: 888 ----------- 889 parse_strings: bool 890 If True, will attempt to parse the instrument methods into a dictionary. If False, will return the raw string. 891 892 Returns: 893 -------- 894 List[Dict[str, Any]] or List 895 A list of dictionaries containing the instrument methods, or a list of strings if parsing fails. 896 """ 897 898 if not self.iRawDataPlus.HasInstrumentMethod: 899 raise ValueError( 900 "Raw Data file does not have any instrument methods attached" 901 ) 902 return None 903 else: 904 905 def parse_instrument_method(data): 906 lines = data.split("\r\n") 907 method = {} 908 current_section = None 909 sub_section = None 910 911 for line in lines: 912 if not line.strip(): # Skip empty lines 913 continue 914 if ( 915 line.startswith("----") 916 or line.endswith("Settings") 917 or line.endswith("Summary") 918 or line.startswith("Experiment") 919 or line.startswith("Scan Event") 920 ): 921 current_section = line.replace("-", "").strip() 922 method[current_section] = {} 923 sub_section = None 924 elif line.startswith("\t"): 925 if "\t\t" in line: 926 indent_level = line.count("\t") 927 key_value = line.strip() 928 929 if indent_level == 2: 930 if sub_section: 931 key, value = ( 932 key_value.split("=", 1) 933 if "=" in key_value 934 else (key_value, None) 935 ) 936 method[current_section][sub_section][ 937 key.strip() 938 ] = value.strip() if value else None 939 elif indent_level == 3: 940 scan_type, key_value = ( 941 key_value.split(" ", 1) 942 if " " in key_value 943 else (key_value, None) 944 ) 945 method.setdefault(current_section, {}).setdefault( 946 sub_section, {} 947 ).setdefault(scan_type, {}) 948 949 if key_value: 950 key, value = ( 951 key_value.split("=", 1) 952 if "=" in key_value 953 else (key_value, None) 954 ) 955 method[current_section][sub_section][scan_type][ 956 key.strip() 957 ] = value.strip() if value else None 958 else: 959 key_value = line.strip() 960 if "=" in key_value: 961 key, value = key_value.split("=", 1) 962 method.setdefault(current_section, {})[key.strip()] = ( 963 value.strip() 964 ) 965 else: 966 sub_section = key_value 967 else: 968 if ":" in line: 969 key, value = line.split(":", 1) 970 method[current_section][key.strip()] = value.strip() 971 else: 972 method[current_section][line] = {} 973 974 return method 975 976 count_instrument_methods = self.iRawDataPlus.InstrumentMethodsCount 977 # TODO make this code better... 978 instrument_methods = [] 979 for i in range(count_instrument_methods): 980 instrument_method_string = self.iRawDataPlus.GetInstrumentMethod(i) 981 if parse_strings: 982 try: 983 instrument_method_dict = parse_instrument_method( 984 instrument_method_string 985 ) 986 except: # if it fails for any reason 987 instrument_method_dict = instrument_method_string 988 else: 989 instrument_method_dict = instrument_method_string 990 instrument_methods.append(instrument_method_dict) 991 return instrument_methods 992 993 def get_tune_method(self): 994 """ 995 This code will extract the tune method from the raw file 996 It has been tested on data from a Thermo Orbitrap ID-X, Astral and Q-Exactive, but may fail on other instrument types. 997 It attempts to parse out section headers and sub-sections, but may not work for all instrument types. 998 It will also not return Labels (keys) where the value is blank 999 1000 Returns: 1001 -------- 1002 Dict[str, Any] 1003 A dictionary containing the tune method information 1004 1005 Raises: 1006 ------- 1007 ValueError 1008 If no tune methods are found in the raw file 1009 1010 """ 1011 tunemethodcount = self.iRawDataPlus.GetTuneDataCount() 1012 if tunemethodcount == 0: 1013 raise ValueError("No tune methods found in the raw data file") 1014 return None 1015 elif tunemethodcount > 1: 1016 warnings.warn( 1017 "Multiple tune methods found in the raw data file, returning the 1st" 1018 ) 1019 1020 header = self.iRawDataPlus.GetTuneData(0) 1021 1022 header_dic = {} 1023 current_section = None 1024 1025 for i in range(header.Length): 1026 label = header.Labels[i] 1027 value = header.Values[i] 1028 1029 # Check for section headers 1030 if "===" in label or ( 1031 (value == "" or value is None) and not label.endswith(":") 1032 ): 1033 # This is a section header 1034 section_name = ( 1035 label.replace("=", "").replace(":", "").strip() 1036 ) # Clean the label if it contains '=' 1037 header_dic[section_name] = {} 1038 current_section = section_name 1039 else: 1040 if current_section: 1041 header_dic[current_section][label] = value 1042 else: 1043 header_dic[label] = value 1044 return header_dic 1045 1046 def get_status_log(self, retention_time: float = 0): 1047 """ 1048 This code will extract the status logs from the raw file 1049 It has been tested on data from a Thermo Orbitrap ID-X, Astral and Q-Exactive, but may fail on other instrument types. 1050 It attempts to parse out section headers and sub-sections, but may not work for all instrument types. 1051 It will also not return Labels (keys) where the value is blank 1052 1053 Parameters: 1054 ----------- 1055 retention_time: float 1056 The retention time in minutes to extract the status log data from. 1057 Will use the closest retention time found. Default 0. 1058 1059 Returns: 1060 -------- 1061 Dict[str, Any] 1062 A dictionary containing the status log information 1063 1064 Raises: 1065 ------- 1066 ValueError 1067 If no status logs are found in the raw file 1068 1069 """ 1070 tunemethodcount = self.iRawDataPlus.GetStatusLogEntriesCount() 1071 if tunemethodcount == 0: 1072 raise ValueError("No status logs found in the raw data file") 1073 return None 1074 1075 header = self.iRawDataPlus.GetStatusLogForRetentionTime(retention_time) 1076 1077 header_dic = {} 1078 current_section = None 1079 1080 for i in range(header.Length): 1081 label = header.Labels[i] 1082 value = header.Values[i] 1083 1084 # Check for section headers 1085 if "===" in label or ( 1086 (value == "" or value is None) and not label.endswith(":") 1087 ): 1088 # This is a section header 1089 section_name = ( 1090 label.replace("=", "").replace(":", "").strip() 1091 ) # Clean the label if it contains '=' 1092 header_dic[section_name] = {} 1093 current_section = section_name 1094 else: 1095 if current_section: 1096 header_dic[current_section][label] = value 1097 else: 1098 header_dic[label] = value 1099 return header_dic 1100 1101 def get_error_logs(self): 1102 """ 1103 This code will extract the error logs from the raw file 1104 1105 Returns: 1106 -------- 1107 Dict[float, str] 1108 A dictionary containing the error log information with the retention time as the key 1109 1110 Raises: 1111 ------- 1112 ValueError 1113 If no error logs are found in the raw file 1114 """ 1115 1116 error_log_count = self.iRawDataPlus.RunHeaderEx.ErrorLogCount 1117 if error_log_count == 0: 1118 raise ValueError("No error logs found in the raw data file") 1119 return None 1120 1121 error_logs = {} 1122 1123 for i in range(error_log_count): 1124 error_log_item = self.iRawDataPlus.GetErrorLogItem(i) 1125 rt = error_log_item.RetentionTime 1126 message = error_log_item.Message 1127 # Use the index `i` as the unique ID key 1128 error_logs[i] = {"rt": rt, "message": message} 1129 return error_logs 1130 1131 def get_sample_information(self): 1132 """ 1133 This code will extract the sample information from the raw file 1134 1135 Returns: 1136 -------- 1137 Dict[str, Any] 1138 A dictionary containing the sample information 1139 Note that UserText field may not be handled properly and may need further processing 1140 """ 1141 sminfo = self.iRawDataPlus.SampleInformation 1142 smdict = {} 1143 smdict["Comment"] = sminfo.Comment 1144 smdict["SampleId"] = sminfo.SampleId 1145 smdict["SampleName"] = sminfo.SampleName 1146 smdict["Vial"] = sminfo.Vial 1147 smdict["InjectionVolume"] = sminfo.InjectionVolume 1148 smdict["Barcode"] = sminfo.Barcode 1149 smdict["BarcodeStatus"] = str(sminfo.BarcodeStatus) 1150 smdict["CalibrationLevel"] = sminfo.CalibrationLevel 1151 smdict["DilutionFactor"] = sminfo.DilutionFactor 1152 smdict["InstrumentMethodFile"] = sminfo.InstrumentMethodFile 1153 smdict["RawFileName"] = sminfo.RawFileName 1154 smdict["CalibrationFile"] = sminfo.CalibrationFile 1155 smdict["IstdAmount"] = sminfo.IstdAmount 1156 smdict["RowNumber"] = sminfo.RowNumber 1157 smdict["Path"] = sminfo.Path 1158 smdict["ProcessingMethodFile"] = sminfo.ProcessingMethodFile 1159 smdict["SampleType"] = str(sminfo.SampleType) 1160 smdict["SampleWeight"] = sminfo.SampleWeight 1161 smdict["UserText"] = { 1162 "UserText": [x for x in sminfo.UserText] 1163 } # [0] #This may not work - needs debugging with 1164 return smdict 1165 1166 def get_instrument_data(self): 1167 """ 1168 This code will extract the instrument data from the raw file 1169 1170 Returns: 1171 -------- 1172 Dict[str, Any] 1173 A dictionary containing the instrument data 1174 """ 1175 instrument_data = self.iRawDataPlus.GetInstrumentData() 1176 id_dict = {} 1177 id_dict["Name"] = instrument_data.Name 1178 id_dict["Model"] = instrument_data.Model 1179 id_dict["SerialNumber"] = instrument_data.SerialNumber 1180 id_dict["SoftwareVersion"] = instrument_data.SoftwareVersion 1181 id_dict["HardwareVersion"] = instrument_data.HardwareVersion 1182 id_dict["ChannelLabels"] = { 1183 "ChannelLabels": [x for x in instrument_data.ChannelLabels] 1184 } 1185 id_dict["Flags"] = instrument_data.Flags 1186 id_dict["AxisLabelY"] = instrument_data.AxisLabelY 1187 id_dict["AxisLabelX"] = instrument_data.AxisLabelX 1188 return id_dict 1189 1190 def get_centroid_msms_data(self, scan): 1191 """ 1192 .. deprecated:: 2.0 1193 This function will be removed in CoreMS 2.0. Please use `get_average_mass_spectrum()` instead for similar functionality. 1194 """ 1195 1196 warnings.warn( 1197 "The `get_centroid_msms_data()` is deprecated as of CoreMS 2.0 and will be removed in a future version. " 1198 "Please use `get_average_mass_spectrum()` instead.", 1199 DeprecationWarning, 1200 ) 1201 1202 d_params = self.set_metadata(scans_list=[scan], label=Labels.thermo_centroid) 1203 1204 centroidStream = self.iRawDataPlus.GetCentroidStream(scan, False) 1205 1206 noise = list(centroidStream.Noises) 1207 1208 baselines = list(centroidStream.Baselines) 1209 1210 rp = list(centroidStream.Resolutions) 1211 1212 magnitude = list(centroidStream.Intensities) 1213 1214 mz = list(centroidStream.Masses) 1215 1216 # charge = scans_labels[5] 1217 array_noise_std = (np.array(noise) - np.array(baselines)) / 3 1218 l_signal_to_noise = np.array(magnitude) / array_noise_std 1219 1220 d_params["baseline_noise"] = np.average(array_noise_std) 1221 1222 d_params["baseline_noise_std"] = np.std(array_noise_std) 1223 1224 data_dict = { 1225 Labels.mz: mz, 1226 Labels.abundance: magnitude, 1227 Labels.rp: rp, 1228 Labels.s2n: list(l_signal_to_noise), 1229 } 1230 1231 mass_spec = MassSpecCentroid(data_dict, d_params, auto_process=False) 1232 mass_spec.settings.noise_threshold_method = "relative_abundance" 1233 mass_spec.settings.noise_threshold_min_relative_abundance = 1 1234 mass_spec.process_mass_spec() 1235 return mass_spec 1236 1237 def get_average_mass_spectrum_by_scanlist( 1238 self, 1239 scans_list: List[int], 1240 auto_process: bool = True, 1241 ppm_tolerance: float = 5.0, 1242 ) -> MassSpecProfile: 1243 """ 1244 Averages selected scans mass spectra using Thermo's AverageScans method 1245 scans_list: list[int] 1246 auto_process: bool 1247 If true performs peak picking, and noise threshold calculation after creation of mass spectrum object 1248 Returns: 1249 MassSpecProfile 1250 1251 .. deprecated:: 2.0 1252 This function will be removed in CoreMS 2.0. Please use `get_average_mass_spectrum()` instead for similar functionality. 1253 """ 1254 1255 warnings.warn( 1256 "The `get_average_mass_spectrum_by_scanlist()` is deprecated as of CoreMS 2.0 and will be removed in a future version. " 1257 "Please use `get_average_mass_spectrum()` instead.", 1258 DeprecationWarning, 1259 ) 1260 1261 d_params = self.set_metadata(scans_list=scans_list) 1262 1263 # assumes scans is full scan or reduced profile scan 1264 1265 scans = DotNetList[int]() 1266 for scan in scans_list: 1267 scans.Add(scan) 1268 1269 # Create the mass options object that will be used when averaging the scans 1270 options = MassOptions() 1271 options.ToleranceUnits = ToleranceUnits.ppm 1272 options.Tolerance = ppm_tolerance 1273 1274 # Get the scan filter for the first scan. This scan filter will be used to located 1275 # scans within the given scan range of the same type 1276 1277 averageScan = Extensions.AverageScans(self.iRawDataPlus, scans, options) 1278 1279 len_data = averageScan.SegmentedScan.Positions.Length 1280 1281 mz_list = list(averageScan.SegmentedScan.Positions) 1282 abund_list = list(averageScan.SegmentedScan.Intensities) 1283 1284 data_dict = { 1285 Labels.mz: mz_list, 1286 Labels.abundance: abund_list, 1287 } 1288 1289 mass_spec = MassSpecProfile(data_dict, d_params, auto_process=auto_process) 1290 1291 return mass_spec
Class for parsing Thermo Raw files and extracting information from them.
Parameters:
file_location : str or pathlib.Path or s3path.S3Path Thermo Raw file path or S3 path.
Attributes:
file_path : str or pathlib.Path or s3path.S3Path The file path of the Thermo Raw file. parameters : LCMSParameters The LCMS parameters for the Thermo Raw file. chromatogram_settings : LiquidChromatographSetting The chromatogram settings for the Thermo Raw file. scans : list or tuple The selected scans for the Thermo Raw file. start_scan : int The starting scan number for the Thermo Raw file. end_scan : int The ending scan number for the Thermo Raw file.
Methods:
- set_msordertype(scanFilter, mstype: str = 'ms1') -> scanFilter Convert the user-passed MS Type string to a Thermo MSOrderType object.
- get_instrument_info() -> dict Get the instrument information from the Thermo Raw file.
- get_creation_time() -> datetime.datetime Extract the creation date stamp from the .RAW file and return it as a formatted datetime object.
- remove_temp_file() Remove the temporary file if the path is from S3Path.
- get_polarity_mode(scan_number: int) -> int Get the polarity mode for the given scan number.
- get_filter_for_scan_num(scan_number: int) -> List[str] Get the filter for the given scan number.
- check_full_scan(scan_number: int) -> bool Check if the given scan number is a full scan.
- get_all_filters() -> Tuple[Dict[int, str], List[str]] Get all scan filters for the Thermo Raw file.
- get_scan_header(scan: int) -> Dict[str, Any] Get the full dictionary of scan header metadata for the given scan number.
- get_rt_time_from_trace(trace) -> Tuple[List[float], List[float], List[int]] Get the retention time, intensity, and scan number from the given trace.
- get_eics(target_mzs: List[float], tic_data: Dict[str, Any], ms_type: str = 'MS !d', peak_detection: bool = True, smooth: bool = True, plot: bool = False, ax: Optional[matplotlib.axes.Axes] = None, legend: bool = False) -> Tuple[Dict[float, EIC_Data], matplotlib.axes.Axes] Get the extracted ion chromatograms (EICs) for the target m/z values.
111 def __init__(self, file_location): 112 """file_location: srt pathlib.Path or s3path.S3Path 113 Thermo Raw file path 114 """ 115 # Thread.__init__(self) 116 if isinstance(file_location, str): 117 file_path = Path(file_location) 118 119 elif isinstance(file_location, S3Path): 120 temp_dir = Path("tmp/") 121 temp_dir.mkdir(exist_ok=True) 122 123 file_path = temp_dir / file_location.name 124 with open(file_path, "wb") as fh: 125 fh.write(file_location.read_bytes()) 126 127 else: 128 file_path = file_location 129 130 self.iRawDataPlus = RawFileReaderAdapter.FileFactory(str(file_path)) 131 self.iFileHeader = None 132 133 if not self.iRawDataPlus.IsOpen: 134 self.close_file() 135 raise FileNotFoundError( 136 "Unable to access the RAW file using the RawFileReader class!" 137 ) 138 139 # Check for any errors in the RAW file 140 if self.iRawDataPlus.IsError: 141 file_error = self.iRawDataPlus.FileError 142 self.close_file() 143 raise IOError("Error opening ({}) - {}".format(file_error, file_path)) 144 145 self.res = self.iRawDataPlus.SelectInstrument(Device.MS, 1) 146 147 self.file_path = file_location 148 self.iFileHeader = FileHeaderReaderFactory.ReadFile(str(file_path)) 149 150 # removing tmp file 151 152 self._init_settings()
file_location: srt pathlib.Path or s3path.S3Path Thermo Raw file path
160 @property 161 def parameters(self) -> LCMSParameters: 162 """ 163 Get or set the LCMSParameters object. 164 """ 165 return self._parameters
Get or set the LCMSParameters object.
171 @property 172 def chromatogram_settings(self) -> LiquidChromatographSetting: 173 """ 174 Get or set the LiquidChromatographSetting object. 175 """ 176 return self.parameters.lc_ms
Get or set the LiquidChromatographSetting object.
184 @property 185 def scans(self) -> list | tuple: 186 """scans : list or tuple 187 If list uses Thermo AverageScansInScanRange for selected scans, ortherwise uses Thermo AverageScans for a scan range 188 """ 189 return self.chromatogram_settings.scans
scans : list or tuple If list uses Thermo AverageScansInScanRange for selected scans, ortherwise uses Thermo AverageScans for a scan range
191 @property 192 def start_scan(self) -> int: 193 """ 194 Get the starting scan number for the Thermo Raw file. 195 """ 196 if self.scans[0] == -1: 197 return self.iRawDataPlus.RunHeaderEx.FirstSpectrum 198 else: 199 return self.scans[0]
Get the starting scan number for the Thermo Raw file.
201 @property 202 def end_scan(self) -> int: 203 """ 204 Get the ending scan number for the Thermo Raw file. 205 """ 206 if self.scans[-1] == -1: 207 return self.iRawDataPlus.RunHeaderEx.LastSpectrum 208 else: 209 return self.scans[-1]
Get the ending scan number for the Thermo Raw file.
211 def set_msordertype(self, scanFilter, mstype: str = "ms1"): 212 """ 213 Function to convert user passed string MS Type to Thermo MSOrderType object 214 Limited to MS1 through MS10. 215 216 Parameters: 217 ----------- 218 scanFilter : Thermo.ScanFilter 219 The scan filter object. 220 mstype : str, optional 221 The MS Type string, by default 'ms1' 222 223 """ 224 mstype = mstype.upper() 225 # Check that a valid mstype is passed 226 if (int(mstype.split("MS")[1]) > 10) or (int(mstype.split("MS")[1]) < 1): 227 warn("MS Type not valid, must be between MS1 and MS10") 228 229 msordertypedict = { 230 "MS1": MSOrderType.Ms, 231 "MS2": MSOrderType.Ms2, 232 "MS3": MSOrderType.Ms3, 233 "MS4": MSOrderType.Ms4, 234 "MS5": MSOrderType.Ms5, 235 "MS6": MSOrderType.Ms6, 236 "MS7": MSOrderType.Ms7, 237 "MS8": MSOrderType.Ms8, 238 "MS9": MSOrderType.Ms9, 239 "MS10": MSOrderType.Ms10, 240 } 241 scanFilter.MSOrder = msordertypedict[mstype] 242 return scanFilter
Function to convert user passed string MS Type to Thermo MSOrderType object Limited to MS1 through MS10.
Parameters:
scanFilter : Thermo.ScanFilter The scan filter object. mstype : str, optional The MS Type string, by default 'ms1'
244 def get_instrument_info(self) -> dict: 245 """ 246 Get the instrument information from the Thermo Raw file. 247 248 Returns: 249 -------- 250 dict 251 A dictionary with the keys 'model', and 'serial_number'. 252 """ 253 instrumentData = self.iRawDataPlus.GetInstrumentData() 254 return { 255 "model": instrumentData.Model, 256 "serial_number": instrumentData.SerialNumber, 257 }
Get the instrument information from the Thermo Raw file.
Returns:
dict A dictionary with the keys 'model', and 'serial_number'.
259 def get_creation_time(self) -> datetime.datetime: 260 """ 261 Extract the creation date stamp from the .RAW file 262 Return formatted creation date stamp. 263 264 """ 265 credate = self.iRawDataPlus.CreationDate.get_Ticks() 266 credate = datetime.datetime(1, 1, 1) + datetime.timedelta( 267 microseconds=credate / 10 268 ) 269 return credate
Extract the creation date stamp from the .RAW file Return formatted creation date stamp.
271 def remove_temp_file(self) -> None: 272 """if the path is from S3Path data cannot be serialized to io.ByteStream and 273 a temporary copy is stored at the temp dir 274 use this function only at the end of your execution scrip 275 some LCMS class methods depend on this file 276 """ 277 278 self.file_path.unlink()
if the path is from S3Path data cannot be serialized to io.ByteStream and a temporary copy is stored at the temp dir use this function only at the end of your execution scrip some LCMS class methods depend on this file
280 def close_file(self) -> None: 281 """Dispose the Thermo file handles. 282 283 A second call is a no-op. This does not delete an S3 temp copy; 284 ``remove_temp_file`` does that. 285 """ 286 for attr in ("iRawDataPlus", "iFileHeader"): 287 handle = getattr(self, attr, None) 288 if handle is None: 289 continue 290 dispose = getattr(handle, "Dispose", None) 291 setattr(self, attr, None) 292 if callable(dispose): 293 dispose()
Dispose the Thermo file handles.
A second call is a no-op. This does not delete an S3 temp copy;
remove_temp_file does that.
301 def get_polarity_mode(self, scan_number: int) -> int: 302 """ 303 Get the polarity mode for the given scan number. 304 305 Parameters: 306 ----------- 307 scan_number : int 308 The scan number. 309 310 Raises: 311 ------- 312 Exception 313 If the polarity mode is unknown. 314 315 """ 316 polarity_symbol = self.get_filter_for_scan_num(scan_number)[1] 317 318 if polarity_symbol == "+": 319 return 1 320 # return 'POSITIVE_ION_MODE' 321 322 elif polarity_symbol == "-": 323 return -1 324 325 else: 326 raise Exception("Polarity Mode Unknown, please set it manually")
Get the polarity mode for the given scan number.
Parameters:
scan_number : int The scan number.
Raises:
Exception If the polarity mode is unknown.
328 def get_filter_for_scan_num(self, scan_number: int) -> List[str]: 329 """ 330 Returns the closest matching run time that corresponds to scan_number for the current 331 controller. This function is only supported for MS device controllers. 332 e.g. ['FTMS', '-', 'p', 'NSI', 'Full', 'ms', '[200.00-1000.00]'] 333 334 Parameters: 335 ----------- 336 scan_number : int 337 The scan number. 338 339 """ 340 scan_label = self.iRawDataPlus.GetScanEventStringForScanNumber(scan_number) 341 342 return str(scan_label).split()
Returns the closest matching run time that corresponds to scan_number for the current controller. This function is only supported for MS device controllers. e.g. ['FTMS', '-', 'p', 'NSI', 'Full', 'ms', '[200.00-1000.00]']
Parameters:
scan_number : int The scan number.
344 def get_ms_level_for_scan_num(self, scan_number: int) -> str: 345 """ 346 Get the MS order for the given scan number. 347 348 Parameters: 349 ----------- 350 scan_number : int 351 The scan number 352 353 Returns: 354 -------- 355 int 356 The MS order type (1 for MS, 2 for MS2, etc.) 357 """ 358 scan_filter = self.iRawDataPlus.GetFilterForScanNumber(scan_number) 359 360 msordertype = { 361 MSOrderType.Ms: 1, 362 MSOrderType.Ms2: 2, 363 MSOrderType.Ms3: 3, 364 MSOrderType.Ms4: 4, 365 MSOrderType.Ms5: 5, 366 MSOrderType.Ms6: 6, 367 MSOrderType.Ms7: 7, 368 MSOrderType.Ms8: 8, 369 MSOrderType.Ms9: 9, 370 MSOrderType.Ms10: 10, 371 } 372 373 if scan_filter.MSOrder in msordertype: 374 return msordertype[scan_filter.MSOrder] 375 else: 376 raise Exception("MS Order Type not found")
Get the MS order for the given scan number.
Parameters:
scan_number : int The scan number
Returns:
int The MS order type (1 for MS, 2 for MS2, etc.)
384 def get_all_filters(self) -> Tuple[Dict[int, str], List[str]]: 385 """ 386 Get all scan filters. 387 This function is only supported for MS device controllers. 388 e.g. ['FTMS', '-', 'p', 'NSI', 'Full', 'ms', '[200.00-1000.00]'] 389 390 """ 391 392 scanrange = range(self.start_scan, self.end_scan + 1) 393 scanfiltersdic = {} 394 scanfilterslist = [] 395 for scan_number in scanrange: 396 scan_label = self.iRawDataPlus.GetScanEventStringForScanNumber(scan_number) 397 scanfiltersdic[scan_number] = scan_label 398 scanfilterslist.append(scan_label) 399 scanfilterset = list(set(scanfilterslist)) 400 return scanfiltersdic, scanfilterset
Get all scan filters. This function is only supported for MS device controllers. e.g. ['FTMS', '-', 'p', 'NSI', 'Full', 'ms', '[200.00-1000.00]']
402 def get_scan_header(self, scan: int) -> Dict[str, Any]: 403 """ 404 Get full dictionary of scan header meta data, i.e. AGC status, ion injection time, etc. 405 406 Parameters: 407 ----------- 408 scan : int 409 The scan number. 410 411 """ 412 header = self.iRawDataPlus.GetTrailerExtraInformation(scan) 413 414 header_dic = {} 415 for i in range(header.Length): 416 header_dic.update({header.Labels[i]: header.Values[i]}) 417 return header_dic
Get full dictionary of scan header meta data, i.e. AGC status, ion injection time, etc.
Parameters:
scan : int The scan number.
419 @staticmethod 420 def get_rt_time_from_trace(trace) -> Tuple[List[float], List[float], List[int]]: 421 """trace: ThermoFisher.CommonCore.Data.Business.ChromatogramSignal""" 422 return list(trace.Times), list(trace.Intensities), list(trace.Scans)
trace: ThermoFisher.CommonCore.Data.Business.ChromatogramSignal
424 def get_eics( 425 self, 426 target_mzs: List[float], 427 tic_data: Dict[str, Any], 428 ms_type="MS !d", 429 peak_detection=False, 430 smooth=False, 431 plot=False, 432 ax: Optional[axes.Axes] = None, 433 legend=False, 434 ) -> Tuple[Dict[float, EIC_Data], axes.Axes]: 435 """ms_type: str ('MS', MS2') 436 start_scan: int default -1 will select the lowest available 437 end_scan: int default -1 will select the highest available 438 439 returns: 440 441 chroma: dict{target_mz: EIC_Data( 442 Scans: [int] 443 original thermo scan numbers 444 Time: [floats] 445 list of retention times 446 TIC: [floats] 447 total ion chromatogram 448 Apexes: [int] 449 original thermo apex scan number after peak picking 450 ) 451 452 """ 453 # If peak_detection or smooth is True, raise exception 454 if peak_detection or smooth: 455 raise Exception( 456 "Peak detection and smoothing are no longer implemented in this function" 457 ) 458 459 options = MassOptions() 460 options.ToleranceUnits = ToleranceUnits.ppm 461 options.Tolerance = self.chromatogram_settings.eic_tolerance_ppm 462 463 all_chroma_settings = [] 464 465 for target_mz in target_mzs: 466 settings = ChromatogramTraceSettings(TraceType.MassRange) 467 settings.Filter = ms_type 468 settings.MassRanges = [Range(target_mz, target_mz)] 469 470 chroma_settings = IChromatogramSettings(settings) 471 472 all_chroma_settings.append(chroma_settings) 473 474 # chroma_settings2 = IChromatogramSettings(settings) 475 # print(chroma_settings.FragmentMass) 476 # print(chroma_settings.FragmentMass) 477 # print(chroma_settings) 478 # print(chroma_settings) 479 480 data = self.iRawDataPlus.GetChromatogramData( 481 all_chroma_settings, self.start_scan, self.end_scan, options 482 ) 483 484 traces = ChromatogramSignal.FromChromatogramData(data) 485 486 chroma = {} 487 488 if plot: 489 from matplotlib.transforms import Bbox 490 import matplotlib.pyplot as plt 491 492 if not ax: 493 # ax = plt.gca() 494 # ax.clear() 495 fig, ax = plt.subplots() 496 497 else: 498 fig = plt.gcf() 499 500 # plt.show() 501 502 for i, trace in enumerate(traces): 503 if trace.Length > 0: 504 rt, eic, scans = self.get_rt_time_from_trace(trace) 505 if smooth: 506 eic = self.smooth_tic(eic) 507 508 chroma[target_mzs[i]] = EIC_Data(scans=scans, time=rt, eic=eic) 509 if plot: 510 ax.plot(rt, eic, label="{:.5f}".format(target_mzs[i])) 511 512 if peak_detection: 513 # max_eic = self.get_max_eic(chroma) 514 max_signal = max(tic_data.tic) 515 516 for eic_data in chroma.values(): 517 eic = eic_data.eic 518 time = eic_data.time 519 520 if len(eic) != len(tic_data.tic): 521 warn( 522 "The software assumes same lenth of TIC and EIC, this does not seems to be the case and the results mass spectrum selected by the scan number might not be correct" 523 ) 524 525 if eic.max() > 0: 526 centroid_eics = self.eic_centroid_detector(time, eic, max_signal) 527 eic_data.apexes = [i for i in centroid_eics] 528 529 if plot: 530 for peak_indexes in eic_data.apexes: 531 apex_index = peak_indexes[1] 532 ax.plot( 533 time[apex_index], 534 eic[apex_index], 535 marker="x", 536 linewidth=0, 537 ) 538 539 if plot: 540 ax.set_xlabel("Time (min)") 541 ax.set_ylabel("a.u.") 542 ax.set_title(ms_type + " EIC") 543 ax.tick_params(axis="both", which="major", labelsize=12) 544 ax.axes.spines["top"].set_visible(False) 545 ax.axes.spines["right"].set_visible(False) 546 547 if legend: 548 legend = ax.legend(loc="upper left", bbox_to_anchor=(1.02, 0, 0.07, 1)) 549 fig.subplots_adjust(right=0.76) 550 # ax.set_prop_cycle(color=plt.cm.gist_rainbow(np.linspace(0, 1, len(traces)))) 551 552 d = {"down": 30, "up": -30} 553 554 def func(evt): 555 if legend.contains(evt): 556 bbox = legend.get_bbox_to_anchor() 557 bbox = Bbox.from_bounds( 558 bbox.x0, bbox.y0 + d[evt.button], bbox.width, bbox.height 559 ) 560 tr = legend.axes.transAxes.inverted() 561 legend.set_bbox_to_anchor(bbox.transformed(tr)) 562 fig.canvas.draw_idle() 563 564 fig.canvas.mpl_connect("scroll_event", func) 565 return chroma, ax 566 else: 567 return chroma, None 568 rt = [] 569 tic = [] 570 scans = [] 571 for i in range(traces[0].Length): 572 # print(trace[0].HasBasePeakData,trace[0].EndTime ) 573 574 # print(" {} - {}, {}".format( i, trace[0].Times[i], trace[0].Intensities[i] )) 575 rt.append(traces[0].Times[i]) 576 tic.append(traces[0].Intensities[i]) 577 scans.append(traces[0].Scans[i]) 578 579 return traces 580 # plot_chroma(rt, tic) 581 # plt.show()
ms_type: str ('MS', MS2') start_scan: int default -1 will select the lowest available end_scan: int default -1 will select the highest available
returns:
chroma: dict{target_mz: EIC_Data(
Scans: [int]
original thermo scan numbers
Time: [floats]
list of retention times
TIC: [floats]
total ion chromatogram
Apexes: [int]
original thermo apex scan number after peak picking
)
583 def get_tic( 584 self, 585 ms_type="MS !d", 586 peak_detection=False, # This wont work right now 587 smooth=False, # This wont work right now 588 plot=False, 589 ax=None, 590 trace_type="TIC", 591 ) -> Tuple[TIC_Data, axes.Axes]: 592 """ms_type: str ('MS !d', 'MS2', None) 593 if you use None you get all scans. 594 peak_detection: bool 595 smooth: bool 596 plot: bool 597 ax: matplotlib axis object 598 trace_type: str ('TIC','BPC') 599 600 returns: 601 chroma: dict 602 { 603 Scan: [int] 604 original thermo scan numberMS 605 Time: [floats] 606 list of retention times 607 TIC: [floats] 608 total ion chromatogram 609 Apexes: [int] 610 original thermo apex scan number after peak picking 611 } 612 """ 613 # If peak_detection or smooth is True, raise exception 614 if peak_detection or smooth: 615 raise Exception( 616 "Peak detection and smoothing are no longer implemented in this function" 617 ) 618 619 if trace_type == "TIC": 620 settings = ChromatogramTraceSettings(TraceType.TIC) 621 elif trace_type == "BPC": 622 settings = ChromatogramTraceSettings(TraceType.BasePeak) 623 else: 624 raise ValueError(f"{trace_type} undefined") 625 if ms_type == "all": 626 settings.Filter = None 627 else: 628 settings.Filter = ms_type 629 630 chroma_settings = IChromatogramSettings(settings) 631 632 data = self.iRawDataPlus.GetChromatogramData( 633 [chroma_settings], self.start_scan, self.end_scan 634 ) 635 636 trace = ChromatogramSignal.FromChromatogramData(data) 637 638 data = TIC_Data(time=[], scans=[], tic=[], bpc=[], apexes=[]) 639 640 if trace[0].Length > 0: 641 for i in range(trace[0].Length): 642 # print(trace[0].HasBasePeakData,trace[0].EndTime ) 643 644 # print(" {} - {}, {}".format( i, trace[0].Times[i], trace[0].Intensities[i] )) 645 data.time.append(trace[0].Times[i]) 646 data.tic.append(trace[0].Intensities[i]) 647 data.scans.append(trace[0].Scans[i]) 648 649 # print(trace[0].Scans[i]) 650 if smooth: 651 data.tic = self.smooth_tic(data.tic) 652 653 else: 654 data.tic = np.array(data.tic) 655 656 if peak_detection: 657 centroid_peak_indexes = [ 658 i for i in self.centroid_detector(data.time, data.tic) 659 ] 660 661 data.apexes = centroid_peak_indexes 662 663 if plot: 664 if not ax: 665 import matplotlib.pyplot as plt 666 667 ax = plt.gca() 668 # fig, ax = plt.subplots(figsize=(6, 3)) 669 670 ax.plot(data.time, data.tic, label=trace_type) 671 ax.set_xlabel("Time (min)") 672 ax.set_ylabel("a.u.") 673 if peak_detection: 674 for peak_indexes in data.apexes: 675 apex_index = peak_indexes[1] 676 ax.plot( 677 data.time[apex_index], 678 data.tic[apex_index], 679 marker="x", 680 linewidth=0, 681 ) 682 683 # plt.show() 684 if trace_type == "BPC": 685 data.bpc = data.tic 686 data.tic = [] 687 return data, ax 688 if trace_type == "BPC": 689 data.bpc = data.tic 690 data.tic = [] 691 return data, None 692 693 else: 694 return None, None
ms_type: str ('MS !d', 'MS2', None) if you use None you get all scans. peak_detection: bool smooth: bool plot: bool ax: matplotlib axis object trace_type: str ('TIC','BPC')
returns: chroma: dict { Scan: [int] original thermo scan numberMS Time: [floats] list of retention times TIC: [floats] total ion chromatogram Apexes: [int] original thermo apex scan number after peak picking }
696 def get_average_mass_spectrum( 697 self, 698 spectrum_mode: str = "profile", 699 auto_process: bool = True, 700 ppm_tolerance: float = 5.0, 701 ms_type: str = "MS1", 702 ) -> MassSpecProfile | MassSpecCentroid: 703 """ 704 Averages mass spectra over a scan range using Thermo's AverageScansInScanRange method 705 or a scan list using Thermo's AverageScans method 706 spectrum_mode: str 707 centroid or profile mass spectrum 708 auto_process: bool 709 If true performs peak picking, and noise threshold calculation after creation of mass spectrum object 710 ms_type: str 711 String of form 'ms1' or 'ms2' or 'MS3' etc. Valid up to MS10. 712 Internal function converts to Thermo MSOrderType class. 713 714 """ 715 716 def get_profile_mass_spec(averageScan, d_params: dict, auto_process: bool): 717 mz_list = list(averageScan.SegmentedScan.Positions) 718 abund_list = list(averageScan.SegmentedScan.Intensities) 719 720 data_dict = { 721 Labels.mz: mz_list, 722 Labels.abundance: abund_list, 723 } 724 725 return MassSpecProfile(data_dict, d_params, auto_process=auto_process) 726 727 def get_centroid_mass_spec(averageScan, d_params: dict): 728 noise = list(averageScan.centroidScan.Noises) 729 730 baselines = list(averageScan.centroidScan.Baselines) 731 732 rp = list(averageScan.centroidScan.Resolutions) 733 734 magnitude = list(averageScan.centroidScan.Intensities) 735 736 mz = list(averageScan.centroidScan.Masses) 737 738 array_noise_std = (np.array(noise) - np.array(baselines)) / 3 739 l_signal_to_noise = np.array(magnitude) / array_noise_std 740 741 d_params["baseline_noise"] = np.average(array_noise_std) 742 743 d_params["baseline_noise_std"] = np.std(array_noise_std) 744 745 data_dict = { 746 Labels.mz: mz, 747 Labels.abundance: magnitude, 748 Labels.rp: rp, 749 Labels.s2n: list(l_signal_to_noise), 750 } 751 752 mass_spec = MassSpecCentroid(data_dict, d_params, auto_process=False) 753 754 return mass_spec 755 756 d_params = self.set_metadata( 757 firstScanNumber=self.start_scan, lastScanNumber=self.end_scan 758 ) 759 760 # Create the mass options object that will be used when averaging the scans 761 options = MassOptions() 762 options.ToleranceUnits = ToleranceUnits.ppm 763 options.Tolerance = ppm_tolerance 764 765 # Get the scan filter for the first scan. This scan filter will be used to located 766 # scans within the given scan range of the same type 767 scanFilter = self.iRawDataPlus.GetFilterForScanNumber(self.start_scan) 768 769 # force it to only look for the MSType 770 scanFilter = self.set_msordertype(scanFilter, ms_type) 771 772 if isinstance(self.scans, tuple): 773 averageScan = Extensions.AverageScansInScanRange( 774 self.iRawDataPlus, self.start_scan, self.end_scan, scanFilter, options 775 ) 776 777 if averageScan: 778 if spectrum_mode == "profile": 779 mass_spec = get_profile_mass_spec( 780 averageScan, d_params, auto_process 781 ) 782 783 return mass_spec 784 785 elif spectrum_mode == "centroid": 786 if averageScan.HasCentroidStream: 787 mass_spec = get_centroid_mass_spec(averageScan, d_params) 788 789 return mass_spec 790 791 else: 792 raise ValueError( 793 "No Centroind data available for the selected scans" 794 ) 795 else: 796 raise ValueError("spectrum_mode must be 'profile' or centroid") 797 else: 798 raise ValueError("No data found for the selected scans") 799 800 elif isinstance(self.scans, list): 801 d_params = self.set_metadata(scans_list=self.scans) 802 803 scans = DotNetList[int]() 804 for scan in self.scans: 805 scans.Add(scan) 806 807 averageScan = Extensions.AverageScans(self.iRawDataPlus, scans, options) 808 809 if averageScan: 810 if spectrum_mode == "profile": 811 mass_spec = get_profile_mass_spec( 812 averageScan, d_params, auto_process 813 ) 814 815 return mass_spec 816 817 elif spectrum_mode == "centroid": 818 if averageScan.HasCentroidStream: 819 mass_spec = get_centroid_mass_spec(averageScan, d_params) 820 821 return mass_spec 822 823 else: 824 raise ValueError( 825 "No Centroind data available for the selected scans" 826 ) 827 828 else: 829 raise ValueError("spectrum_mode must be 'profile' or centroid") 830 831 else: 832 raise ValueError("No data found for the selected scans") 833 834 else: 835 raise ValueError("scans must be a list intergers or a tuple if integers")
Averages mass spectra over a scan range using Thermo's AverageScansInScanRange method or a scan list using Thermo's AverageScans method spectrum_mode: str centroid or profile mass spectrum auto_process: bool If true performs peak picking, and noise threshold calculation after creation of mass spectrum object ms_type: str String of form 'ms1' or 'ms2' or 'MS3' etc. Valid up to MS10. Internal function converts to Thermo MSOrderType class.
837 def set_metadata( 838 self, 839 firstScanNumber=0, 840 lastScanNumber=0, 841 scans_list=False, 842 label=Labels.thermo_profile, 843 ): 844 """ 845 Collect metadata to be ingested in the mass spectrum object 846 847 scans_list: list[int] or false 848 lastScanNumber: int 849 firstScanNumber: int 850 """ 851 852 d_params = default_parameters(self.file_path) 853 854 # assumes scans is full scan or reduced profile scan 855 856 d_params["label"] = label 857 858 if scans_list: 859 d_params["scan_number"] = scans_list 860 861 d_params["polarity"] = self.get_polarity_mode(scans_list[0]) 862 863 else: 864 d_params["scan_number"] = "{}-{}".format(firstScanNumber, lastScanNumber) 865 866 d_params["polarity"] = self.get_polarity_mode(firstScanNumber) 867 868 d_params["analyzer"] = self.iRawDataPlus.GetInstrumentData().Model 869 870 d_params["acquisition_time"] = self.get_creation_time() 871 872 d_params["instrument_label"] = self.iRawDataPlus.GetInstrumentData().Name 873 874 return d_params
Collect metadata to be ingested in the mass spectrum object
scans_list: list[int] or false lastScanNumber: int firstScanNumber: int
876 def get_instrument_methods(self, parse_strings: bool = True): 877 """ 878 This function will extract the instrument methods embedded in the raw file 879 880 First it will check if there are any instrument methods, if not returning None 881 Then it will get the total number of instrument methods. 882 For each method, it will extract the plaintext string of the method and attempt to parse it into a dictionary 883 If this fails, it will return just the string object. 884 885 This has been tested on data from an Orbitrap ID-X with embedded MS and LC methods, but other instrument types may fail. 886 887 Parameters: 888 ----------- 889 parse_strings: bool 890 If True, will attempt to parse the instrument methods into a dictionary. If False, will return the raw string. 891 892 Returns: 893 -------- 894 List[Dict[str, Any]] or List 895 A list of dictionaries containing the instrument methods, or a list of strings if parsing fails. 896 """ 897 898 if not self.iRawDataPlus.HasInstrumentMethod: 899 raise ValueError( 900 "Raw Data file does not have any instrument methods attached" 901 ) 902 return None 903 else: 904 905 def parse_instrument_method(data): 906 lines = data.split("\r\n") 907 method = {} 908 current_section = None 909 sub_section = None 910 911 for line in lines: 912 if not line.strip(): # Skip empty lines 913 continue 914 if ( 915 line.startswith("----") 916 or line.endswith("Settings") 917 or line.endswith("Summary") 918 or line.startswith("Experiment") 919 or line.startswith("Scan Event") 920 ): 921 current_section = line.replace("-", "").strip() 922 method[current_section] = {} 923 sub_section = None 924 elif line.startswith("\t"): 925 if "\t\t" in line: 926 indent_level = line.count("\t") 927 key_value = line.strip() 928 929 if indent_level == 2: 930 if sub_section: 931 key, value = ( 932 key_value.split("=", 1) 933 if "=" in key_value 934 else (key_value, None) 935 ) 936 method[current_section][sub_section][ 937 key.strip() 938 ] = value.strip() if value else None 939 elif indent_level == 3: 940 scan_type, key_value = ( 941 key_value.split(" ", 1) 942 if " " in key_value 943 else (key_value, None) 944 ) 945 method.setdefault(current_section, {}).setdefault( 946 sub_section, {} 947 ).setdefault(scan_type, {}) 948 949 if key_value: 950 key, value = ( 951 key_value.split("=", 1) 952 if "=" in key_value 953 else (key_value, None) 954 ) 955 method[current_section][sub_section][scan_type][ 956 key.strip() 957 ] = value.strip() if value else None 958 else: 959 key_value = line.strip() 960 if "=" in key_value: 961 key, value = key_value.split("=", 1) 962 method.setdefault(current_section, {})[key.strip()] = ( 963 value.strip() 964 ) 965 else: 966 sub_section = key_value 967 else: 968 if ":" in line: 969 key, value = line.split(":", 1) 970 method[current_section][key.strip()] = value.strip() 971 else: 972 method[current_section][line] = {} 973 974 return method 975 976 count_instrument_methods = self.iRawDataPlus.InstrumentMethodsCount 977 # TODO make this code better... 978 instrument_methods = [] 979 for i in range(count_instrument_methods): 980 instrument_method_string = self.iRawDataPlus.GetInstrumentMethod(i) 981 if parse_strings: 982 try: 983 instrument_method_dict = parse_instrument_method( 984 instrument_method_string 985 ) 986 except: # if it fails for any reason 987 instrument_method_dict = instrument_method_string 988 else: 989 instrument_method_dict = instrument_method_string 990 instrument_methods.append(instrument_method_dict) 991 return instrument_methods
This function will extract the instrument methods embedded in the raw file
First it will check if there are any instrument methods, if not returning None Then it will get the total number of instrument methods. For each method, it will extract the plaintext string of the method and attempt to parse it into a dictionary If this fails, it will return just the string object.
This has been tested on data from an Orbitrap ID-X with embedded MS and LC methods, but other instrument types may fail.
Parameters:
parse_strings: bool If True, will attempt to parse the instrument methods into a dictionary. If False, will return the raw string.
Returns:
List[Dict[str, Any]] or List A list of dictionaries containing the instrument methods, or a list of strings if parsing fails.
993 def get_tune_method(self): 994 """ 995 This code will extract the tune method from the raw file 996 It has been tested on data from a Thermo Orbitrap ID-X, Astral and Q-Exactive, but may fail on other instrument types. 997 It attempts to parse out section headers and sub-sections, but may not work for all instrument types. 998 It will also not return Labels (keys) where the value is blank 999 1000 Returns: 1001 -------- 1002 Dict[str, Any] 1003 A dictionary containing the tune method information 1004 1005 Raises: 1006 ------- 1007 ValueError 1008 If no tune methods are found in the raw file 1009 1010 """ 1011 tunemethodcount = self.iRawDataPlus.GetTuneDataCount() 1012 if tunemethodcount == 0: 1013 raise ValueError("No tune methods found in the raw data file") 1014 return None 1015 elif tunemethodcount > 1: 1016 warnings.warn( 1017 "Multiple tune methods found in the raw data file, returning the 1st" 1018 ) 1019 1020 header = self.iRawDataPlus.GetTuneData(0) 1021 1022 header_dic = {} 1023 current_section = None 1024 1025 for i in range(header.Length): 1026 label = header.Labels[i] 1027 value = header.Values[i] 1028 1029 # Check for section headers 1030 if "===" in label or ( 1031 (value == "" or value is None) and not label.endswith(":") 1032 ): 1033 # This is a section header 1034 section_name = ( 1035 label.replace("=", "").replace(":", "").strip() 1036 ) # Clean the label if it contains '=' 1037 header_dic[section_name] = {} 1038 current_section = section_name 1039 else: 1040 if current_section: 1041 header_dic[current_section][label] = value 1042 else: 1043 header_dic[label] = value 1044 return header_dic
This code will extract the tune method from the raw file It has been tested on data from a Thermo Orbitrap ID-X, Astral and Q-Exactive, but may fail on other instrument types. It attempts to parse out section headers and sub-sections, but may not work for all instrument types. It will also not return Labels (keys) where the value is blank
Returns:
Dict[str, Any] A dictionary containing the tune method information
Raises:
ValueError If no tune methods are found in the raw file
1046 def get_status_log(self, retention_time: float = 0): 1047 """ 1048 This code will extract the status logs from the raw file 1049 It has been tested on data from a Thermo Orbitrap ID-X, Astral and Q-Exactive, but may fail on other instrument types. 1050 It attempts to parse out section headers and sub-sections, but may not work for all instrument types. 1051 It will also not return Labels (keys) where the value is blank 1052 1053 Parameters: 1054 ----------- 1055 retention_time: float 1056 The retention time in minutes to extract the status log data from. 1057 Will use the closest retention time found. Default 0. 1058 1059 Returns: 1060 -------- 1061 Dict[str, Any] 1062 A dictionary containing the status log information 1063 1064 Raises: 1065 ------- 1066 ValueError 1067 If no status logs are found in the raw file 1068 1069 """ 1070 tunemethodcount = self.iRawDataPlus.GetStatusLogEntriesCount() 1071 if tunemethodcount == 0: 1072 raise ValueError("No status logs found in the raw data file") 1073 return None 1074 1075 header = self.iRawDataPlus.GetStatusLogForRetentionTime(retention_time) 1076 1077 header_dic = {} 1078 current_section = None 1079 1080 for i in range(header.Length): 1081 label = header.Labels[i] 1082 value = header.Values[i] 1083 1084 # Check for section headers 1085 if "===" in label or ( 1086 (value == "" or value is None) and not label.endswith(":") 1087 ): 1088 # This is a section header 1089 section_name = ( 1090 label.replace("=", "").replace(":", "").strip() 1091 ) # Clean the label if it contains '=' 1092 header_dic[section_name] = {} 1093 current_section = section_name 1094 else: 1095 if current_section: 1096 header_dic[current_section][label] = value 1097 else: 1098 header_dic[label] = value 1099 return header_dic
This code will extract the status logs from the raw file It has been tested on data from a Thermo Orbitrap ID-X, Astral and Q-Exactive, but may fail on other instrument types. It attempts to parse out section headers and sub-sections, but may not work for all instrument types. It will also not return Labels (keys) where the value is blank
Parameters:
retention_time: float The retention time in minutes to extract the status log data from. Will use the closest retention time found. Default 0.
Returns:
Dict[str, Any] A dictionary containing the status log information
Raises:
ValueError If no status logs are found in the raw file
1101 def get_error_logs(self): 1102 """ 1103 This code will extract the error logs from the raw file 1104 1105 Returns: 1106 -------- 1107 Dict[float, str] 1108 A dictionary containing the error log information with the retention time as the key 1109 1110 Raises: 1111 ------- 1112 ValueError 1113 If no error logs are found in the raw file 1114 """ 1115 1116 error_log_count = self.iRawDataPlus.RunHeaderEx.ErrorLogCount 1117 if error_log_count == 0: 1118 raise ValueError("No error logs found in the raw data file") 1119 return None 1120 1121 error_logs = {} 1122 1123 for i in range(error_log_count): 1124 error_log_item = self.iRawDataPlus.GetErrorLogItem(i) 1125 rt = error_log_item.RetentionTime 1126 message = error_log_item.Message 1127 # Use the index `i` as the unique ID key 1128 error_logs[i] = {"rt": rt, "message": message} 1129 return error_logs
This code will extract the error logs from the raw file
Returns:
Dict[float, str] A dictionary containing the error log information with the retention time as the key
Raises:
ValueError If no error logs are found in the raw file
1131 def get_sample_information(self): 1132 """ 1133 This code will extract the sample information from the raw file 1134 1135 Returns: 1136 -------- 1137 Dict[str, Any] 1138 A dictionary containing the sample information 1139 Note that UserText field may not be handled properly and may need further processing 1140 """ 1141 sminfo = self.iRawDataPlus.SampleInformation 1142 smdict = {} 1143 smdict["Comment"] = sminfo.Comment 1144 smdict["SampleId"] = sminfo.SampleId 1145 smdict["SampleName"] = sminfo.SampleName 1146 smdict["Vial"] = sminfo.Vial 1147 smdict["InjectionVolume"] = sminfo.InjectionVolume 1148 smdict["Barcode"] = sminfo.Barcode 1149 smdict["BarcodeStatus"] = str(sminfo.BarcodeStatus) 1150 smdict["CalibrationLevel"] = sminfo.CalibrationLevel 1151 smdict["DilutionFactor"] = sminfo.DilutionFactor 1152 smdict["InstrumentMethodFile"] = sminfo.InstrumentMethodFile 1153 smdict["RawFileName"] = sminfo.RawFileName 1154 smdict["CalibrationFile"] = sminfo.CalibrationFile 1155 smdict["IstdAmount"] = sminfo.IstdAmount 1156 smdict["RowNumber"] = sminfo.RowNumber 1157 smdict["Path"] = sminfo.Path 1158 smdict["ProcessingMethodFile"] = sminfo.ProcessingMethodFile 1159 smdict["SampleType"] = str(sminfo.SampleType) 1160 smdict["SampleWeight"] = sminfo.SampleWeight 1161 smdict["UserText"] = { 1162 "UserText": [x for x in sminfo.UserText] 1163 } # [0] #This may not work - needs debugging with 1164 return smdict
This code will extract the sample information from the raw file
Returns:
Dict[str, Any] A dictionary containing the sample information Note that UserText field may not be handled properly and may need further processing
1166 def get_instrument_data(self): 1167 """ 1168 This code will extract the instrument data from the raw file 1169 1170 Returns: 1171 -------- 1172 Dict[str, Any] 1173 A dictionary containing the instrument data 1174 """ 1175 instrument_data = self.iRawDataPlus.GetInstrumentData() 1176 id_dict = {} 1177 id_dict["Name"] = instrument_data.Name 1178 id_dict["Model"] = instrument_data.Model 1179 id_dict["SerialNumber"] = instrument_data.SerialNumber 1180 id_dict["SoftwareVersion"] = instrument_data.SoftwareVersion 1181 id_dict["HardwareVersion"] = instrument_data.HardwareVersion 1182 id_dict["ChannelLabels"] = { 1183 "ChannelLabels": [x for x in instrument_data.ChannelLabels] 1184 } 1185 id_dict["Flags"] = instrument_data.Flags 1186 id_dict["AxisLabelY"] = instrument_data.AxisLabelY 1187 id_dict["AxisLabelX"] = instrument_data.AxisLabelX 1188 return id_dict
This code will extract the instrument data from the raw file
Returns:
Dict[str, Any] A dictionary containing the instrument data
1190 def get_centroid_msms_data(self, scan): 1191 """ 1192 .. deprecated:: 2.0 1193 This function will be removed in CoreMS 2.0. Please use `get_average_mass_spectrum()` instead for similar functionality. 1194 """ 1195 1196 warnings.warn( 1197 "The `get_centroid_msms_data()` is deprecated as of CoreMS 2.0 and will be removed in a future version. " 1198 "Please use `get_average_mass_spectrum()` instead.", 1199 DeprecationWarning, 1200 ) 1201 1202 d_params = self.set_metadata(scans_list=[scan], label=Labels.thermo_centroid) 1203 1204 centroidStream = self.iRawDataPlus.GetCentroidStream(scan, False) 1205 1206 noise = list(centroidStream.Noises) 1207 1208 baselines = list(centroidStream.Baselines) 1209 1210 rp = list(centroidStream.Resolutions) 1211 1212 magnitude = list(centroidStream.Intensities) 1213 1214 mz = list(centroidStream.Masses) 1215 1216 # charge = scans_labels[5] 1217 array_noise_std = (np.array(noise) - np.array(baselines)) / 3 1218 l_signal_to_noise = np.array(magnitude) / array_noise_std 1219 1220 d_params["baseline_noise"] = np.average(array_noise_std) 1221 1222 d_params["baseline_noise_std"] = np.std(array_noise_std) 1223 1224 data_dict = { 1225 Labels.mz: mz, 1226 Labels.abundance: magnitude, 1227 Labels.rp: rp, 1228 Labels.s2n: list(l_signal_to_noise), 1229 } 1230 1231 mass_spec = MassSpecCentroid(data_dict, d_params, auto_process=False) 1232 mass_spec.settings.noise_threshold_method = "relative_abundance" 1233 mass_spec.settings.noise_threshold_min_relative_abundance = 1 1234 mass_spec.process_mass_spec() 1235 return mass_spec
Deprecated since version 2.0:
This function will be removed in CoreMS 2.0. Please use get_average_mass_spectrum() instead for similar functionality.
1237 def get_average_mass_spectrum_by_scanlist( 1238 self, 1239 scans_list: List[int], 1240 auto_process: bool = True, 1241 ppm_tolerance: float = 5.0, 1242 ) -> MassSpecProfile: 1243 """ 1244 Averages selected scans mass spectra using Thermo's AverageScans method 1245 scans_list: list[int] 1246 auto_process: bool 1247 If true performs peak picking, and noise threshold calculation after creation of mass spectrum object 1248 Returns: 1249 MassSpecProfile 1250 1251 .. deprecated:: 2.0 1252 This function will be removed in CoreMS 2.0. Please use `get_average_mass_spectrum()` instead for similar functionality. 1253 """ 1254 1255 warnings.warn( 1256 "The `get_average_mass_spectrum_by_scanlist()` is deprecated as of CoreMS 2.0 and will be removed in a future version. " 1257 "Please use `get_average_mass_spectrum()` instead.", 1258 DeprecationWarning, 1259 ) 1260 1261 d_params = self.set_metadata(scans_list=scans_list) 1262 1263 # assumes scans is full scan or reduced profile scan 1264 1265 scans = DotNetList[int]() 1266 for scan in scans_list: 1267 scans.Add(scan) 1268 1269 # Create the mass options object that will be used when averaging the scans 1270 options = MassOptions() 1271 options.ToleranceUnits = ToleranceUnits.ppm 1272 options.Tolerance = ppm_tolerance 1273 1274 # Get the scan filter for the first scan. This scan filter will be used to located 1275 # scans within the given scan range of the same type 1276 1277 averageScan = Extensions.AverageScans(self.iRawDataPlus, scans, options) 1278 1279 len_data = averageScan.SegmentedScan.Positions.Length 1280 1281 mz_list = list(averageScan.SegmentedScan.Positions) 1282 abund_list = list(averageScan.SegmentedScan.Intensities) 1283 1284 data_dict = { 1285 Labels.mz: mz_list, 1286 Labels.abundance: abund_list, 1287 } 1288 1289 mass_spec = MassSpecProfile(data_dict, d_params, auto_process=auto_process) 1290 1291 return mass_spec
Averages selected scans mass spectra using Thermo's AverageScans method scans_list: list[int] auto_process: bool If true performs peak picking, and noise threshold calculation after creation of mass spectrum object Returns: MassSpecProfile
Deprecated since version 2.0.
This function will be removed in CoreMS 2.0. Please use get_average_mass_spectrum() instead for similar functionality.
1294class ImportMassSpectraThermoMSFileReader(ThermoBaseClass, SpectraParserInterface): 1295 """A class for parsing Thermo RAW mass spectrometry data files and instatiating MassSpectraBase or LCMSBase objects 1296 1297 Parameters 1298 ---------- 1299 file_location : str or Path 1300 The path to the RAW file to be parsed. 1301 analyzer : str, optional 1302 The type of mass analyzer used in the instrument. Default is "Unknown". 1303 instrument_label : str, optional 1304 The name of the instrument used to acquire the data. Default is "Unknown". 1305 sample_name : str, optional 1306 The name of the sample being analyzed. If not provided, the stem of the file_location path will be used. 1307 1308 Attributes 1309 ---------- 1310 file_location : Path 1311 The path to the RAW file being parsed. 1312 analyzer : str 1313 The type of mass analyzer used in the instrument. 1314 instrument_label : str 1315 The name of the instrument used to acquire the data. 1316 sample_name : str 1317 The name of the sample being analyzed. 1318 1319 Methods 1320 ------- 1321 * run(spectra=True). 1322 Parses the RAW file and returns a dictionary of mass spectra dataframes and a scan metadata dataframe. 1323 * get_mass_spectrum_from_scan(scan_number, polarity, auto_process=True) 1324 Parses the RAW file and returns a MassSpecBase object from a single scan. 1325 * get_mass_spectra_obj(). 1326 Parses the RAW file and instantiates a MassSpectraBase object. 1327 * get_lcms_obj(). 1328 Parses the RAW file and instantiates an LCMSBase object. 1329 * get_icr_transient_times(). 1330 Return a list for transient time targets for all scans, or selected scans range 1331 1332 Inherits from ThermoBaseClass and SpectraParserInterface 1333 """ 1334 1335 def __init__( 1336 self, 1337 file_location, 1338 analyzer="Unknown", 1339 instrument_label="Unknown", 1340 sample_name=None, 1341 ): 1342 super().__init__(file_location) 1343 if isinstance(file_location, str): 1344 # if obj is a string it defaults to create a Path obj, pass the S3Path if needed 1345 file_location = Path(file_location) 1346 if not file_location.exists(): 1347 raise FileExistsError("File does not exist: " + str(file_location)) 1348 1349 self.file_location = file_location 1350 self.analyzer = analyzer 1351 self.instrument_label = instrument_label 1352 1353 if sample_name: 1354 self.sample_name = sample_name 1355 else: 1356 self.sample_name = file_location.stem 1357 1358 def load(self): 1359 pass 1360 1361 def get_scans_in_time_range( 1362 self, 1363 time_range: Union[Tuple[float, float], List[Tuple[float, float]]], 1364 ms_level: Optional[int] = None, 1365 ) -> List[int]: 1366 """Return scan numbers within specified retention time range(s). 1367 1368 Parameters 1369 ---------- 1370 time_range : tuple or list of tuples 1371 Retention time range(s) in minutes. Can be: 1372 - Single range: (start_time, end_time) 1373 - Multiple ranges: [(start1, end1), (start2, end2), ...] 1374 ms_level : int, optional 1375 If specified, only return scans of this MS level (e.g., 1 for MS1, 2 for MS2). 1376 If None, returns scans of all MS levels. 1377 1378 Returns 1379 ------- 1380 list of int 1381 List of scan numbers within the specified time range(s) and MS level. 1382 """ 1383 # Normalize time range to list of tuples 1384 time_ranges = self._normalize_time_range(time_range) 1385 1386 # Get all scan data 1387 scan_df = self.get_scan_df() 1388 1389 # Filter by time range 1390 mask = pd.Series([False] * len(scan_df), index=scan_df.index) 1391 for start_time, end_time in time_ranges: 1392 mask |= (scan_df.scan_time >= start_time) & (scan_df.scan_time <= end_time) 1393 1394 filtered_df = scan_df[mask] 1395 1396 # Filter by MS level if specified 1397 if ms_level is not None: 1398 filtered_df = filtered_df[filtered_df.ms_level == ms_level] 1399 1400 return filtered_df.scan.tolist() 1401 1402 def get_scan_df( 1403 self, 1404 time_range: Optional[ 1405 Union[Tuple[float, float], List[Tuple[float, float]]] 1406 ] = None, 1407 ): 1408 """Return scan data as a pandas DataFrame. 1409 1410 Parameters 1411 ---------- 1412 time_range : tuple or list of tuples, optional 1413 Retention time range(s) to filter scans. Can be: 1414 - Single range: (start_time, end_time) in minutes 1415 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 1416 If None, returns all scans. 1417 1418 Returns 1419 ------- 1420 pd.DataFrame 1421 DataFrame containing scan information, optionally filtered by time range. 1422 """ 1423 # This automatically brings in all the data 1424 self.chromatogram_settings.scans = (-1, -1) 1425 1426 # Get scan df info; starting with TIC data 1427 tic_data, _ = self.get_tic(ms_type="all", peak_detection=False, smooth=False) 1428 tic_data = { 1429 "scan": tic_data.scans, 1430 "scan_time": tic_data.time, 1431 "tic": tic_data.tic, 1432 } 1433 scan_df = pd.DataFrame.from_dict(tic_data) 1434 scan_df["ms_level"] = None 1435 1436 # get scan text 1437 scan_filter_df = pd.DataFrame.from_dict( 1438 self.get_all_filters()[0], orient="index" 1439 ) 1440 scan_filter_df.reset_index(inplace=True) 1441 scan_filter_df.rename(columns={"index": "scan", 0: "scan_text"}, inplace=True) 1442 1443 scan_df = scan_df.merge(scan_filter_df, on="scan", how="left") 1444 scan_df["scan_window_lower"] = scan_df.scan_text.str.extract( 1445 r"\[(\d+\.\d+)-\d+\.\d+\]" 1446 ) 1447 scan_df["scan_window_upper"] = scan_df.scan_text.str.extract( 1448 r"\[\d+\.\d+-(\d+\.\d+)\]" 1449 ) 1450 scan_df["polarity"] = np.where( 1451 scan_df.scan_text.str.contains(" - "), "negative", "positive" 1452 ) 1453 scan_df["precursor_mz"] = scan_df.scan_text.str.extract(r"(\d+\.\d+)@") 1454 scan_df["precursor_mz"] = scan_df["precursor_mz"].astype(float) 1455 1456 # Assign each scan as centroid or profile and add ms_level 1457 scan_df["ms_format"] = None 1458 for i in scan_df.scan.to_list(): 1459 scan_df.loc[scan_df.scan == i, "ms_level"] = self.get_ms_level_for_scan_num( 1460 i 1461 ) 1462 if self.iRawDataPlus.IsCentroidScanFromScanNumber(i): 1463 scan_df.loc[scan_df.scan == i, "ms_format"] = "centroid" 1464 else: 1465 scan_df.loc[scan_df.scan == i, "ms_format"] = "profile" 1466 1467 # Remove any non-mass spectra scans (e.g., MS level 0 or None) 1468 scan_df = scan_df[ 1469 scan_df.ms_level.notnull() & (scan_df.ms_level > 0) 1470 ].reset_index(drop=True) 1471 1472 # Filter by time range if specified 1473 if time_range is not None: 1474 time_ranges = self._normalize_time_range(time_range) 1475 mask = pd.Series([False] * len(scan_df), index=scan_df.index) 1476 for start_time, end_time in time_ranges: 1477 mask |= (scan_df.scan_time >= start_time) & ( 1478 scan_df.scan_time <= end_time 1479 ) 1480 scan_df = scan_df[mask].reset_index(drop=True) 1481 1482 return scan_df 1483 1484 def get_ms_raw( 1485 self, 1486 spectra, 1487 scan_df, 1488 time_range: Optional[ 1489 Union[Tuple[float, float], List[Tuple[float, float]]] 1490 ] = None, 1491 ): 1492 """Return a dictionary of mass spectra data as pandas DataFrames. 1493 1494 Parameters 1495 ---------- 1496 spectra : str 1497 Specifies which spectra to load (e.g., 'all', 'ms1', 'ms2') 1498 scan_df : pd.DataFrame 1499 Scan information DataFrame 1500 time_range : tuple or list of tuples, optional 1501 Retention time range(s) to filter scans. Can be: 1502 - Single range: (start_time, end_time) in minutes 1503 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 1504 If None, returns all scans. Note: filtering is typically done at scan_df level. 1505 1506 Returns 1507 ------- 1508 dict 1509 Dictionary of raw mass spectra data, optionally filtered by time range. 1510 """ 1511 # Note: time_range filtering is handled at the scan_df level before calling this method 1512 # The parameter is here for interface consistency with SpectraParserInterface 1513 1514 if spectra == "all": 1515 scan_df_forspec = scan_df 1516 elif spectra == "ms1": 1517 scan_df_forspec = scan_df[scan_df.ms_level == 1] 1518 elif spectra == "ms2": 1519 scan_df_forspec = scan_df[scan_df.ms_level == 2] 1520 else: 1521 raise ValueError("spectra must be 'none', 'all', 'ms1', or 'ms2'") 1522 1523 # Result container 1524 res = {} 1525 1526 # Row count container 1527 counter = {} 1528 1529 # Column name container 1530 cols = {} 1531 1532 # set at float32 1533 dtype = np.float32 1534 1535 # First pass: get nrows 1536 N = defaultdict(lambda: 0) 1537 for i in scan_df_forspec.scan.to_list(): 1538 level = scan_df_forspec.loc[scan_df_forspec.scan == i, "ms_level"].values[0] 1539 scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber(i) 1540 profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber( 1541 i, scanStatistics 1542 ) 1543 abun = list(profileStream.Intensities) 1544 abun = np.array(abun)[np.where(np.array(abun) > 0)[0]] 1545 1546 N[level] += len(abun) 1547 1548 # Second pass: parse 1549 for i in scan_df_forspec.scan.to_list(): 1550 scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber(i) 1551 profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber( 1552 i, scanStatistics 1553 ) 1554 abun = list(profileStream.Intensities) 1555 mz = list(profileStream.Positions) 1556 1557 # Get index of abun that are > 0 1558 inx = np.where(np.array(abun) > 0)[0] 1559 mz = np.array(mz)[inx] 1560 mz = np.float32(mz) 1561 abun = np.array(abun)[inx] 1562 abun = np.float32(abun) 1563 1564 level = scan_df_forspec.loc[scan_df_forspec.scan == i, "ms_level"].values[0] 1565 1566 # Number of rows 1567 n = len(mz) 1568 1569 # No measurements 1570 if n == 0: 1571 continue 1572 1573 # Dimension check 1574 if len(mz) != len(abun): 1575 warnings.warn("m/z and intensity array dimension mismatch") 1576 continue 1577 1578 # Scan/frame info 1579 id_dict = i 1580 1581 # Columns 1582 cols[level] = ["scan", "mz", "intensity"] 1583 m = len(cols[level]) 1584 1585 # Subarray init 1586 arr = np.empty((n, m), dtype=dtype) 1587 inx = 0 1588 1589 # Populate scan/frame info 1590 arr[:, inx] = i 1591 inx += 1 1592 1593 # Populate m/z 1594 arr[:, inx] = mz 1595 inx += 1 1596 1597 # Populate intensity 1598 arr[:, inx] = abun 1599 inx += 1 1600 1601 # Initialize output container 1602 if level not in res: 1603 res[level] = np.empty((N[level], m), dtype=dtype) 1604 counter[level] = 0 1605 1606 # Insert subarray 1607 res[level][counter[level] : counter[level] + n, :] = arr 1608 counter[level] += n 1609 1610 # Construct ms1 and ms2 mz dataframes 1611 for level in res.keys(): 1612 res[level] = pd.DataFrame(res[level]) 1613 res[level].columns = cols[level] 1614 # rename keys in res to add 'ms' prefix 1615 res = {f"ms{key}": value for key, value in res.items()} 1616 1617 return res 1618 1619 def run( 1620 self, 1621 spectra="all", 1622 scan_df=None, 1623 time_range: Optional[ 1624 Union[Tuple[float, float], List[Tuple[float, float]]] 1625 ] = None, 1626 ): 1627 """ 1628 Extracts mass spectra data from a raw file. 1629 1630 Parameters 1631 ---------- 1632 spectra : str, optional 1633 Which mass spectra data to include in the output. Default is all. Other options: none, ms1, ms2. 1634 scan_df : pandas.DataFrame, optional 1635 Scan dataframe. If not provided, the scan dataframe is created from the mzML file. 1636 time_range : tuple or list of tuples, optional 1637 Retention time range(s) to filter scans. Can be: 1638 - Single range: (start_time, end_time) in minutes 1639 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 1640 If None, returns all scans. 1641 1642 Returns 1643 ------- 1644 tuple 1645 A tuple containing two elements: 1646 - A dictionary containing mass spectra data, separated by MS level. 1647 - A pandas DataFrame containing scan information, including scan number, scan time, TIC, MS level, 1648 scan text, scan window lower and upper bounds, polarity, and precursor m/z (if applicable). 1649 """ 1650 # Prepare scan_df 1651 if scan_df is None: 1652 scan_df = self.get_scan_df(time_range=time_range) 1653 1654 # Prepare mass spectra data 1655 if spectra != "none": 1656 res = self.get_ms_raw( 1657 spectra=spectra, scan_df=scan_df, time_range=time_range 1658 ) 1659 else: 1660 res = None 1661 1662 return res, scan_df 1663 1664 def get_mass_spectra_from_scan_list( 1665 self, scan_list, spectrum_mode, auto_process=True 1666 ): 1667 """Instantiate multiple MassSpecBase objects from a list of scan numbers from the binary file. 1668 1669 Parameters 1670 ---------- 1671 scan_list : list[int] 1672 A list of scan numbers to extract the mass spectra from. 1673 spectrum_mode : str 1674 The type of mass spectrum to extract. Must be 'profile' or 'centroid'. 1675 All scans in the list must be of the same type. 1676 auto_process : bool, optional 1677 If True, perform peak picking and noise threshold calculation after creating the mass spectrum object. Default is True. 1678 """ 1679 mass_spectra = [] 1680 for scan in scan_list: 1681 mass_spectrum = self.get_mass_spectrum_from_scan( 1682 scan, spectrum_mode, auto_process=auto_process 1683 ) 1684 mass_spectra.append(mass_spectrum) 1685 1686 return mass_spectra 1687 1688 def get_mass_spectrum_from_scan( 1689 self, scan_number, spectrum_mode, auto_process=True 1690 ): 1691 """Instantiate a MassSpecBase object from a single scan number from the binary file. 1692 1693 Parameters 1694 ---------- 1695 scan_number : int 1696 The scan number to extract the mass spectrum from. 1697 polarity : int 1698 The polarity of the scan. 1 for positive mode, -1 for negative mode. 1699 spectrum_mode : str 1700 The type of mass spectrum to extract. Must be 'profile' or 'centroid'. 1701 auto_process : bool, optional 1702 If True, perform peak picking and noise threshold calculation after creating the mass spectrum object. Default is True. 1703 1704 Returns 1705 ------- 1706 MassSpecProfile | MassSpecCentroid 1707 The MassSpecProfile or MassSpecCentroid object containing the parsed mass spectrum. 1708 """ 1709 1710 if spectrum_mode == "profile": 1711 scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber(scan_number) 1712 profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber( 1713 scan_number, scanStatistics 1714 ) 1715 abun = list(profileStream.Intensities) 1716 mz = list(profileStream.Positions) 1717 data_dict = { 1718 Labels.mz: mz, 1719 Labels.abundance: abun, 1720 } 1721 d_params = self.set_metadata( 1722 firstScanNumber=scan_number, 1723 lastScanNumber=scan_number, 1724 scans_list=False, 1725 label=Labels.thermo_profile, 1726 ) 1727 mass_spectrum_obj = MassSpecProfile( 1728 data_dict, d_params, auto_process=auto_process 1729 ) 1730 1731 elif spectrum_mode == "centroid": 1732 centroid_scan = self.iRawDataPlus.GetCentroidStream(scan_number, False) 1733 if centroid_scan.Masses is not None: 1734 mz = list(centroid_scan.Masses) 1735 abun = list(centroid_scan.Intensities) 1736 rp = list(centroid_scan.Resolutions) 1737 magnitude = list(centroid_scan.Intensities) 1738 noise = list(centroid_scan.Noises) 1739 baselines = list(centroid_scan.Baselines) 1740 array_noise_std = (np.array(noise) - np.array(baselines)) / 3 1741 l_signal_to_noise = np.array(magnitude) / array_noise_std 1742 data_dict = { 1743 Labels.mz: mz, 1744 Labels.abundance: abun, 1745 Labels.rp: rp, 1746 Labels.s2n: list(l_signal_to_noise), 1747 } 1748 else: # For CID MS2, the centroid data are stored in the profile data location, they do not have any associated rp or baseline data, but they should be treated as centroid data 1749 scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber( 1750 scan_number 1751 ) 1752 profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber( 1753 scan_number, scanStatistics 1754 ) 1755 abun = list(profileStream.Intensities) 1756 mz = list(profileStream.Positions) 1757 data_dict = { 1758 Labels.mz: mz, 1759 Labels.abundance: abun, 1760 Labels.rp: [np.nan] * len(mz), 1761 Labels.s2n: [np.nan] * len(mz), 1762 } 1763 d_params = self.set_metadata( 1764 firstScanNumber=scan_number, 1765 lastScanNumber=scan_number, 1766 scans_list=False, 1767 label=Labels.thermo_centroid, 1768 ) 1769 mass_spectrum_obj = MassSpecCentroid( 1770 data_dict, d_params, auto_process=auto_process 1771 ) 1772 1773 return mass_spectrum_obj 1774 1775 def get_mass_spectra_obj( 1776 self, 1777 time_range: Optional[ 1778 Union[Tuple[float, float], List[Tuple[float, float]]] 1779 ] = None, 1780 ): 1781 """Instatiate a MassSpectraBase object from the binary data file file. 1782 1783 Parameters 1784 ---------- 1785 time_range : tuple or list of tuples, optional 1786 Retention time range(s) to load. Can be: 1787 - Single range: (start_time, end_time) in minutes 1788 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 1789 If None, loads all scans. Useful for targeted workflows to improve performance. 1790 1791 Returns 1792 ------- 1793 MassSpectraBase 1794 The MassSpectra object containing the parsed mass spectra. The object is instatiated with the mzML file, analyzer, instrument, sample name, and scan dataframe. 1795 """ 1796 _, scan_df = self.run(spectra="none", time_range=time_range) 1797 mass_spectra_obj = MassSpectraBase( 1798 self.file_location, 1799 self.analyzer, 1800 self.instrument_label, 1801 self.sample_name, 1802 self, 1803 ) 1804 scan_df = scan_df.set_index("scan", drop=False) 1805 mass_spectra_obj.scan_df = scan_df 1806 1807 return mass_spectra_obj 1808 1809 def get_lcms_obj( 1810 self, 1811 spectra="all", 1812 time_range: Optional[ 1813 Union[Tuple[float, float], List[Tuple[float, float]]] 1814 ] = None, 1815 ): 1816 """Instatiates a LCMSBase object from the mzML file. 1817 1818 Parameters 1819 ---------- 1820 spectra : str, optional 1821 Which mass spectra data to include in the output. Default is "all". Other options: "none", "ms1", "ms2". 1822 time_range : tuple or list of tuples, optional 1823 Retention time range(s) to load. Can be: 1824 - Single range: (start_time, end_time) in minutes 1825 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 1826 If None, loads all scans. Useful for targeted workflows to improve performance. 1827 1828 Returns 1829 ------- 1830 LCMSBase 1831 LCMS object containing mass spectra data. The object is instatiated with the file location, analyzer, instrument, sample name, scan info, mz dataframe (as specifified), polarity, as well as the attributes holding the scans, retention times, and tics. 1832 """ 1833 _, scan_df = self.run( 1834 spectra="none", time_range=time_range 1835 ) # first run it to just get scan info 1836 res, scan_df = self.run( 1837 scan_df=scan_df, spectra=spectra, time_range=time_range 1838 ) # second run to parse data 1839 lcms_obj = LCMSBase( 1840 self.file_location, 1841 self.analyzer, 1842 self.instrument_label, 1843 self.sample_name, 1844 self, 1845 ) 1846 if spectra != "none": 1847 for key in res: 1848 key_int = int(key.replace("ms", "")) 1849 res[key] = res[key][res[key].intensity > 0] 1850 res[key] = ( 1851 res[key].sort_values(by=["scan", "mz"]).reset_index(drop=True) 1852 ) 1853 lcms_obj._ms_unprocessed[key_int] = res[key] 1854 lcms_obj.scan_df = scan_df.set_index("scan", drop=False) 1855 # Check if polarity is mixed 1856 if len(set(scan_df.polarity)) > 1: 1857 raise ValueError("Mixed polarities detected in scan data") 1858 lcms_obj.polarity = scan_df.polarity.iloc[0] 1859 lcms_obj._scans_number_list = list(scan_df.scan) 1860 lcms_obj._retention_time_list = list(scan_df.scan_time) 1861 lcms_obj._tic_list = list(scan_df.tic) 1862 1863 return lcms_obj 1864 1865 def get_icr_transient_times(self): 1866 """Return a list for transient time targets for all scans, or selected scans range 1867 1868 Notes 1869 -------- 1870 Resolving Power and Transient time targets based on 7T FT-ICR MS system 1871 """ 1872 1873 res_trans_time = { 1874 "50": 0.384, 1875 "100000": 0.768, 1876 "200000": 1.536, 1877 "400000": 3.072, 1878 "750000": 6.144, 1879 "1000000": 12.288, 1880 } 1881 1882 firstScanNumber = self.start_scan 1883 1884 lastScanNumber = self.end_scan 1885 1886 transient_time_list = [] 1887 1888 for scan in range(firstScanNumber, lastScanNumber): 1889 scan_header = self.get_scan_header(scan) 1890 1891 rp_target = scan_header["FT Resolution:"] 1892 1893 transient_time = res_trans_time.get(rp_target) 1894 1895 transient_time_list.append(transient_time) 1896 1897 # print(transient_time, rp_target) 1898 1899 return transient_time_list
A class for parsing Thermo RAW mass spectrometry data files and instatiating 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
- run(spectra=True). Parses the RAW 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 RAW file and returns a MassSpecBase object from a single scan.
- get_mass_spectra_obj(). Parses the RAW file and instantiates a MassSpectraBase object.
- get_lcms_obj(). Parses the RAW file and instantiates an LCMSBase object.
- get_icr_transient_times(). Return a list for transient time targets for all scans, or selected scans range
Inherits from ThermoBaseClass and SpectraParserInterface
1335 def __init__( 1336 self, 1337 file_location, 1338 analyzer="Unknown", 1339 instrument_label="Unknown", 1340 sample_name=None, 1341 ): 1342 super().__init__(file_location) 1343 if isinstance(file_location, str): 1344 # if obj is a string it defaults to create a Path obj, pass the S3Path if needed 1345 file_location = Path(file_location) 1346 if not file_location.exists(): 1347 raise FileExistsError("File does not exist: " + str(file_location)) 1348 1349 self.file_location = file_location 1350 self.analyzer = analyzer 1351 self.instrument_label = instrument_label 1352 1353 if sample_name: 1354 self.sample_name = sample_name 1355 else: 1356 self.sample_name = file_location.stem
file_location: srt pathlib.Path or s3path.S3Path Thermo Raw file path
1361 def get_scans_in_time_range( 1362 self, 1363 time_range: Union[Tuple[float, float], List[Tuple[float, float]]], 1364 ms_level: Optional[int] = None, 1365 ) -> List[int]: 1366 """Return scan numbers within specified retention time range(s). 1367 1368 Parameters 1369 ---------- 1370 time_range : tuple or list of tuples 1371 Retention time range(s) in minutes. Can be: 1372 - Single range: (start_time, end_time) 1373 - Multiple ranges: [(start1, end1), (start2, end2), ...] 1374 ms_level : int, optional 1375 If specified, only return scans of this MS level (e.g., 1 for MS1, 2 for MS2). 1376 If None, returns scans of all MS levels. 1377 1378 Returns 1379 ------- 1380 list of int 1381 List of scan numbers within the specified time range(s) and MS level. 1382 """ 1383 # Normalize time range to list of tuples 1384 time_ranges = self._normalize_time_range(time_range) 1385 1386 # Get all scan data 1387 scan_df = self.get_scan_df() 1388 1389 # Filter by time range 1390 mask = pd.Series([False] * len(scan_df), index=scan_df.index) 1391 for start_time, end_time in time_ranges: 1392 mask |= (scan_df.scan_time >= start_time) & (scan_df.scan_time <= end_time) 1393 1394 filtered_df = scan_df[mask] 1395 1396 # Filter by MS level if specified 1397 if ms_level is not None: 1398 filtered_df = filtered_df[filtered_df.ms_level == ms_level] 1399 1400 return filtered_df.scan.tolist()
Return scan numbers within specified retention time range(s).
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.
1402 def get_scan_df( 1403 self, 1404 time_range: Optional[ 1405 Union[Tuple[float, float], List[Tuple[float, float]]] 1406 ] = None, 1407 ): 1408 """Return scan data as a pandas DataFrame. 1409 1410 Parameters 1411 ---------- 1412 time_range : tuple or list of tuples, optional 1413 Retention time range(s) to filter scans. Can be: 1414 - Single range: (start_time, end_time) in minutes 1415 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 1416 If None, returns all scans. 1417 1418 Returns 1419 ------- 1420 pd.DataFrame 1421 DataFrame containing scan information, optionally filtered by time range. 1422 """ 1423 # This automatically brings in all the data 1424 self.chromatogram_settings.scans = (-1, -1) 1425 1426 # Get scan df info; starting with TIC data 1427 tic_data, _ = self.get_tic(ms_type="all", peak_detection=False, smooth=False) 1428 tic_data = { 1429 "scan": tic_data.scans, 1430 "scan_time": tic_data.time, 1431 "tic": tic_data.tic, 1432 } 1433 scan_df = pd.DataFrame.from_dict(tic_data) 1434 scan_df["ms_level"] = None 1435 1436 # get scan text 1437 scan_filter_df = pd.DataFrame.from_dict( 1438 self.get_all_filters()[0], orient="index" 1439 ) 1440 scan_filter_df.reset_index(inplace=True) 1441 scan_filter_df.rename(columns={"index": "scan", 0: "scan_text"}, inplace=True) 1442 1443 scan_df = scan_df.merge(scan_filter_df, on="scan", how="left") 1444 scan_df["scan_window_lower"] = scan_df.scan_text.str.extract( 1445 r"\[(\d+\.\d+)-\d+\.\d+\]" 1446 ) 1447 scan_df["scan_window_upper"] = scan_df.scan_text.str.extract( 1448 r"\[\d+\.\d+-(\d+\.\d+)\]" 1449 ) 1450 scan_df["polarity"] = np.where( 1451 scan_df.scan_text.str.contains(" - "), "negative", "positive" 1452 ) 1453 scan_df["precursor_mz"] = scan_df.scan_text.str.extract(r"(\d+\.\d+)@") 1454 scan_df["precursor_mz"] = scan_df["precursor_mz"].astype(float) 1455 1456 # Assign each scan as centroid or profile and add ms_level 1457 scan_df["ms_format"] = None 1458 for i in scan_df.scan.to_list(): 1459 scan_df.loc[scan_df.scan == i, "ms_level"] = self.get_ms_level_for_scan_num( 1460 i 1461 ) 1462 if self.iRawDataPlus.IsCentroidScanFromScanNumber(i): 1463 scan_df.loc[scan_df.scan == i, "ms_format"] = "centroid" 1464 else: 1465 scan_df.loc[scan_df.scan == i, "ms_format"] = "profile" 1466 1467 # Remove any non-mass spectra scans (e.g., MS level 0 or None) 1468 scan_df = scan_df[ 1469 scan_df.ms_level.notnull() & (scan_df.ms_level > 0) 1470 ].reset_index(drop=True) 1471 1472 # Filter by time range if specified 1473 if time_range is not None: 1474 time_ranges = self._normalize_time_range(time_range) 1475 mask = pd.Series([False] * len(scan_df), index=scan_df.index) 1476 for start_time, end_time in time_ranges: 1477 mask |= (scan_df.scan_time >= start_time) & ( 1478 scan_df.scan_time <= end_time 1479 ) 1480 scan_df = scan_df[mask].reset_index(drop=True) 1481 1482 return scan_df
Return scan data as a pandas DataFrame.
Parameters
- 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
- pd.DataFrame: DataFrame containing scan information, optionally filtered by time range.
1484 def get_ms_raw( 1485 self, 1486 spectra, 1487 scan_df, 1488 time_range: Optional[ 1489 Union[Tuple[float, float], List[Tuple[float, float]]] 1490 ] = None, 1491 ): 1492 """Return a dictionary of mass spectra data as pandas DataFrames. 1493 1494 Parameters 1495 ---------- 1496 spectra : str 1497 Specifies which spectra to load (e.g., 'all', 'ms1', 'ms2') 1498 scan_df : pd.DataFrame 1499 Scan information DataFrame 1500 time_range : tuple or list of tuples, optional 1501 Retention time range(s) to filter scans. Can be: 1502 - Single range: (start_time, end_time) in minutes 1503 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 1504 If None, returns all scans. Note: filtering is typically done at scan_df level. 1505 1506 Returns 1507 ------- 1508 dict 1509 Dictionary of raw mass spectra data, optionally filtered by time range. 1510 """ 1511 # Note: time_range filtering is handled at the scan_df level before calling this method 1512 # The parameter is here for interface consistency with SpectraParserInterface 1513 1514 if spectra == "all": 1515 scan_df_forspec = scan_df 1516 elif spectra == "ms1": 1517 scan_df_forspec = scan_df[scan_df.ms_level == 1] 1518 elif spectra == "ms2": 1519 scan_df_forspec = scan_df[scan_df.ms_level == 2] 1520 else: 1521 raise ValueError("spectra must be 'none', 'all', 'ms1', or 'ms2'") 1522 1523 # Result container 1524 res = {} 1525 1526 # Row count container 1527 counter = {} 1528 1529 # Column name container 1530 cols = {} 1531 1532 # set at float32 1533 dtype = np.float32 1534 1535 # First pass: get nrows 1536 N = defaultdict(lambda: 0) 1537 for i in scan_df_forspec.scan.to_list(): 1538 level = scan_df_forspec.loc[scan_df_forspec.scan == i, "ms_level"].values[0] 1539 scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber(i) 1540 profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber( 1541 i, scanStatistics 1542 ) 1543 abun = list(profileStream.Intensities) 1544 abun = np.array(abun)[np.where(np.array(abun) > 0)[0]] 1545 1546 N[level] += len(abun) 1547 1548 # Second pass: parse 1549 for i in scan_df_forspec.scan.to_list(): 1550 scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber(i) 1551 profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber( 1552 i, scanStatistics 1553 ) 1554 abun = list(profileStream.Intensities) 1555 mz = list(profileStream.Positions) 1556 1557 # Get index of abun that are > 0 1558 inx = np.where(np.array(abun) > 0)[0] 1559 mz = np.array(mz)[inx] 1560 mz = np.float32(mz) 1561 abun = np.array(abun)[inx] 1562 abun = np.float32(abun) 1563 1564 level = scan_df_forspec.loc[scan_df_forspec.scan == i, "ms_level"].values[0] 1565 1566 # Number of rows 1567 n = len(mz) 1568 1569 # No measurements 1570 if n == 0: 1571 continue 1572 1573 # Dimension check 1574 if len(mz) != len(abun): 1575 warnings.warn("m/z and intensity array dimension mismatch") 1576 continue 1577 1578 # Scan/frame info 1579 id_dict = i 1580 1581 # Columns 1582 cols[level] = ["scan", "mz", "intensity"] 1583 m = len(cols[level]) 1584 1585 # Subarray init 1586 arr = np.empty((n, m), dtype=dtype) 1587 inx = 0 1588 1589 # Populate scan/frame info 1590 arr[:, inx] = i 1591 inx += 1 1592 1593 # Populate m/z 1594 arr[:, inx] = mz 1595 inx += 1 1596 1597 # Populate intensity 1598 arr[:, inx] = abun 1599 inx += 1 1600 1601 # Initialize output container 1602 if level not in res: 1603 res[level] = np.empty((N[level], m), dtype=dtype) 1604 counter[level] = 0 1605 1606 # Insert subarray 1607 res[level][counter[level] : counter[level] + n, :] = arr 1608 counter[level] += n 1609 1610 # Construct ms1 and ms2 mz dataframes 1611 for level in res.keys(): 1612 res[level] = pd.DataFrame(res[level]) 1613 res[level].columns = cols[level] 1614 # rename keys in res to add 'ms' prefix 1615 res = {f"ms{key}": value for key, value in res.items()} 1616 1617 return res
Return a dictionary of mass spectra data as pandas DataFrames.
Parameters
- spectra (str): Specifies which spectra to load (e.g., 'all', 'ms1', 'ms2')
- scan_df (pd.DataFrame): Scan information DataFrame
- 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: Dictionary of raw mass spectra data, optionally filtered by time range.
1619 def run( 1620 self, 1621 spectra="all", 1622 scan_df=None, 1623 time_range: Optional[ 1624 Union[Tuple[float, float], List[Tuple[float, float]]] 1625 ] = None, 1626 ): 1627 """ 1628 Extracts mass spectra data from a raw file. 1629 1630 Parameters 1631 ---------- 1632 spectra : str, optional 1633 Which mass spectra data to include in the output. Default is all. Other options: none, ms1, ms2. 1634 scan_df : pandas.DataFrame, optional 1635 Scan dataframe. If not provided, the scan dataframe is created from the mzML file. 1636 time_range : tuple or list of tuples, optional 1637 Retention time range(s) to filter scans. Can be: 1638 - Single range: (start_time, end_time) in minutes 1639 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 1640 If None, returns all scans. 1641 1642 Returns 1643 ------- 1644 tuple 1645 A tuple containing two elements: 1646 - A dictionary containing mass spectra data, separated by MS level. 1647 - A pandas DataFrame containing scan information, including scan number, scan time, TIC, MS level, 1648 scan text, scan window lower and upper bounds, polarity, and precursor m/z (if applicable). 1649 """ 1650 # Prepare scan_df 1651 if scan_df is None: 1652 scan_df = self.get_scan_df(time_range=time_range) 1653 1654 # Prepare mass spectra data 1655 if spectra != "none": 1656 res = self.get_ms_raw( 1657 spectra=spectra, scan_df=scan_df, time_range=time_range 1658 ) 1659 else: 1660 res = None 1661 1662 return res, scan_df
Extracts mass spectra data from a raw file.
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 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
- tuple: A tuple containing two elements:
- A dictionary containing mass spectra data, separated by MS level.
- A pandas DataFrame containing scan information, including scan number, scan time, TIC, MS level, scan text, scan window lower and upper bounds, polarity, and precursor m/z (if applicable).
1664 def get_mass_spectra_from_scan_list( 1665 self, scan_list, spectrum_mode, auto_process=True 1666 ): 1667 """Instantiate multiple MassSpecBase objects from a list of scan numbers from the binary file. 1668 1669 Parameters 1670 ---------- 1671 scan_list : list[int] 1672 A list of scan numbers to extract the mass spectra from. 1673 spectrum_mode : str 1674 The type of mass spectrum to extract. Must be 'profile' or 'centroid'. 1675 All scans in the list must be of the same type. 1676 auto_process : bool, optional 1677 If True, perform peak picking and noise threshold calculation after creating the mass spectrum object. Default is True. 1678 """ 1679 mass_spectra = [] 1680 for scan in scan_list: 1681 mass_spectrum = self.get_mass_spectrum_from_scan( 1682 scan, spectrum_mode, auto_process=auto_process 1683 ) 1684 mass_spectra.append(mass_spectrum) 1685 1686 return mass_spectra
Instantiate multiple MassSpecBase objects from a list of scan numbers from the binary file.
Parameters
- scan_list (list[int]): A list of scan numbers to extract the mass spectra from.
- spectrum_mode (str): The type of mass spectrum to extract. Must be 'profile' or 'centroid'. All scans in the list must be of the same type.
- auto_process (bool, optional): If True, perform peak picking and noise threshold calculation after creating the mass spectrum object. Default is True.
1688 def get_mass_spectrum_from_scan( 1689 self, scan_number, spectrum_mode, auto_process=True 1690 ): 1691 """Instantiate a MassSpecBase object from a single scan number from the binary file. 1692 1693 Parameters 1694 ---------- 1695 scan_number : int 1696 The scan number to extract the mass spectrum from. 1697 polarity : int 1698 The polarity of the scan. 1 for positive mode, -1 for negative mode. 1699 spectrum_mode : str 1700 The type of mass spectrum to extract. Must be 'profile' or 'centroid'. 1701 auto_process : bool, optional 1702 If True, perform peak picking and noise threshold calculation after creating the mass spectrum object. Default is True. 1703 1704 Returns 1705 ------- 1706 MassSpecProfile | MassSpecCentroid 1707 The MassSpecProfile or MassSpecCentroid object containing the parsed mass spectrum. 1708 """ 1709 1710 if spectrum_mode == "profile": 1711 scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber(scan_number) 1712 profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber( 1713 scan_number, scanStatistics 1714 ) 1715 abun = list(profileStream.Intensities) 1716 mz = list(profileStream.Positions) 1717 data_dict = { 1718 Labels.mz: mz, 1719 Labels.abundance: abun, 1720 } 1721 d_params = self.set_metadata( 1722 firstScanNumber=scan_number, 1723 lastScanNumber=scan_number, 1724 scans_list=False, 1725 label=Labels.thermo_profile, 1726 ) 1727 mass_spectrum_obj = MassSpecProfile( 1728 data_dict, d_params, auto_process=auto_process 1729 ) 1730 1731 elif spectrum_mode == "centroid": 1732 centroid_scan = self.iRawDataPlus.GetCentroidStream(scan_number, False) 1733 if centroid_scan.Masses is not None: 1734 mz = list(centroid_scan.Masses) 1735 abun = list(centroid_scan.Intensities) 1736 rp = list(centroid_scan.Resolutions) 1737 magnitude = list(centroid_scan.Intensities) 1738 noise = list(centroid_scan.Noises) 1739 baselines = list(centroid_scan.Baselines) 1740 array_noise_std = (np.array(noise) - np.array(baselines)) / 3 1741 l_signal_to_noise = np.array(magnitude) / array_noise_std 1742 data_dict = { 1743 Labels.mz: mz, 1744 Labels.abundance: abun, 1745 Labels.rp: rp, 1746 Labels.s2n: list(l_signal_to_noise), 1747 } 1748 else: # For CID MS2, the centroid data are stored in the profile data location, they do not have any associated rp or baseline data, but they should be treated as centroid data 1749 scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber( 1750 scan_number 1751 ) 1752 profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber( 1753 scan_number, scanStatistics 1754 ) 1755 abun = list(profileStream.Intensities) 1756 mz = list(profileStream.Positions) 1757 data_dict = { 1758 Labels.mz: mz, 1759 Labels.abundance: abun, 1760 Labels.rp: [np.nan] * len(mz), 1761 Labels.s2n: [np.nan] * len(mz), 1762 } 1763 d_params = self.set_metadata( 1764 firstScanNumber=scan_number, 1765 lastScanNumber=scan_number, 1766 scans_list=False, 1767 label=Labels.thermo_centroid, 1768 ) 1769 mass_spectrum_obj = MassSpecCentroid( 1770 data_dict, d_params, auto_process=auto_process 1771 ) 1772 1773 return mass_spectrum_obj
Instantiate a MassSpecBase object from a single scan number from the binary file.
Parameters
- scan_number (int): The scan number to extract the mass spectrum from.
- polarity (int): The polarity of the scan. 1 for positive mode, -1 for negative mode.
- spectrum_mode (str): The type of mass spectrum to extract. Must be 'profile' or 'centroid'.
- auto_process (bool, optional): If True, perform peak picking and noise threshold calculation after creating the mass spectrum object. Default is True.
Returns
- MassSpecProfile | MassSpecCentroid: The MassSpecProfile or MassSpecCentroid object containing the parsed mass spectrum.
1775 def get_mass_spectra_obj( 1776 self, 1777 time_range: Optional[ 1778 Union[Tuple[float, float], List[Tuple[float, float]]] 1779 ] = None, 1780 ): 1781 """Instatiate a MassSpectraBase object from the binary data file file. 1782 1783 Parameters 1784 ---------- 1785 time_range : tuple or list of tuples, optional 1786 Retention time range(s) to load. Can be: 1787 - Single range: (start_time, end_time) in minutes 1788 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 1789 If None, loads all scans. Useful for targeted workflows to improve performance. 1790 1791 Returns 1792 ------- 1793 MassSpectraBase 1794 The MassSpectra object containing the parsed mass spectra. The object is instatiated with the mzML file, analyzer, instrument, sample name, and scan dataframe. 1795 """ 1796 _, scan_df = self.run(spectra="none", time_range=time_range) 1797 mass_spectra_obj = MassSpectraBase( 1798 self.file_location, 1799 self.analyzer, 1800 self.instrument_label, 1801 self.sample_name, 1802 self, 1803 ) 1804 scan_df = scan_df.set_index("scan", drop=False) 1805 mass_spectra_obj.scan_df = scan_df 1806 1807 return mass_spectra_obj
Instatiate a MassSpectraBase object from the binary data file 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. Useful for targeted workflows to improve performance.
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.
1809 def get_lcms_obj( 1810 self, 1811 spectra="all", 1812 time_range: Optional[ 1813 Union[Tuple[float, float], List[Tuple[float, float]]] 1814 ] = None, 1815 ): 1816 """Instatiates a LCMSBase object from the mzML file. 1817 1818 Parameters 1819 ---------- 1820 spectra : str, optional 1821 Which mass spectra data to include in the output. Default is "all". Other options: "none", "ms1", "ms2". 1822 time_range : tuple or list of tuples, optional 1823 Retention time range(s) to load. Can be: 1824 - Single range: (start_time, end_time) in minutes 1825 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 1826 If None, loads all scans. Useful for targeted workflows to improve performance. 1827 1828 Returns 1829 ------- 1830 LCMSBase 1831 LCMS object containing mass spectra data. The object is instatiated with the file location, analyzer, instrument, sample name, scan info, mz dataframe (as specifified), polarity, as well as the attributes holding the scans, retention times, and tics. 1832 """ 1833 _, scan_df = self.run( 1834 spectra="none", time_range=time_range 1835 ) # first run it to just get scan info 1836 res, scan_df = self.run( 1837 scan_df=scan_df, spectra=spectra, time_range=time_range 1838 ) # second run to parse data 1839 lcms_obj = LCMSBase( 1840 self.file_location, 1841 self.analyzer, 1842 self.instrument_label, 1843 self.sample_name, 1844 self, 1845 ) 1846 if spectra != "none": 1847 for key in res: 1848 key_int = int(key.replace("ms", "")) 1849 res[key] = res[key][res[key].intensity > 0] 1850 res[key] = ( 1851 res[key].sort_values(by=["scan", "mz"]).reset_index(drop=True) 1852 ) 1853 lcms_obj._ms_unprocessed[key_int] = res[key] 1854 lcms_obj.scan_df = scan_df.set_index("scan", drop=False) 1855 # Check if polarity is mixed 1856 if len(set(scan_df.polarity)) > 1: 1857 raise ValueError("Mixed polarities detected in scan data") 1858 lcms_obj.polarity = scan_df.polarity.iloc[0] 1859 lcms_obj._scans_number_list = list(scan_df.scan) 1860 lcms_obj._retention_time_list = list(scan_df.scan_time) 1861 lcms_obj._tic_list = list(scan_df.tic) 1862 1863 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 file location, analyzer, instrument, sample name, scan info, mz dataframe (as specifified), polarity, as well as the attributes holding the scans, retention times, and tics.
1865 def get_icr_transient_times(self): 1866 """Return a list for transient time targets for all scans, or selected scans range 1867 1868 Notes 1869 -------- 1870 Resolving Power and Transient time targets based on 7T FT-ICR MS system 1871 """ 1872 1873 res_trans_time = { 1874 "50": 0.384, 1875 "100000": 0.768, 1876 "200000": 1.536, 1877 "400000": 3.072, 1878 "750000": 6.144, 1879 "1000000": 12.288, 1880 } 1881 1882 firstScanNumber = self.start_scan 1883 1884 lastScanNumber = self.end_scan 1885 1886 transient_time_list = [] 1887 1888 for scan in range(firstScanNumber, lastScanNumber): 1889 scan_header = self.get_scan_header(scan) 1890 1891 rp_target = scan_header["FT Resolution:"] 1892 1893 transient_time = res_trans_time.get(rp_target) 1894 1895 transient_time_list.append(transient_time) 1896 1897 # print(transient_time, rp_target) 1898 1899 return transient_time_list
Return a list for transient time targets for all scans, or selected scans range
Notes
Resolving Power and Transient time targets based on 7T FT-ICR MS system
Inherited Members
- ThermoBaseClass
- iRawDataPlus
- iFileHeader
- res
- file_path
- parameters
- chromatogram_settings
- scans
- start_scan
- end_scan
- set_msordertype
- get_instrument_info
- get_creation_time
- remove_temp_file
- close_file
- get_polarity_mode
- get_filter_for_scan_num
- get_ms_level_for_scan_num
- check_full_scan
- get_all_filters
- get_scan_header
- get_rt_time_from_trace
- get_eics
- get_tic
- get_average_mass_spectrum
- set_metadata
- get_instrument_methods
- get_tune_method
- get_status_log
- get_error_logs
- get_sample_information
- get_instrument_data
- get_centroid_msms_data
- get_average_mass_spectrum_by_scanlist