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
spec = ModuleSpec(name='corems', loader=<_frozen_importlib_external.SourceFileLoader object>, origin='/Users/heal742/LOCAL/corems_dev/corems/corems/__init__.py', submodule_search_locations=['/Users/heal742/LOCAL/corems_dev/corems/corems'])
class ThermoBaseClass:
  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.
ThermoBaseClass(file_location)
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

iRawDataPlus
res
file_path
iFileHeader
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.

scans: list | tuple
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

start_scan: int
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.

end_scan: int
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.

def set_msordertype(self, scanFilter, mstype: str = 'ms1'):
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'

def get_instrument_info(self) -> dict:
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'.

def get_creation_time(self) -> datetime.datetime:
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.

def remove_temp_file(self) -> None:
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

def close_file(self) -> None:
280    def close_file(self) -> None:
281        """
282        Close the Thermo Raw file.
283        """
284        self.iRawDataPlus.Dispose()

Close the Thermo Raw file.

def get_polarity_mode(self, scan_number: int) -> int:
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.

def get_filter_for_scan_num(self, scan_number: int) -> List[str]:
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.

def get_ms_level_for_scan_num(self, scan_number: int) -> str:
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.)

def check_full_scan(self, scan_number: int) -> bool:
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
def get_all_filters(self) -> Tuple[Dict[int, str], List[str]]:
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]']

def get_scan_header(self, scan: int) -> Dict[str, Any]:
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.

@staticmethod
def get_rt_time_from_trace(trace) -> Tuple[List[float], List[float], List[int]]:
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

def get_eics( self, target_mzs: List[float], tic_data: Dict[str, Any], ms_type='MS !d', peak_detection=False, smooth=False, plot=False, ax: Optional[matplotlib.axes._axes.Axes] = None, legend=False) -> Tuple[Dict[float, corems.mass_spectra.factory.chromat_data.EIC_Data], matplotlib.axes._axes.Axes]:
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
                            )
def get_tic( self, ms_type='MS !d', peak_detection=False, smooth=False, plot=False, ax=None, trace_type='TIC') -> Tuple[corems.mass_spectra.factory.chromat_data.TIC_Data, matplotlib.axes._axes.Axes]:
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 }

def get_average_mass_spectrum( self, spectrum_mode: str = 'profile', auto_process: bool = True, ppm_tolerance: float = 5.0, ms_type: str = 'MS1') -> corems.mass_spectrum.factory.MassSpectrumClasses.MassSpecProfile | corems.mass_spectrum.factory.MassSpectrumClasses.MassSpecCentroid:
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.

def set_metadata( self, firstScanNumber=0, lastScanNumber=0, scans_list=False, label='Thermo_Profile'):
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

def get_instrument_methods(self, parse_strings: bool = True):
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.

def get_tune_method(self):
 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

def get_status_log(self, retention_time: float = 0):
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

def get_error_logs(self):
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

def get_sample_information(self):
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

def get_instrument_data(self):
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

def get_centroid_msms_data(self, scan):
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.

def get_average_mass_spectrum_by_scanlist( self, scans_list: List[int], auto_process: bool = True, ppm_tolerance: float = 5.0) -> corems.mass_spectrum.factory.MassSpectrumClasses.MassSpecProfile:
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.

class ImportMassSpectraThermoMSFileReader(ThermoBaseClass, corems.mass_spectra.input.parserbase.SpectraParserInterface):
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

ImportMassSpectraThermoMSFileReader( file_location, analyzer='Unknown', instrument_label='Unknown', sample_name=None)
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

file_location
analyzer
instrument_label
def load(self):
1339    def load(self):
1340        pass

Load mass spectra data.

def get_scans_in_time_range( self, time_range: Union[Tuple[float, float], List[Tuple[float, float]]], ms_level: Optional[int] = None) -> List[int]:
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.
def get_scan_df( self, time_range: Union[Tuple[float, float], List[Tuple[float, float]], NoneType] = None):
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.
def get_ms_raw( self, spectra, scan_df, time_range: Union[Tuple[float, float], List[Tuple[float, float]], NoneType] = None):
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.
def run( self, spectra='all', scan_df=None, time_range: Union[Tuple[float, float], List[Tuple[float, float]], NoneType] = None):
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).
def get_mass_spectra_from_scan_list(self, scan_list, spectrum_mode, auto_process=True):
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.
def get_mass_spectrum_from_scan(self, scan_number, spectrum_mode, auto_process=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.
def get_mass_spectra_obj( self, time_range: Union[Tuple[float, float], List[Tuple[float, float]], NoneType] = None):
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.
def get_lcms_obj( self, spectra='all', time_range: Union[Tuple[float, float], List[Tuple[float, float]], NoneType] = None):
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.
def get_icr_transient_times(self):
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