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