corems.mass_spectra.input.rawFileReader

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

Class for parsing Thermo Raw files and extracting information from them.

Parameters:

file_location : str or pathlib.Path or s3path.S3Path Thermo Raw file path or S3 path.

Attributes:

file_path : str or pathlib.Path or s3path.S3Path The file path of the Thermo Raw file. parameters : LCMSParameters The LCMS parameters for the Thermo Raw file. chromatogram_settings : LiquidChromatographSetting The chromatogram settings for the Thermo Raw file. scans : list or tuple The selected scans for the Thermo Raw file. start_scan : int The starting scan number for the Thermo Raw file. end_scan : int The ending scan number for the Thermo Raw file.

Methods:

  • set_msordertype(scanFilter, mstype: str = 'ms1') -> scanFilter Convert the user-passed MS Type string to a Thermo MSOrderType object.
  • get_instrument_info() -> dict Get the instrument information from the Thermo Raw file.
  • get_creation_time() -> datetime.datetime Extract the creation date stamp from the .RAW file and return it as a formatted datetime object.
  • remove_temp_file() Remove the temporary file if the path is from S3Path.
  • get_polarity_mode(scan_number: int) -> int Get the polarity mode for the given scan number.
  • get_filter_for_scan_num(scan_number: int) -> List[str] Get the filter for the given scan number.
  • check_full_scan(scan_number: int) -> bool Check if the given scan number is a full scan.
  • get_all_filters() -> Tuple[Dict[int, str], List[str]] Get all scan filters for the Thermo Raw file.
  • get_scan_header(scan: int) -> Dict[str, Any] Get the full dictionary of scan header metadata for the given scan number.
  • get_rt_time_from_trace(trace) -> Tuple[List[float], List[float], List[int]] Get the retention time, intensity, and scan number from the given trace.
  • get_eics(target_mzs: List[float], tic_data: Dict[str, Any], ms_type: str = 'MS !d', peak_detection: bool = True, smooth: bool = True, plot: bool = False, ax: Optional[matplotlib.axes.Axes] = None, legend: bool = False) -> Tuple[Dict[float, EIC_Data], matplotlib.axes.Axes] Get the extracted ion chromatograms (EICs) for the target m/z values.
ThermoBaseClass(file_location)
111    def __init__(self, file_location):
112        """file_location: srt pathlib.Path or s3path.S3Path
113        Thermo Raw file path
114        """
115        # Thread.__init__(self)
116        if isinstance(file_location, str):
117            file_path = Path(file_location)
118
119        elif isinstance(file_location, S3Path):
120            temp_dir = Path("tmp/")
121            temp_dir.mkdir(exist_ok=True)
122
123            file_path = temp_dir / file_location.name
124            with open(file_path, "wb") as fh:
125                fh.write(file_location.read_bytes())
126
127        else:
128            file_path = file_location
129
130        self.iRawDataPlus = RawFileReaderAdapter.FileFactory(str(file_path))
131        self.iFileHeader = None
132
133        if not self.iRawDataPlus.IsOpen:
134            self.close_file()
135            raise FileNotFoundError(
136                "Unable to access the RAW file using the RawFileReader class!"
137            )
138
139        # Check for any errors in the RAW file
140        if self.iRawDataPlus.IsError:
141            file_error = self.iRawDataPlus.FileError
142            self.close_file()
143            raise IOError("Error opening ({}) - {}".format(file_error, file_path))
144
145        self.res = self.iRawDataPlus.SelectInstrument(Device.MS, 1)
146
147        self.file_path = file_location
148        self.iFileHeader = FileHeaderReaderFactory.ReadFile(str(file_path))
149
150        # removing tmp file
151
152        self._init_settings()

file_location: srt pathlib.Path or s3path.S3Path Thermo Raw file path

iRawDataPlus
iFileHeader
res
file_path
160    @property
161    def parameters(self) -> LCMSParameters:
162        """
163        Get or set the LCMSParameters object.
164        """
165        return self._parameters

Get or set the LCMSParameters object.

171    @property
172    def chromatogram_settings(self) -> LiquidChromatographSetting:
173        """
174        Get or set the LiquidChromatographSetting object.
175        """
176        return self.parameters.lc_ms

Get or set the LiquidChromatographSetting object.

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        """Dispose the Thermo file handles.
282
283        A second call is a no-op. This does not delete an S3 temp copy;
284        ``remove_temp_file`` does that.
285        """
286        for attr in ("iRawDataPlus", "iFileHeader"):
287            handle = getattr(self, attr, None)
288            if handle is None:
289                continue
290            dispose = getattr(handle, "Dispose", None)
291            setattr(self, attr, None)
292            if callable(dispose):
293                dispose()

Dispose the Thermo file handles.

A second call is a no-op. This does not delete an S3 temp copy; remove_temp_file does that.

def get_polarity_mode(self, scan_number: int) -> int:
301    def get_polarity_mode(self, scan_number: int) -> int:
302        """
303        Get the polarity mode for the given scan number.
304
305        Parameters:
306        -----------
307        scan_number : int
308            The scan number.
309
310        Raises:
311        -------
312        Exception
313            If the polarity mode is unknown.
314
315        """
316        polarity_symbol = self.get_filter_for_scan_num(scan_number)[1]
317
318        if polarity_symbol == "+":
319            return 1
320            # return 'POSITIVE_ION_MODE'
321
322        elif polarity_symbol == "-":
323            return -1
324
325        else:
326            raise Exception("Polarity Mode Unknown, please set it manually")

Get the polarity mode for the given scan number.

Parameters:

scan_number : int The scan number.

Raises:

Exception If the polarity mode is unknown.

def get_filter_for_scan_num(self, scan_number: int) -> List[str]:
328    def get_filter_for_scan_num(self, scan_number: int) -> List[str]:
329        """
330        Returns the closest matching run time that corresponds to scan_number for the current
331        controller. This function is only supported for MS device controllers.
332        e.g.  ['FTMS', '-', 'p', 'NSI', 'Full', 'ms', '[200.00-1000.00]']
333
334        Parameters:
335        -----------
336        scan_number : int
337            The scan number.
338
339        """
340        scan_label = self.iRawDataPlus.GetScanEventStringForScanNumber(scan_number)
341
342        return str(scan_label).split()

Returns the closest matching run time that corresponds to scan_number for the current controller. This function is only supported for MS device controllers. e.g. ['FTMS', '-', 'p', 'NSI', 'Full', 'ms', '[200.00-1000.00]']

Parameters:

scan_number : int The scan number.

def get_ms_level_for_scan_num(self, scan_number: int) -> str:
344    def get_ms_level_for_scan_num(self, scan_number: int) -> str:
345        """
346        Get the MS order for the given scan number.
347
348        Parameters:
349        -----------
350        scan_number : int
351            The scan number
352
353        Returns:
354        --------
355        int
356            The MS order type (1 for MS, 2 for MS2, etc.)
357        """
358        scan_filter = self.iRawDataPlus.GetFilterForScanNumber(scan_number)
359
360        msordertype = {
361            MSOrderType.Ms: 1,
362            MSOrderType.Ms2: 2,
363            MSOrderType.Ms3: 3,
364            MSOrderType.Ms4: 4,
365            MSOrderType.Ms5: 5,
366            MSOrderType.Ms6: 6,
367            MSOrderType.Ms7: 7,
368            MSOrderType.Ms8: 8,
369            MSOrderType.Ms9: 9,
370            MSOrderType.Ms10: 10,
371        }
372
373        if scan_filter.MSOrder in msordertype:
374            return msordertype[scan_filter.MSOrder]
375        else:
376            raise Exception("MS Order Type not found")

Get the MS order for the given scan number.

Parameters:

scan_number : int The scan number

Returns:

int The MS order type (1 for MS, 2 for MS2, etc.)

def check_full_scan(self, scan_number: int) -> bool:
378    def check_full_scan(self, scan_number: int) -> bool:
379        # scan_filter.ScanMode 0 = FULL
380        scan_filter = self.iRawDataPlus.GetFilterForScanNumber(scan_number)
381
382        return scan_filter.ScanMode == MSOrderType.Ms
def get_all_filters(self) -> Tuple[Dict[int, str], List[str]]:
384    def get_all_filters(self) -> Tuple[Dict[int, str], List[str]]:
385        """
386        Get all scan filters.
387        This function is only supported for MS device controllers.
388        e.g.  ['FTMS', '-', 'p', 'NSI', 'Full', 'ms', '[200.00-1000.00]']
389
390        """
391
392        scanrange = range(self.start_scan, self.end_scan + 1)
393        scanfiltersdic = {}
394        scanfilterslist = []
395        for scan_number in scanrange:
396            scan_label = self.iRawDataPlus.GetScanEventStringForScanNumber(scan_number)
397            scanfiltersdic[scan_number] = scan_label
398            scanfilterslist.append(scan_label)
399        scanfilterset = list(set(scanfilterslist))
400        return scanfiltersdic, scanfilterset

Get all scan filters. This function is only supported for MS device controllers. e.g. ['FTMS', '-', 'p', 'NSI', 'Full', 'ms', '[200.00-1000.00]']

def get_scan_header(self, scan: int) -> Dict[str, Any]:
402    def get_scan_header(self, scan: int) -> Dict[str, Any]:
403        """
404        Get full dictionary of scan header meta data, i.e. AGC status, ion injection time, etc.
405
406        Parameters:
407        -----------
408        scan : int
409            The scan number.
410
411        """
412        header = self.iRawDataPlus.GetTrailerExtraInformation(scan)
413
414        header_dic = {}
415        for i in range(header.Length):
416            header_dic.update({header.Labels[i]: header.Values[i]})
417        return header_dic

Get full dictionary of scan header meta data, i.e. AGC status, ion injection time, etc.

Parameters:

scan : int The scan number.

@staticmethod
def get_rt_time_from_trace(trace) -> Tuple[List[float], List[float], List[int]]:
419    @staticmethod
420    def get_rt_time_from_trace(trace) -> Tuple[List[float], List[float], List[int]]:
421        """trace: ThermoFisher.CommonCore.Data.Business.ChromatogramSignal"""
422        return list(trace.Times), list(trace.Intensities), list(trace.Scans)

trace: ThermoFisher.CommonCore.Data.Business.ChromatogramSignal

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]:
424    def get_eics(
425        self,
426        target_mzs: List[float],
427        tic_data: Dict[str, Any],
428        ms_type="MS !d",
429        peak_detection=False,
430        smooth=False,
431        plot=False,
432        ax: Optional[axes.Axes] = None,
433        legend=False,
434    ) -> Tuple[Dict[float, EIC_Data], axes.Axes]:
435        """ms_type: str ('MS', MS2')
436        start_scan: int default -1 will select the lowest available
437        end_scan: int default -1 will select the highest available
438
439        returns:
440
441            chroma: dict{target_mz: EIC_Data(
442                                        Scans: [int]
443                                            original thermo scan numbers
444                                        Time: [floats]
445                                            list of retention times
446                                        TIC: [floats]
447                                            total ion chromatogram
448                                        Apexes: [int]
449                                            original thermo apex scan number after peak picking
450                                        )
451
452        """
453        # If peak_detection or smooth is True, raise exception
454        if peak_detection or smooth:
455            raise Exception(
456                "Peak detection and smoothing are no longer implemented in this function"
457            )
458
459        options = MassOptions()
460        options.ToleranceUnits = ToleranceUnits.ppm
461        options.Tolerance = self.chromatogram_settings.eic_tolerance_ppm
462
463        all_chroma_settings = []
464
465        for target_mz in target_mzs:
466            settings = ChromatogramTraceSettings(TraceType.MassRange)
467            settings.Filter = ms_type
468            settings.MassRanges = [Range(target_mz, target_mz)]
469
470            chroma_settings = IChromatogramSettings(settings)
471
472            all_chroma_settings.append(chroma_settings)
473
474        # chroma_settings2 = IChromatogramSettings(settings)
475        # print(chroma_settings.FragmentMass)
476        # print(chroma_settings.FragmentMass)
477        # print(chroma_settings)
478        # print(chroma_settings)
479
480        data = self.iRawDataPlus.GetChromatogramData(
481            all_chroma_settings, self.start_scan, self.end_scan, options
482        )
483
484        traces = ChromatogramSignal.FromChromatogramData(data)
485
486        chroma = {}
487
488        if plot:
489            from matplotlib.transforms import Bbox
490            import matplotlib.pyplot as plt
491
492            if not ax:
493                # ax = plt.gca()
494                # ax.clear()
495                fig, ax = plt.subplots()
496
497            else:
498                fig = plt.gcf()
499
500            # plt.show()
501
502        for i, trace in enumerate(traces):
503            if trace.Length > 0:
504                rt, eic, scans = self.get_rt_time_from_trace(trace)
505                if smooth:
506                    eic = self.smooth_tic(eic)
507
508                chroma[target_mzs[i]] = EIC_Data(scans=scans, time=rt, eic=eic)
509                if plot:
510                    ax.plot(rt, eic, label="{:.5f}".format(target_mzs[i]))
511
512        if peak_detection:
513            # max_eic = self.get_max_eic(chroma)
514            max_signal = max(tic_data.tic)
515
516            for eic_data in chroma.values():
517                eic = eic_data.eic
518                time = eic_data.time
519
520                if len(eic) != len(tic_data.tic):
521                    warn(
522                        "The software assumes same lenth of TIC and EIC, this does not seems to be the case and the results mass spectrum selected by the scan number might not be correct"
523                    )
524
525                if eic.max() > 0:
526                    centroid_eics = self.eic_centroid_detector(time, eic, max_signal)
527                    eic_data.apexes = [i for i in centroid_eics]
528
529                    if plot:
530                        for peak_indexes in eic_data.apexes:
531                            apex_index = peak_indexes[1]
532                            ax.plot(
533                                time[apex_index],
534                                eic[apex_index],
535                                marker="x",
536                                linewidth=0,
537                            )
538
539        if plot:
540            ax.set_xlabel("Time (min)")
541            ax.set_ylabel("a.u.")
542            ax.set_title(ms_type + " EIC")
543            ax.tick_params(axis="both", which="major", labelsize=12)
544            ax.axes.spines["top"].set_visible(False)
545            ax.axes.spines["right"].set_visible(False)
546
547            if legend:
548                legend = ax.legend(loc="upper left", bbox_to_anchor=(1.02, 0, 0.07, 1))
549                fig.subplots_adjust(right=0.76)
550                # ax.set_prop_cycle(color=plt.cm.gist_rainbow(np.linspace(0, 1, len(traces))))
551
552                d = {"down": 30, "up": -30}
553
554                def func(evt):
555                    if legend.contains(evt):
556                        bbox = legend.get_bbox_to_anchor()
557                        bbox = Bbox.from_bounds(
558                            bbox.x0, bbox.y0 + d[evt.button], bbox.width, bbox.height
559                        )
560                        tr = legend.axes.transAxes.inverted()
561                        legend.set_bbox_to_anchor(bbox.transformed(tr))
562                        fig.canvas.draw_idle()
563
564                fig.canvas.mpl_connect("scroll_event", func)
565            return chroma, ax
566        else:
567            return chroma, None
568            rt = []
569            tic = []
570            scans = []
571            for i in range(traces[0].Length):
572                # print(trace[0].HasBasePeakData,trace[0].EndTime )
573
574                # print("  {} - {}, {}".format( i, trace[0].Times[i], trace[0].Intensities[i] ))
575                rt.append(traces[0].Times[i])
576                tic.append(traces[0].Intensities[i])
577                scans.append(traces[0].Scans[i])
578
579            return traces
580            # plot_chroma(rt, tic)
581            # plt.show()

ms_type: str ('MS', MS2') start_scan: int default -1 will select the lowest available end_scan: int default -1 will select the highest available

returns:

chroma: dict{target_mz: EIC_Data(
                            Scans: [int]
                                original thermo scan numbers
                            Time: [floats]
                                list of retention times
                            TIC: [floats]
                                total ion chromatogram
                            Apexes: [int]
                                original thermo apex scan number after peak picking
                            )
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]:
583    def get_tic(
584        self,
585        ms_type="MS !d",
586        peak_detection=False,  # This wont work right now
587        smooth=False,  # This wont work right now
588        plot=False,
589        ax=None,
590        trace_type="TIC",
591    ) -> Tuple[TIC_Data, axes.Axes]:
592        """ms_type: str ('MS !d', 'MS2', None)
593            if you use None you get all scans.
594        peak_detection: bool
595        smooth: bool
596        plot: bool
597        ax: matplotlib axis object
598        trace_type: str ('TIC','BPC')
599
600        returns:
601            chroma: dict
602            {
603            Scan: [int]
604                original thermo scan numberMS
605            Time: [floats]
606                list of retention times
607            TIC: [floats]
608                total ion chromatogram
609            Apexes: [int]
610                original thermo apex scan number after peak picking
611            }
612        """
613        # If peak_detection or smooth is True, raise exception
614        if peak_detection or smooth:
615            raise Exception(
616                "Peak detection and smoothing are no longer implemented in this function"
617            )
618
619        if trace_type == "TIC":
620            settings = ChromatogramTraceSettings(TraceType.TIC)
621        elif trace_type == "BPC":
622            settings = ChromatogramTraceSettings(TraceType.BasePeak)
623        else:
624            raise ValueError(f"{trace_type} undefined")
625        if ms_type == "all":
626            settings.Filter = None
627        else:
628            settings.Filter = ms_type
629
630        chroma_settings = IChromatogramSettings(settings)
631
632        data = self.iRawDataPlus.GetChromatogramData(
633            [chroma_settings], self.start_scan, self.end_scan
634        )
635
636        trace = ChromatogramSignal.FromChromatogramData(data)
637
638        data = TIC_Data(time=[], scans=[], tic=[], bpc=[], apexes=[])
639
640        if trace[0].Length > 0:
641            for i in range(trace[0].Length):
642                # print(trace[0].HasBasePeakData,trace[0].EndTime )
643
644                # print("  {} - {}, {}".format( i, trace[0].Times[i], trace[0].Intensities[i] ))
645                data.time.append(trace[0].Times[i])
646                data.tic.append(trace[0].Intensities[i])
647                data.scans.append(trace[0].Scans[i])
648
649                # print(trace[0].Scans[i])
650            if smooth:
651                data.tic = self.smooth_tic(data.tic)
652
653            else:
654                data.tic = np.array(data.tic)
655
656            if peak_detection:
657                centroid_peak_indexes = [
658                    i for i in self.centroid_detector(data.time, data.tic)
659                ]
660
661                data.apexes = centroid_peak_indexes
662
663            if plot:
664                if not ax:
665                    import matplotlib.pyplot as plt
666
667                    ax = plt.gca()
668                    # fig, ax = plt.subplots(figsize=(6, 3))
669
670                ax.plot(data.time, data.tic, label=trace_type)
671                ax.set_xlabel("Time (min)")
672                ax.set_ylabel("a.u.")
673                if peak_detection:
674                    for peak_indexes in data.apexes:
675                        apex_index = peak_indexes[1]
676                        ax.plot(
677                            data.time[apex_index],
678                            data.tic[apex_index],
679                            marker="x",
680                            linewidth=0,
681                        )
682
683                # plt.show()
684                if trace_type == "BPC":
685                    data.bpc = data.tic
686                    data.tic = []
687                return data, ax
688            if trace_type == "BPC":
689                data.bpc = data.tic
690                data.tic = []
691            return data, None
692
693        else:
694            return None, None

ms_type: str ('MS !d', 'MS2', None) if you use None you get all scans. peak_detection: bool smooth: bool plot: bool ax: matplotlib axis object trace_type: str ('TIC','BPC')

returns: chroma: dict { Scan: [int] original thermo scan numberMS Time: [floats] list of retention times TIC: [floats] total ion chromatogram Apexes: [int] original thermo apex scan number after peak picking }

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:
696    def get_average_mass_spectrum(
697        self,
698        spectrum_mode: str = "profile",
699        auto_process: bool = True,
700        ppm_tolerance: float = 5.0,
701        ms_type: str = "MS1",
702    ) -> MassSpecProfile | MassSpecCentroid:
703        """
704        Averages mass spectra over a scan range using Thermo's AverageScansInScanRange method
705        or a scan list using Thermo's AverageScans method
706        spectrum_mode: str
707            centroid or profile mass spectrum
708        auto_process: bool
709            If true performs peak picking, and noise threshold calculation after creation of mass spectrum object
710        ms_type: str
711            String of form 'ms1' or 'ms2' or 'MS3' etc. Valid up to MS10.
712            Internal function converts to Thermo MSOrderType class.
713
714        """
715
716        def get_profile_mass_spec(averageScan, d_params: dict, auto_process: bool):
717            mz_list = list(averageScan.SegmentedScan.Positions)
718            abund_list = list(averageScan.SegmentedScan.Intensities)
719
720            data_dict = {
721                Labels.mz: mz_list,
722                Labels.abundance: abund_list,
723            }
724
725            return MassSpecProfile(data_dict, d_params, auto_process=auto_process)
726
727        def get_centroid_mass_spec(averageScan, d_params: dict):
728            noise = list(averageScan.centroidScan.Noises)
729
730            baselines = list(averageScan.centroidScan.Baselines)
731
732            rp = list(averageScan.centroidScan.Resolutions)
733
734            magnitude = list(averageScan.centroidScan.Intensities)
735
736            mz = list(averageScan.centroidScan.Masses)
737
738            array_noise_std = (np.array(noise) - np.array(baselines)) / 3
739            l_signal_to_noise = np.array(magnitude) / array_noise_std
740
741            d_params["baseline_noise"] = np.average(array_noise_std)
742
743            d_params["baseline_noise_std"] = np.std(array_noise_std)
744
745            data_dict = {
746                Labels.mz: mz,
747                Labels.abundance: magnitude,
748                Labels.rp: rp,
749                Labels.s2n: list(l_signal_to_noise),
750            }
751
752            mass_spec = MassSpecCentroid(data_dict, d_params, auto_process=False)
753
754            return mass_spec
755
756        d_params = self.set_metadata(
757            firstScanNumber=self.start_scan, lastScanNumber=self.end_scan
758        )
759
760        # Create the mass options object that will be used when averaging the scans
761        options = MassOptions()
762        options.ToleranceUnits = ToleranceUnits.ppm
763        options.Tolerance = ppm_tolerance
764
765        # Get the scan filter for the first scan.  This scan filter will be used to located
766        # scans within the given scan range of the same type
767        scanFilter = self.iRawDataPlus.GetFilterForScanNumber(self.start_scan)
768
769        # force it to only look for the MSType
770        scanFilter = self.set_msordertype(scanFilter, ms_type)
771
772        if isinstance(self.scans, tuple):
773            averageScan = Extensions.AverageScansInScanRange(
774                self.iRawDataPlus, self.start_scan, self.end_scan, scanFilter, options
775            )
776
777            if averageScan:
778                if spectrum_mode == "profile":
779                    mass_spec = get_profile_mass_spec(
780                        averageScan, d_params, auto_process
781                    )
782
783                    return mass_spec
784
785                elif spectrum_mode == "centroid":
786                    if averageScan.HasCentroidStream:
787                        mass_spec = get_centroid_mass_spec(averageScan, d_params)
788
789                        return mass_spec
790
791                    else:
792                        raise ValueError(
793                            "No Centroind data available for the selected scans"
794                        )
795                else:
796                    raise ValueError("spectrum_mode must be 'profile' or centroid")
797            else:
798                raise ValueError("No data found for the selected scans")
799
800        elif isinstance(self.scans, list):
801            d_params = self.set_metadata(scans_list=self.scans)
802
803            scans = DotNetList[int]()
804            for scan in self.scans:
805                scans.Add(scan)
806
807            averageScan = Extensions.AverageScans(self.iRawDataPlus, scans, options)
808
809            if averageScan:
810                if spectrum_mode == "profile":
811                    mass_spec = get_profile_mass_spec(
812                        averageScan, d_params, auto_process
813                    )
814
815                    return mass_spec
816
817                elif spectrum_mode == "centroid":
818                    if averageScan.HasCentroidStream:
819                        mass_spec = get_centroid_mass_spec(averageScan, d_params)
820
821                        return mass_spec
822
823                    else:
824                        raise ValueError(
825                            "No Centroind data available for the selected scans"
826                        )
827
828                else:
829                    raise ValueError("spectrum_mode must be 'profile' or centroid")
830
831            else:
832                raise ValueError("No data found for the selected scans")
833
834        else:
835            raise ValueError("scans must be a list intergers or a tuple if integers")

Averages mass spectra over a scan range using Thermo's AverageScansInScanRange method or a scan list using Thermo's AverageScans method spectrum_mode: str centroid or profile mass spectrum auto_process: bool If true performs peak picking, and noise threshold calculation after creation of mass spectrum object ms_type: str String of form 'ms1' or 'ms2' or 'MS3' etc. Valid up to MS10. Internal function converts to Thermo MSOrderType class.

def set_metadata( self, firstScanNumber=0, lastScanNumber=0, scans_list=False, label='Thermo_Profile'):
837    def set_metadata(
838        self,
839        firstScanNumber=0,
840        lastScanNumber=0,
841        scans_list=False,
842        label=Labels.thermo_profile,
843    ):
844        """
845        Collect metadata to be ingested in the mass spectrum object
846
847        scans_list: list[int] or false
848        lastScanNumber: int
849        firstScanNumber: int
850        """
851
852        d_params = default_parameters(self.file_path)
853
854        # assumes scans is full scan or reduced profile scan
855
856        d_params["label"] = label
857
858        if scans_list:
859            d_params["scan_number"] = scans_list
860
861            d_params["polarity"] = self.get_polarity_mode(scans_list[0])
862
863        else:
864            d_params["scan_number"] = "{}-{}".format(firstScanNumber, lastScanNumber)
865
866            d_params["polarity"] = self.get_polarity_mode(firstScanNumber)
867
868        d_params["analyzer"] = self.iRawDataPlus.GetInstrumentData().Model
869
870        d_params["acquisition_time"] = self.get_creation_time()
871
872        d_params["instrument_label"] = self.iRawDataPlus.GetInstrumentData().Name
873
874        return d_params

Collect metadata to be ingested in the mass spectrum object

scans_list: list[int] or false lastScanNumber: int firstScanNumber: int

def get_instrument_methods(self, parse_strings: bool = True):
876    def get_instrument_methods(self, parse_strings: bool = True):
877        """
878        This function will extract the instrument methods embedded in the raw file
879
880        First it will check if there are any instrument methods, if not returning None
881        Then it will get the total number of instrument methods.
882        For each method, it will extract the plaintext string of the method and attempt to parse it into a dictionary
883        If this fails, it will return just the string object.
884
885        This has been tested on data from an Orbitrap ID-X with embedded MS and LC methods, but other instrument types may fail.
886
887        Parameters:
888        -----------
889        parse_strings: bool
890            If True, will attempt to parse the instrument methods into a dictionary. If False, will return the raw string.
891
892        Returns:
893        --------
894        List[Dict[str, Any]] or List
895            A list of dictionaries containing the instrument methods, or a list of strings if parsing fails.
896        """
897
898        if not self.iRawDataPlus.HasInstrumentMethod:
899            raise ValueError(
900                "Raw Data file does not have any instrument methods attached"
901            )
902            return None
903        else:
904
905            def parse_instrument_method(data):
906                lines = data.split("\r\n")
907                method = {}
908                current_section = None
909                sub_section = None
910
911                for line in lines:
912                    if not line.strip():  # Skip empty lines
913                        continue
914                    if (
915                        line.startswith("----")
916                        or line.endswith("Settings")
917                        or line.endswith("Summary")
918                        or line.startswith("Experiment")
919                        or line.startswith("Scan Event")
920                    ):
921                        current_section = line.replace("-", "").strip()
922                        method[current_section] = {}
923                        sub_section = None
924                    elif line.startswith("\t"):
925                        if "\t\t" in line:
926                            indent_level = line.count("\t")
927                            key_value = line.strip()
928
929                            if indent_level == 2:
930                                if sub_section:
931                                    key, value = (
932                                        key_value.split("=", 1)
933                                        if "=" in key_value
934                                        else (key_value, None)
935                                    )
936                                    method[current_section][sub_section][
937                                        key.strip()
938                                    ] = value.strip() if value else None
939                            elif indent_level == 3:
940                                scan_type, key_value = (
941                                    key_value.split(" ", 1)
942                                    if " " in key_value
943                                    else (key_value, None)
944                                )
945                                method.setdefault(current_section, {}).setdefault(
946                                    sub_section, {}
947                                ).setdefault(scan_type, {})
948
949                                if key_value:
950                                    key, value = (
951                                        key_value.split("=", 1)
952                                        if "=" in key_value
953                                        else (key_value, None)
954                                    )
955                                    method[current_section][sub_section][scan_type][
956                                        key.strip()
957                                    ] = value.strip() if value else None
958                        else:
959                            key_value = line.strip()
960                            if "=" in key_value:
961                                key, value = key_value.split("=", 1)
962                                method.setdefault(current_section, {})[key.strip()] = (
963                                    value.strip()
964                                )
965                            else:
966                                sub_section = key_value
967                    else:
968                        if ":" in line:
969                            key, value = line.split(":", 1)
970                            method[current_section][key.strip()] = value.strip()
971                        else:
972                            method[current_section][line] = {}
973
974                return method
975
976            count_instrument_methods = self.iRawDataPlus.InstrumentMethodsCount
977            # TODO make this code better...
978            instrument_methods = []
979            for i in range(count_instrument_methods):
980                instrument_method_string = self.iRawDataPlus.GetInstrumentMethod(i)
981                if parse_strings:
982                    try:
983                        instrument_method_dict = parse_instrument_method(
984                            instrument_method_string
985                        )
986                    except:  # if it fails for any reason
987                        instrument_method_dict = instrument_method_string
988                else:
989                    instrument_method_dict = instrument_method_string
990                instrument_methods.append(instrument_method_dict)
991            return instrument_methods

This function will extract the instrument methods embedded in the raw file

First it will check if there are any instrument methods, if not returning None Then it will get the total number of instrument methods. For each method, it will extract the plaintext string of the method and attempt to parse it into a dictionary If this fails, it will return just the string object.

This has been tested on data from an Orbitrap ID-X with embedded MS and LC methods, but other instrument types may fail.

Parameters:

parse_strings: bool If True, will attempt to parse the instrument methods into a dictionary. If False, will return the raw string.

Returns:

List[Dict[str, Any]] or List A list of dictionaries containing the instrument methods, or a list of strings if parsing fails.

def get_tune_method(self):
 993    def get_tune_method(self):
 994        """
 995        This code will extract the tune method from the raw file
 996        It has been tested on data from a Thermo Orbitrap ID-X, Astral and Q-Exactive, but may fail on other instrument types.
 997        It attempts to parse out section headers and sub-sections, but may not work for all instrument types.
 998        It will also not return Labels (keys) where the value is blank
 999
1000        Returns:
1001        --------
1002        Dict[str, Any]
1003            A dictionary containing the tune method information
1004
1005        Raises:
1006        -------
1007        ValueError
1008            If no tune methods are found in the raw file
1009
1010        """
1011        tunemethodcount = self.iRawDataPlus.GetTuneDataCount()
1012        if tunemethodcount == 0:
1013            raise ValueError("No tune methods found in the raw data file")
1014            return None
1015        elif tunemethodcount > 1:
1016            warnings.warn(
1017                "Multiple tune methods found in the raw data file, returning the 1st"
1018            )
1019
1020        header = self.iRawDataPlus.GetTuneData(0)
1021
1022        header_dic = {}
1023        current_section = None
1024
1025        for i in range(header.Length):
1026            label = header.Labels[i]
1027            value = header.Values[i]
1028
1029            # Check for section headers
1030            if "===" in label or (
1031                (value == "" or value is None) and not label.endswith(":")
1032            ):
1033                # This is a section header
1034                section_name = (
1035                    label.replace("=", "").replace(":", "").strip()
1036                )  # Clean the label if it contains '='
1037                header_dic[section_name] = {}
1038                current_section = section_name
1039            else:
1040                if current_section:
1041                    header_dic[current_section][label] = value
1042                else:
1043                    header_dic[label] = value
1044        return header_dic

This code will extract the tune method from the raw file It has been tested on data from a Thermo Orbitrap ID-X, Astral and Q-Exactive, but may fail on other instrument types. It attempts to parse out section headers and sub-sections, but may not work for all instrument types. It will also not return Labels (keys) where the value is blank

Returns:

Dict[str, Any] A dictionary containing the tune method information

Raises:

ValueError If no tune methods are found in the raw file

def get_status_log(self, retention_time: float = 0):
1046    def get_status_log(self, retention_time: float = 0):
1047        """
1048        This code will extract the status logs from the raw file
1049        It has been tested on data from a Thermo Orbitrap ID-X, Astral and Q-Exactive, but may fail on other instrument types.
1050        It attempts to parse out section headers and sub-sections, but may not work for all instrument types.
1051        It will also not return Labels (keys) where the value is blank
1052
1053        Parameters:
1054        -----------
1055        retention_time: float
1056            The retention time in minutes to extract the status log data from.
1057            Will use the closest retention time found. Default 0.
1058
1059        Returns:
1060        --------
1061        Dict[str, Any]
1062            A dictionary containing the status log information
1063
1064        Raises:
1065        -------
1066        ValueError
1067            If no status logs are found in the raw file
1068
1069        """
1070        tunemethodcount = self.iRawDataPlus.GetStatusLogEntriesCount()
1071        if tunemethodcount == 0:
1072            raise ValueError("No status logs found in the raw data file")
1073            return None
1074
1075        header = self.iRawDataPlus.GetStatusLogForRetentionTime(retention_time)
1076
1077        header_dic = {}
1078        current_section = None
1079
1080        for i in range(header.Length):
1081            label = header.Labels[i]
1082            value = header.Values[i]
1083
1084            # Check for section headers
1085            if "===" in label or (
1086                (value == "" or value is None) and not label.endswith(":")
1087            ):
1088                # This is a section header
1089                section_name = (
1090                    label.replace("=", "").replace(":", "").strip()
1091                )  # Clean the label if it contains '='
1092                header_dic[section_name] = {}
1093                current_section = section_name
1094            else:
1095                if current_section:
1096                    header_dic[current_section][label] = value
1097                else:
1098                    header_dic[label] = value
1099        return header_dic

This code will extract the status logs from the raw file It has been tested on data from a Thermo Orbitrap ID-X, Astral and Q-Exactive, but may fail on other instrument types. It attempts to parse out section headers and sub-sections, but may not work for all instrument types. It will also not return Labels (keys) where the value is blank

Parameters:

retention_time: float The retention time in minutes to extract the status log data from. Will use the closest retention time found. Default 0.

Returns:

Dict[str, Any] A dictionary containing the status log information

Raises:

ValueError If no status logs are found in the raw file

def get_error_logs(self):
1101    def get_error_logs(self):
1102        """
1103        This code will extract the error logs from the raw file
1104
1105        Returns:
1106        --------
1107        Dict[float, str]
1108            A dictionary containing the error log information with the retention time as the key
1109
1110        Raises:
1111        -------
1112        ValueError
1113            If no error logs are found in the raw file
1114        """
1115
1116        error_log_count = self.iRawDataPlus.RunHeaderEx.ErrorLogCount
1117        if error_log_count == 0:
1118            raise ValueError("No error logs found in the raw data file")
1119            return None
1120
1121        error_logs = {}
1122
1123        for i in range(error_log_count):
1124            error_log_item = self.iRawDataPlus.GetErrorLogItem(i)
1125            rt = error_log_item.RetentionTime
1126            message = error_log_item.Message
1127            # Use the index `i` as the unique ID key
1128            error_logs[i] = {"rt": rt, "message": message}
1129        return error_logs

This code will extract the error logs from the raw file

Returns:

Dict[float, str] A dictionary containing the error log information with the retention time as the key

Raises:

ValueError If no error logs are found in the raw file

def get_sample_information(self):
1131    def get_sample_information(self):
1132        """
1133        This code will extract the sample information from the raw file
1134
1135        Returns:
1136        --------
1137        Dict[str, Any]
1138            A dictionary containing the sample information
1139            Note that UserText field may not be handled properly and may need further processing
1140        """
1141        sminfo = self.iRawDataPlus.SampleInformation
1142        smdict = {}
1143        smdict["Comment"] = sminfo.Comment
1144        smdict["SampleId"] = sminfo.SampleId
1145        smdict["SampleName"] = sminfo.SampleName
1146        smdict["Vial"] = sminfo.Vial
1147        smdict["InjectionVolume"] = sminfo.InjectionVolume
1148        smdict["Barcode"] = sminfo.Barcode
1149        smdict["BarcodeStatus"] = str(sminfo.BarcodeStatus)
1150        smdict["CalibrationLevel"] = sminfo.CalibrationLevel
1151        smdict["DilutionFactor"] = sminfo.DilutionFactor
1152        smdict["InstrumentMethodFile"] = sminfo.InstrumentMethodFile
1153        smdict["RawFileName"] = sminfo.RawFileName
1154        smdict["CalibrationFile"] = sminfo.CalibrationFile
1155        smdict["IstdAmount"] = sminfo.IstdAmount
1156        smdict["RowNumber"] = sminfo.RowNumber
1157        smdict["Path"] = sminfo.Path
1158        smdict["ProcessingMethodFile"] = sminfo.ProcessingMethodFile
1159        smdict["SampleType"] = str(sminfo.SampleType)
1160        smdict["SampleWeight"] = sminfo.SampleWeight
1161        smdict["UserText"] = {
1162            "UserText": [x for x in sminfo.UserText]
1163        }  # [0] #This may not work - needs debugging with
1164        return smdict

This code will extract the sample information from the raw file

Returns:

Dict[str, Any] A dictionary containing the sample information Note that UserText field may not be handled properly and may need further processing

def get_instrument_data(self):
1166    def get_instrument_data(self):
1167        """
1168        This code will extract the instrument data from the raw file
1169
1170        Returns:
1171        --------
1172        Dict[str, Any]
1173            A dictionary containing the instrument data
1174        """
1175        instrument_data = self.iRawDataPlus.GetInstrumentData()
1176        id_dict = {}
1177        id_dict["Name"] = instrument_data.Name
1178        id_dict["Model"] = instrument_data.Model
1179        id_dict["SerialNumber"] = instrument_data.SerialNumber
1180        id_dict["SoftwareVersion"] = instrument_data.SoftwareVersion
1181        id_dict["HardwareVersion"] = instrument_data.HardwareVersion
1182        id_dict["ChannelLabels"] = {
1183            "ChannelLabels": [x for x in instrument_data.ChannelLabels]
1184        }
1185        id_dict["Flags"] = instrument_data.Flags
1186        id_dict["AxisLabelY"] = instrument_data.AxisLabelY
1187        id_dict["AxisLabelX"] = instrument_data.AxisLabelX
1188        return id_dict

This code will extract the instrument data from the raw file

Returns:

Dict[str, Any] A dictionary containing the instrument data

def get_centroid_msms_data(self, scan):
1190    def get_centroid_msms_data(self, scan):
1191        """
1192        .. deprecated:: 2.0
1193            This function will be removed in CoreMS 2.0. Please use `get_average_mass_spectrum()` instead for similar functionality.
1194        """
1195
1196        warnings.warn(
1197            "The `get_centroid_msms_data()` is deprecated as of CoreMS 2.0 and will be removed in a future version. "
1198            "Please use `get_average_mass_spectrum()` instead.",
1199            DeprecationWarning,
1200        )
1201
1202        d_params = self.set_metadata(scans_list=[scan], label=Labels.thermo_centroid)
1203
1204        centroidStream = self.iRawDataPlus.GetCentroidStream(scan, False)
1205
1206        noise = list(centroidStream.Noises)
1207
1208        baselines = list(centroidStream.Baselines)
1209
1210        rp = list(centroidStream.Resolutions)
1211
1212        magnitude = list(centroidStream.Intensities)
1213
1214        mz = list(centroidStream.Masses)
1215
1216        # charge = scans_labels[5]
1217        array_noise_std = (np.array(noise) - np.array(baselines)) / 3
1218        l_signal_to_noise = np.array(magnitude) / array_noise_std
1219
1220        d_params["baseline_noise"] = np.average(array_noise_std)
1221
1222        d_params["baseline_noise_std"] = np.std(array_noise_std)
1223
1224        data_dict = {
1225            Labels.mz: mz,
1226            Labels.abundance: magnitude,
1227            Labels.rp: rp,
1228            Labels.s2n: list(l_signal_to_noise),
1229        }
1230
1231        mass_spec = MassSpecCentroid(data_dict, d_params, auto_process=False)
1232        mass_spec.settings.noise_threshold_method = "relative_abundance"
1233        mass_spec.settings.noise_threshold_min_relative_abundance = 1
1234        mass_spec.process_mass_spec()
1235        return mass_spec

Deprecated since version 2.0: This function will be removed in CoreMS 2.0. Please use get_average_mass_spectrum() instead for similar functionality.

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:
1237    def get_average_mass_spectrum_by_scanlist(
1238        self,
1239        scans_list: List[int],
1240        auto_process: bool = True,
1241        ppm_tolerance: float = 5.0,
1242    ) -> MassSpecProfile:
1243        """
1244        Averages selected scans mass spectra using Thermo's AverageScans method
1245        scans_list: list[int]
1246        auto_process: bool
1247            If true performs peak picking, and noise threshold calculation after creation of mass spectrum object
1248        Returns:
1249            MassSpecProfile
1250
1251         .. deprecated:: 2.0
1252        This function will be removed in CoreMS 2.0. Please use `get_average_mass_spectrum()` instead for similar functionality.
1253        """
1254
1255        warnings.warn(
1256            "The `get_average_mass_spectrum_by_scanlist()` is deprecated as of CoreMS 2.0 and will be removed in a future version. "
1257            "Please use `get_average_mass_spectrum()` instead.",
1258            DeprecationWarning,
1259        )
1260
1261        d_params = self.set_metadata(scans_list=scans_list)
1262
1263        # assumes scans is full scan or reduced profile scan
1264
1265        scans = DotNetList[int]()
1266        for scan in scans_list:
1267            scans.Add(scan)
1268
1269        # Create the mass options object that will be used when averaging the scans
1270        options = MassOptions()
1271        options.ToleranceUnits = ToleranceUnits.ppm
1272        options.Tolerance = ppm_tolerance
1273
1274        # Get the scan filter for the first scan.  This scan filter will be used to located
1275        # scans within the given scan range of the same type
1276
1277        averageScan = Extensions.AverageScans(self.iRawDataPlus, scans, options)
1278
1279        len_data = averageScan.SegmentedScan.Positions.Length
1280
1281        mz_list = list(averageScan.SegmentedScan.Positions)
1282        abund_list = list(averageScan.SegmentedScan.Intensities)
1283
1284        data_dict = {
1285            Labels.mz: mz_list,
1286            Labels.abundance: abund_list,
1287        }
1288
1289        mass_spec = MassSpecProfile(data_dict, d_params, auto_process=auto_process)
1290
1291        return mass_spec

Averages selected scans mass spectra using Thermo's AverageScans method scans_list: list[int] auto_process: bool If true performs peak picking, and noise threshold calculation after creation of mass spectrum object Returns: MassSpecProfile

Deprecated since version 2.0.

This function will be removed in CoreMS 2.0. Please use get_average_mass_spectrum() instead for similar functionality.

class ImportMassSpectraThermoMSFileReader(ThermoBaseClass, corems.mass_spectra.input.parserbase.SpectraParserInterface):
1294class ImportMassSpectraThermoMSFileReader(ThermoBaseClass, SpectraParserInterface):
1295    """A class for parsing Thermo RAW mass spectrometry data files and instatiating MassSpectraBase or LCMSBase objects
1296
1297    Parameters
1298    ----------
1299    file_location : str or Path
1300        The path to the RAW file to be parsed.
1301    analyzer : str, optional
1302        The type of mass analyzer used in the instrument. Default is "Unknown".
1303    instrument_label : str, optional
1304        The name of the instrument used to acquire the data. Default is "Unknown".
1305    sample_name : str, optional
1306        The name of the sample being analyzed. If not provided, the stem of the file_location path will be used.
1307
1308    Attributes
1309    ----------
1310    file_location : Path
1311        The path to the RAW file being parsed.
1312    analyzer : str
1313        The type of mass analyzer used in the instrument.
1314    instrument_label : str
1315        The name of the instrument used to acquire the data.
1316    sample_name : str
1317        The name of the sample being analyzed.
1318
1319    Methods
1320    -------
1321    * run(spectra=True).
1322        Parses the RAW file and returns a dictionary of mass spectra dataframes and a scan metadata dataframe.
1323    * get_mass_spectrum_from_scan(scan_number, polarity, auto_process=True)
1324        Parses the RAW file and returns a MassSpecBase object from a single scan.
1325    * get_mass_spectra_obj().
1326        Parses the RAW file and instantiates a MassSpectraBase object.
1327    * get_lcms_obj().
1328        Parses the RAW file and instantiates an LCMSBase object.
1329    * get_icr_transient_times().
1330        Return a list for transient time targets for all scans, or selected scans range
1331
1332    Inherits from ThermoBaseClass and SpectraParserInterface
1333    """
1334
1335    def __init__(
1336        self,
1337        file_location,
1338        analyzer="Unknown",
1339        instrument_label="Unknown",
1340        sample_name=None,
1341    ):
1342        super().__init__(file_location)
1343        if isinstance(file_location, str):
1344            # if obj is a string it defaults to create a Path obj, pass the S3Path if needed
1345            file_location = Path(file_location)
1346        if not file_location.exists():
1347            raise FileExistsError("File does not exist: " + str(file_location))
1348
1349        self.file_location = file_location
1350        self.analyzer = analyzer
1351        self.instrument_label = instrument_label
1352
1353        if sample_name:
1354            self.sample_name = sample_name
1355        else:
1356            self.sample_name = file_location.stem
1357
1358    def load(self):
1359        pass
1360
1361    def get_scans_in_time_range(
1362        self,
1363        time_range: Union[Tuple[float, float], List[Tuple[float, float]]],
1364        ms_level: Optional[int] = None,
1365    ) -> List[int]:
1366        """Return scan numbers within specified retention time range(s).
1367
1368        Parameters
1369        ----------
1370        time_range : tuple or list of tuples
1371            Retention time range(s) in minutes. Can be:
1372            - Single range: (start_time, end_time)
1373            - Multiple ranges: [(start1, end1), (start2, end2), ...]
1374        ms_level : int, optional
1375            If specified, only return scans of this MS level (e.g., 1 for MS1, 2 for MS2).
1376            If None, returns scans of all MS levels.
1377
1378        Returns
1379        -------
1380        list of int
1381            List of scan numbers within the specified time range(s) and MS level.
1382        """
1383        # Normalize time range to list of tuples
1384        time_ranges = self._normalize_time_range(time_range)
1385
1386        # Get all scan data
1387        scan_df = self.get_scan_df()
1388
1389        # Filter by time range
1390        mask = pd.Series([False] * len(scan_df), index=scan_df.index)
1391        for start_time, end_time in time_ranges:
1392            mask |= (scan_df.scan_time >= start_time) & (scan_df.scan_time <= end_time)
1393
1394        filtered_df = scan_df[mask]
1395
1396        # Filter by MS level if specified
1397        if ms_level is not None:
1398            filtered_df = filtered_df[filtered_df.ms_level == ms_level]
1399
1400        return filtered_df.scan.tolist()
1401
1402    def get_scan_df(
1403        self,
1404        time_range: Optional[
1405            Union[Tuple[float, float], List[Tuple[float, float]]]
1406        ] = None,
1407    ):
1408        """Return scan data as a pandas DataFrame.
1409
1410        Parameters
1411        ----------
1412        time_range : tuple or list of tuples, optional
1413            Retention time range(s) to filter scans. Can be:
1414            - Single range: (start_time, end_time) in minutes
1415            - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes
1416            If None, returns all scans.
1417
1418        Returns
1419        -------
1420        pd.DataFrame
1421            DataFrame containing scan information, optionally filtered by time range.
1422        """
1423        # This automatically brings in all the data
1424        self.chromatogram_settings.scans = (-1, -1)
1425
1426        # Get scan df info; starting with TIC data
1427        tic_data, _ = self.get_tic(ms_type="all", peak_detection=False, smooth=False)
1428        tic_data = {
1429            "scan": tic_data.scans,
1430            "scan_time": tic_data.time,
1431            "tic": tic_data.tic,
1432        }
1433        scan_df = pd.DataFrame.from_dict(tic_data)
1434        scan_df["ms_level"] = None
1435
1436        # get scan text
1437        scan_filter_df = pd.DataFrame.from_dict(
1438            self.get_all_filters()[0], orient="index"
1439        )
1440        scan_filter_df.reset_index(inplace=True)
1441        scan_filter_df.rename(columns={"index": "scan", 0: "scan_text"}, inplace=True)
1442
1443        scan_df = scan_df.merge(scan_filter_df, on="scan", how="left")
1444        scan_df["scan_window_lower"] = scan_df.scan_text.str.extract(
1445            r"\[(\d+\.\d+)-\d+\.\d+\]"
1446        )
1447        scan_df["scan_window_upper"] = scan_df.scan_text.str.extract(
1448            r"\[\d+\.\d+-(\d+\.\d+)\]"
1449        )
1450        scan_df["polarity"] = np.where(
1451            scan_df.scan_text.str.contains(" - "), "negative", "positive"
1452        )
1453        scan_df["precursor_mz"] = scan_df.scan_text.str.extract(r"(\d+\.\d+)@")
1454        scan_df["precursor_mz"] = scan_df["precursor_mz"].astype(float)
1455
1456        # Assign each scan as centroid or profile and add ms_level
1457        scan_df["ms_format"] = None
1458        for i in scan_df.scan.to_list():
1459            scan_df.loc[scan_df.scan == i, "ms_level"] = self.get_ms_level_for_scan_num(
1460                i
1461            )
1462            if self.iRawDataPlus.IsCentroidScanFromScanNumber(i):
1463                scan_df.loc[scan_df.scan == i, "ms_format"] = "centroid"
1464            else:
1465                scan_df.loc[scan_df.scan == i, "ms_format"] = "profile"
1466
1467        # Remove any non-mass spectra scans (e.g., MS level 0 or None)
1468        scan_df = scan_df[
1469            scan_df.ms_level.notnull() & (scan_df.ms_level > 0)
1470        ].reset_index(drop=True)
1471
1472        # Filter by time range if specified
1473        if time_range is not None:
1474            time_ranges = self._normalize_time_range(time_range)
1475            mask = pd.Series([False] * len(scan_df), index=scan_df.index)
1476            for start_time, end_time in time_ranges:
1477                mask |= (scan_df.scan_time >= start_time) & (
1478                    scan_df.scan_time <= end_time
1479                )
1480            scan_df = scan_df[mask].reset_index(drop=True)
1481
1482        return scan_df
1483
1484    def get_ms_raw(
1485        self,
1486        spectra,
1487        scan_df,
1488        time_range: Optional[
1489            Union[Tuple[float, float], List[Tuple[float, float]]]
1490        ] = None,
1491    ):
1492        """Return a dictionary of mass spectra data as pandas DataFrames.
1493
1494        Parameters
1495        ----------
1496        spectra : str
1497            Specifies which spectra to load (e.g., 'all', 'ms1', 'ms2')
1498        scan_df : pd.DataFrame
1499            Scan information DataFrame
1500        time_range : tuple or list of tuples, optional
1501            Retention time range(s) to filter scans. Can be:
1502            - Single range: (start_time, end_time) in minutes
1503            - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes
1504            If None, returns all scans. Note: filtering is typically done at scan_df level.
1505
1506        Returns
1507        -------
1508        dict
1509            Dictionary of raw mass spectra data, optionally filtered by time range.
1510        """
1511        # Note: time_range filtering is handled at the scan_df level before calling this method
1512        # The parameter is here for interface consistency with SpectraParserInterface
1513
1514        if spectra == "all":
1515            scan_df_forspec = scan_df
1516        elif spectra == "ms1":
1517            scan_df_forspec = scan_df[scan_df.ms_level == 1]
1518        elif spectra == "ms2":
1519            scan_df_forspec = scan_df[scan_df.ms_level == 2]
1520        else:
1521            raise ValueError("spectra must be 'none', 'all', 'ms1', or 'ms2'")
1522
1523        # Result container
1524        res = {}
1525
1526        # Row count container
1527        counter = {}
1528
1529        # Column name container
1530        cols = {}
1531
1532        # set at float32
1533        dtype = np.float32
1534
1535        # First pass: get nrows
1536        N = defaultdict(lambda: 0)
1537        for i in scan_df_forspec.scan.to_list():
1538            level = scan_df_forspec.loc[scan_df_forspec.scan == i, "ms_level"].values[0]
1539            scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber(i)
1540            profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber(
1541                i, scanStatistics
1542            )
1543            abun = list(profileStream.Intensities)
1544            abun = np.array(abun)[np.where(np.array(abun) > 0)[0]]
1545
1546            N[level] += len(abun)
1547
1548        # Second pass: parse
1549        for i in scan_df_forspec.scan.to_list():
1550            scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber(i)
1551            profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber(
1552                i, scanStatistics
1553            )
1554            abun = list(profileStream.Intensities)
1555            mz = list(profileStream.Positions)
1556
1557            # Get index of abun that are > 0
1558            inx = np.where(np.array(abun) > 0)[0]
1559            mz = np.array(mz)[inx]
1560            mz = np.float32(mz)
1561            abun = np.array(abun)[inx]
1562            abun = np.float32(abun)
1563
1564            level = scan_df_forspec.loc[scan_df_forspec.scan == i, "ms_level"].values[0]
1565
1566            # Number of rows
1567            n = len(mz)
1568
1569            # No measurements
1570            if n == 0:
1571                continue
1572
1573            # Dimension check
1574            if len(mz) != len(abun):
1575                warnings.warn("m/z and intensity array dimension mismatch")
1576                continue
1577
1578            # Scan/frame info
1579            id_dict = i
1580
1581            # Columns
1582            cols[level] = ["scan", "mz", "intensity"]
1583            m = len(cols[level])
1584
1585            # Subarray init
1586            arr = np.empty((n, m), dtype=dtype)
1587            inx = 0
1588
1589            # Populate scan/frame info
1590            arr[:, inx] = i
1591            inx += 1
1592
1593            # Populate m/z
1594            arr[:, inx] = mz
1595            inx += 1
1596
1597            # Populate intensity
1598            arr[:, inx] = abun
1599            inx += 1
1600
1601            # Initialize output container
1602            if level not in res:
1603                res[level] = np.empty((N[level], m), dtype=dtype)
1604                counter[level] = 0
1605
1606            # Insert subarray
1607            res[level][counter[level] : counter[level] + n, :] = arr
1608            counter[level] += n
1609
1610        # Construct ms1 and ms2 mz dataframes
1611        for level in res.keys():
1612            res[level] = pd.DataFrame(res[level])
1613            res[level].columns = cols[level]
1614        # rename keys in res to add 'ms' prefix
1615        res = {f"ms{key}": value for key, value in res.items()}
1616
1617        return res
1618
1619    def run(
1620        self,
1621        spectra="all",
1622        scan_df=None,
1623        time_range: Optional[
1624            Union[Tuple[float, float], List[Tuple[float, float]]]
1625        ] = None,
1626    ):
1627        """
1628        Extracts mass spectra data from a raw file.
1629
1630        Parameters
1631        ----------
1632        spectra : str, optional
1633            Which mass spectra data to include in the output. Default is all.  Other options: none, ms1, ms2.
1634        scan_df : pandas.DataFrame, optional
1635            Scan dataframe.  If not provided, the scan dataframe is created from the mzML file.
1636        time_range : tuple or list of tuples, optional
1637            Retention time range(s) to filter scans. Can be:
1638            - Single range: (start_time, end_time) in minutes
1639            - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes
1640            If None, returns all scans.
1641
1642        Returns
1643        -------
1644        tuple
1645            A tuple containing two elements:
1646            - A dictionary containing mass spectra data, separated by MS level.
1647            - A pandas DataFrame containing scan information, including scan number, scan time, TIC, MS level,
1648                scan text, scan window lower and upper bounds, polarity, and precursor m/z (if applicable).
1649        """
1650        # Prepare scan_df
1651        if scan_df is None:
1652            scan_df = self.get_scan_df(time_range=time_range)
1653
1654        # Prepare mass spectra data
1655        if spectra != "none":
1656            res = self.get_ms_raw(
1657                spectra=spectra, scan_df=scan_df, time_range=time_range
1658            )
1659        else:
1660            res = None
1661
1662        return res, scan_df
1663
1664    def get_mass_spectra_from_scan_list(
1665        self, scan_list, spectrum_mode, auto_process=True
1666    ):
1667        """Instantiate multiple MassSpecBase objects from a list of scan numbers from the binary file.
1668
1669        Parameters
1670        ----------
1671        scan_list : list[int]
1672            A list of scan numbers to extract the mass spectra from.
1673        spectrum_mode : str
1674            The type of mass spectrum to extract.  Must be 'profile' or 'centroid'.
1675            All scans in the list must be of the same type.
1676        auto_process : bool, optional
1677            If True, perform peak picking and noise threshold calculation after creating the mass spectrum object. Default is True.
1678        """
1679        mass_spectra = []
1680        for scan in scan_list:
1681            mass_spectrum = self.get_mass_spectrum_from_scan(
1682                scan, spectrum_mode, auto_process=auto_process
1683            )
1684            mass_spectra.append(mass_spectrum)
1685
1686        return mass_spectra
1687
1688    def get_mass_spectrum_from_scan(
1689        self, scan_number, spectrum_mode, auto_process=True
1690    ):
1691        """Instantiate a MassSpecBase object from a single scan number from the binary file.
1692
1693        Parameters
1694        ----------
1695        scan_number : int
1696            The scan number to extract the mass spectrum from.
1697        polarity : int
1698            The polarity of the scan.  1 for positive mode, -1 for negative mode.
1699        spectrum_mode : str
1700            The type of mass spectrum to extract.  Must be 'profile' or 'centroid'.
1701        auto_process : bool, optional
1702            If True, perform peak picking and noise threshold calculation after creating the mass spectrum object. Default is True.
1703
1704        Returns
1705        -------
1706        MassSpecProfile | MassSpecCentroid
1707            The MassSpecProfile or MassSpecCentroid object containing the parsed mass spectrum.
1708        """
1709
1710        if spectrum_mode == "profile":
1711            scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber(scan_number)
1712            profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber(
1713                scan_number, scanStatistics
1714            )
1715            abun = list(profileStream.Intensities)
1716            mz = list(profileStream.Positions)
1717            data_dict = {
1718                Labels.mz: mz,
1719                Labels.abundance: abun,
1720            }
1721            d_params = self.set_metadata(
1722                firstScanNumber=scan_number,
1723                lastScanNumber=scan_number,
1724                scans_list=False,
1725                label=Labels.thermo_profile,
1726            )
1727            mass_spectrum_obj = MassSpecProfile(
1728                data_dict, d_params, auto_process=auto_process
1729            )
1730
1731        elif spectrum_mode == "centroid":
1732            centroid_scan = self.iRawDataPlus.GetCentroidStream(scan_number, False)
1733            if centroid_scan.Masses is not None:
1734                mz = list(centroid_scan.Masses)
1735                abun = list(centroid_scan.Intensities)
1736                rp = list(centroid_scan.Resolutions)
1737                magnitude = list(centroid_scan.Intensities)
1738                noise = list(centroid_scan.Noises)
1739                baselines = list(centroid_scan.Baselines)
1740                array_noise_std = (np.array(noise) - np.array(baselines)) / 3
1741                l_signal_to_noise = np.array(magnitude) / array_noise_std
1742                data_dict = {
1743                    Labels.mz: mz,
1744                    Labels.abundance: abun,
1745                    Labels.rp: rp,
1746                    Labels.s2n: list(l_signal_to_noise),
1747                }
1748            else:  # For CID MS2, the centroid data are stored in the profile data location, they do not have any associated rp or baseline data, but they should be treated as centroid data
1749                scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber(
1750                    scan_number
1751                )
1752                profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber(
1753                    scan_number, scanStatistics
1754                )
1755                abun = list(profileStream.Intensities)
1756                mz = list(profileStream.Positions)
1757                data_dict = {
1758                    Labels.mz: mz,
1759                    Labels.abundance: abun,
1760                    Labels.rp: [np.nan] * len(mz),
1761                    Labels.s2n: [np.nan] * len(mz),
1762                }
1763            d_params = self.set_metadata(
1764                firstScanNumber=scan_number,
1765                lastScanNumber=scan_number,
1766                scans_list=False,
1767                label=Labels.thermo_centroid,
1768            )
1769            mass_spectrum_obj = MassSpecCentroid(
1770                data_dict, d_params, auto_process=auto_process
1771            )
1772
1773        return mass_spectrum_obj
1774
1775    def get_mass_spectra_obj(
1776        self,
1777        time_range: Optional[
1778            Union[Tuple[float, float], List[Tuple[float, float]]]
1779        ] = None,
1780    ):
1781        """Instatiate a MassSpectraBase object from the binary data file file.
1782
1783        Parameters
1784        ----------
1785        time_range : tuple or list of tuples, optional
1786            Retention time range(s) to load. Can be:
1787            - Single range: (start_time, end_time) in minutes
1788            - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes
1789            If None, loads all scans. Useful for targeted workflows to improve performance.
1790
1791        Returns
1792        -------
1793        MassSpectraBase
1794            The MassSpectra object containing the parsed mass spectra.  The object is instatiated with the mzML file, analyzer, instrument, sample name, and scan dataframe.
1795        """
1796        _, scan_df = self.run(spectra="none", time_range=time_range)
1797        mass_spectra_obj = MassSpectraBase(
1798            self.file_location,
1799            self.analyzer,
1800            self.instrument_label,
1801            self.sample_name,
1802            self,
1803        )
1804        scan_df = scan_df.set_index("scan", drop=False)
1805        mass_spectra_obj.scan_df = scan_df
1806
1807        return mass_spectra_obj
1808
1809    def get_lcms_obj(
1810        self,
1811        spectra="all",
1812        time_range: Optional[
1813            Union[Tuple[float, float], List[Tuple[float, float]]]
1814        ] = None,
1815    ):
1816        """Instatiates a LCMSBase object from the mzML file.
1817
1818        Parameters
1819        ----------
1820        spectra : str, optional
1821            Which mass spectra data to include in the output. Default is "all".  Other options: "none", "ms1", "ms2".
1822        time_range : tuple or list of tuples, optional
1823            Retention time range(s) to load. Can be:
1824            - Single range: (start_time, end_time) in minutes
1825            - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes
1826            If None, loads all scans. Useful for targeted workflows to improve performance.
1827
1828        Returns
1829        -------
1830        LCMSBase
1831            LCMS object containing mass spectra data. The object is instatiated with the file location, analyzer, instrument, sample name, scan info, mz dataframe (as specifified), polarity, as well as the attributes holding the scans, retention times, and tics.
1832        """
1833        _, scan_df = self.run(
1834            spectra="none", time_range=time_range
1835        )  # first run it to just get scan info
1836        res, scan_df = self.run(
1837            scan_df=scan_df, spectra=spectra, time_range=time_range
1838        )  # second run to parse data
1839        lcms_obj = LCMSBase(
1840            self.file_location,
1841            self.analyzer,
1842            self.instrument_label,
1843            self.sample_name,
1844            self,
1845        )
1846        if spectra != "none":
1847            for key in res:
1848                key_int = int(key.replace("ms", ""))
1849                res[key] = res[key][res[key].intensity > 0]
1850                res[key] = (
1851                    res[key].sort_values(by=["scan", "mz"]).reset_index(drop=True)
1852                )
1853                lcms_obj._ms_unprocessed[key_int] = res[key]
1854        lcms_obj.scan_df = scan_df.set_index("scan", drop=False)
1855        # Check if polarity is mixed
1856        if len(set(scan_df.polarity)) > 1:
1857            raise ValueError("Mixed polarities detected in scan data")
1858        lcms_obj.polarity = scan_df.polarity.iloc[0]
1859        lcms_obj._scans_number_list = list(scan_df.scan)
1860        lcms_obj._retention_time_list = list(scan_df.scan_time)
1861        lcms_obj._tic_list = list(scan_df.tic)
1862
1863        return lcms_obj
1864
1865    def get_icr_transient_times(self):
1866        """Return a list for transient time targets for all scans, or selected scans range
1867
1868        Notes
1869        --------
1870        Resolving Power and Transient time targets based on 7T FT-ICR MS system
1871        """
1872
1873        res_trans_time = {
1874            "50": 0.384,
1875            "100000": 0.768,
1876            "200000": 1.536,
1877            "400000": 3.072,
1878            "750000": 6.144,
1879            "1000000": 12.288,
1880        }
1881
1882        firstScanNumber = self.start_scan
1883
1884        lastScanNumber = self.end_scan
1885
1886        transient_time_list = []
1887
1888        for scan in range(firstScanNumber, lastScanNumber):
1889            scan_header = self.get_scan_header(scan)
1890
1891            rp_target = scan_header["FT Resolution:"]
1892
1893            transient_time = res_trans_time.get(rp_target)
1894
1895            transient_time_list.append(transient_time)
1896
1897            # print(transient_time, rp_target)
1898
1899        return transient_time_list

A class for parsing Thermo RAW mass spectrometry data files and instatiating MassSpectraBase or LCMSBase objects

Parameters
  • file_location (str or Path): The path to the RAW file to be parsed.
  • analyzer (str, optional): The type of mass analyzer used in the instrument. Default is "Unknown".
  • instrument_label (str, optional): The name of the instrument used to acquire the data. Default is "Unknown".
  • sample_name (str, optional): The name of the sample being analyzed. If not provided, the stem of the file_location path will be used.
Attributes
  • file_location (Path): The path to the RAW file being parsed.
  • analyzer (str): The type of mass analyzer used in the instrument.
  • instrument_label (str): The name of the instrument used to acquire the data.
  • sample_name (str): The name of the sample being analyzed.
Methods
  • run(spectra=True). Parses the RAW file and returns a dictionary of mass spectra dataframes and a scan metadata dataframe.
  • get_mass_spectrum_from_scan(scan_number, polarity, auto_process=True) Parses the RAW file and returns a MassSpecBase object from a single scan.
  • get_mass_spectra_obj(). Parses the RAW file and instantiates a MassSpectraBase object.
  • get_lcms_obj(). Parses the RAW file and instantiates an LCMSBase object.
  • get_icr_transient_times(). Return a list for transient time targets for all scans, or selected scans range

Inherits from ThermoBaseClass and SpectraParserInterface

ImportMassSpectraThermoMSFileReader( file_location, analyzer='Unknown', instrument_label='Unknown', sample_name=None)
1335    def __init__(
1336        self,
1337        file_location,
1338        analyzer="Unknown",
1339        instrument_label="Unknown",
1340        sample_name=None,
1341    ):
1342        super().__init__(file_location)
1343        if isinstance(file_location, str):
1344            # if obj is a string it defaults to create a Path obj, pass the S3Path if needed
1345            file_location = Path(file_location)
1346        if not file_location.exists():
1347            raise FileExistsError("File does not exist: " + str(file_location))
1348
1349        self.file_location = file_location
1350        self.analyzer = analyzer
1351        self.instrument_label = instrument_label
1352
1353        if sample_name:
1354            self.sample_name = sample_name
1355        else:
1356            self.sample_name = file_location.stem

file_location: srt pathlib.Path or s3path.S3Path Thermo Raw file path

file_location
analyzer
instrument_label
def load(self):
1358    def load(self):
1359        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]:
1361    def get_scans_in_time_range(
1362        self,
1363        time_range: Union[Tuple[float, float], List[Tuple[float, float]]],
1364        ms_level: Optional[int] = None,
1365    ) -> List[int]:
1366        """Return scan numbers within specified retention time range(s).
1367
1368        Parameters
1369        ----------
1370        time_range : tuple or list of tuples
1371            Retention time range(s) in minutes. Can be:
1372            - Single range: (start_time, end_time)
1373            - Multiple ranges: [(start1, end1), (start2, end2), ...]
1374        ms_level : int, optional
1375            If specified, only return scans of this MS level (e.g., 1 for MS1, 2 for MS2).
1376            If None, returns scans of all MS levels.
1377
1378        Returns
1379        -------
1380        list of int
1381            List of scan numbers within the specified time range(s) and MS level.
1382        """
1383        # Normalize time range to list of tuples
1384        time_ranges = self._normalize_time_range(time_range)
1385
1386        # Get all scan data
1387        scan_df = self.get_scan_df()
1388
1389        # Filter by time range
1390        mask = pd.Series([False] * len(scan_df), index=scan_df.index)
1391        for start_time, end_time in time_ranges:
1392            mask |= (scan_df.scan_time >= start_time) & (scan_df.scan_time <= end_time)
1393
1394        filtered_df = scan_df[mask]
1395
1396        # Filter by MS level if specified
1397        if ms_level is not None:
1398            filtered_df = filtered_df[filtered_df.ms_level == ms_level]
1399
1400        return filtered_df.scan.tolist()

Return scan numbers within specified retention time range(s).

Parameters
  • time_range (tuple or list of tuples): Retention time range(s) in minutes. Can be:
    • Single range: (start_time, end_time)
    • Multiple ranges: [(start1, end1), (start2, end2), ...]
  • ms_level (int, optional): If specified, only return scans of this MS level (e.g., 1 for MS1, 2 for MS2). If None, returns scans of all MS levels.
Returns
  • list of int: List of scan numbers within the specified time range(s) and MS level.
def get_scan_df( self, time_range: Union[Tuple[float, float], List[Tuple[float, float]], NoneType] = None):
1402    def get_scan_df(
1403        self,
1404        time_range: Optional[
1405            Union[Tuple[float, float], List[Tuple[float, float]]]
1406        ] = None,
1407    ):
1408        """Return scan data as a pandas DataFrame.
1409
1410        Parameters
1411        ----------
1412        time_range : tuple or list of tuples, optional
1413            Retention time range(s) to filter scans. Can be:
1414            - Single range: (start_time, end_time) in minutes
1415            - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes
1416            If None, returns all scans.
1417
1418        Returns
1419        -------
1420        pd.DataFrame
1421            DataFrame containing scan information, optionally filtered by time range.
1422        """
1423        # This automatically brings in all the data
1424        self.chromatogram_settings.scans = (-1, -1)
1425
1426        # Get scan df info; starting with TIC data
1427        tic_data, _ = self.get_tic(ms_type="all", peak_detection=False, smooth=False)
1428        tic_data = {
1429            "scan": tic_data.scans,
1430            "scan_time": tic_data.time,
1431            "tic": tic_data.tic,
1432        }
1433        scan_df = pd.DataFrame.from_dict(tic_data)
1434        scan_df["ms_level"] = None
1435
1436        # get scan text
1437        scan_filter_df = pd.DataFrame.from_dict(
1438            self.get_all_filters()[0], orient="index"
1439        )
1440        scan_filter_df.reset_index(inplace=True)
1441        scan_filter_df.rename(columns={"index": "scan", 0: "scan_text"}, inplace=True)
1442
1443        scan_df = scan_df.merge(scan_filter_df, on="scan", how="left")
1444        scan_df["scan_window_lower"] = scan_df.scan_text.str.extract(
1445            r"\[(\d+\.\d+)-\d+\.\d+\]"
1446        )
1447        scan_df["scan_window_upper"] = scan_df.scan_text.str.extract(
1448            r"\[\d+\.\d+-(\d+\.\d+)\]"
1449        )
1450        scan_df["polarity"] = np.where(
1451            scan_df.scan_text.str.contains(" - "), "negative", "positive"
1452        )
1453        scan_df["precursor_mz"] = scan_df.scan_text.str.extract(r"(\d+\.\d+)@")
1454        scan_df["precursor_mz"] = scan_df["precursor_mz"].astype(float)
1455
1456        # Assign each scan as centroid or profile and add ms_level
1457        scan_df["ms_format"] = None
1458        for i in scan_df.scan.to_list():
1459            scan_df.loc[scan_df.scan == i, "ms_level"] = self.get_ms_level_for_scan_num(
1460                i
1461            )
1462            if self.iRawDataPlus.IsCentroidScanFromScanNumber(i):
1463                scan_df.loc[scan_df.scan == i, "ms_format"] = "centroid"
1464            else:
1465                scan_df.loc[scan_df.scan == i, "ms_format"] = "profile"
1466
1467        # Remove any non-mass spectra scans (e.g., MS level 0 or None)
1468        scan_df = scan_df[
1469            scan_df.ms_level.notnull() & (scan_df.ms_level > 0)
1470        ].reset_index(drop=True)
1471
1472        # Filter by time range if specified
1473        if time_range is not None:
1474            time_ranges = self._normalize_time_range(time_range)
1475            mask = pd.Series([False] * len(scan_df), index=scan_df.index)
1476            for start_time, end_time in time_ranges:
1477                mask |= (scan_df.scan_time >= start_time) & (
1478                    scan_df.scan_time <= end_time
1479                )
1480            scan_df = scan_df[mask].reset_index(drop=True)
1481
1482        return scan_df

Return scan data as a pandas DataFrame.

Parameters
  • time_range (tuple or list of tuples, optional): Retention time range(s) to filter scans. Can be:
    • Single range: (start_time, end_time) in minutes
    • Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes If None, returns all scans.
Returns
  • pd.DataFrame: DataFrame containing scan information, optionally filtered by time range.
def get_ms_raw( self, spectra, scan_df, time_range: Union[Tuple[float, float], List[Tuple[float, float]], NoneType] = None):
1484    def get_ms_raw(
1485        self,
1486        spectra,
1487        scan_df,
1488        time_range: Optional[
1489            Union[Tuple[float, float], List[Tuple[float, float]]]
1490        ] = None,
1491    ):
1492        """Return a dictionary of mass spectra data as pandas DataFrames.
1493
1494        Parameters
1495        ----------
1496        spectra : str
1497            Specifies which spectra to load (e.g., 'all', 'ms1', 'ms2')
1498        scan_df : pd.DataFrame
1499            Scan information DataFrame
1500        time_range : tuple or list of tuples, optional
1501            Retention time range(s) to filter scans. Can be:
1502            - Single range: (start_time, end_time) in minutes
1503            - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes
1504            If None, returns all scans. Note: filtering is typically done at scan_df level.
1505
1506        Returns
1507        -------
1508        dict
1509            Dictionary of raw mass spectra data, optionally filtered by time range.
1510        """
1511        # Note: time_range filtering is handled at the scan_df level before calling this method
1512        # The parameter is here for interface consistency with SpectraParserInterface
1513
1514        if spectra == "all":
1515            scan_df_forspec = scan_df
1516        elif spectra == "ms1":
1517            scan_df_forspec = scan_df[scan_df.ms_level == 1]
1518        elif spectra == "ms2":
1519            scan_df_forspec = scan_df[scan_df.ms_level == 2]
1520        else:
1521            raise ValueError("spectra must be 'none', 'all', 'ms1', or 'ms2'")
1522
1523        # Result container
1524        res = {}
1525
1526        # Row count container
1527        counter = {}
1528
1529        # Column name container
1530        cols = {}
1531
1532        # set at float32
1533        dtype = np.float32
1534
1535        # First pass: get nrows
1536        N = defaultdict(lambda: 0)
1537        for i in scan_df_forspec.scan.to_list():
1538            level = scan_df_forspec.loc[scan_df_forspec.scan == i, "ms_level"].values[0]
1539            scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber(i)
1540            profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber(
1541                i, scanStatistics
1542            )
1543            abun = list(profileStream.Intensities)
1544            abun = np.array(abun)[np.where(np.array(abun) > 0)[0]]
1545
1546            N[level] += len(abun)
1547
1548        # Second pass: parse
1549        for i in scan_df_forspec.scan.to_list():
1550            scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber(i)
1551            profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber(
1552                i, scanStatistics
1553            )
1554            abun = list(profileStream.Intensities)
1555            mz = list(profileStream.Positions)
1556
1557            # Get index of abun that are > 0
1558            inx = np.where(np.array(abun) > 0)[0]
1559            mz = np.array(mz)[inx]
1560            mz = np.float32(mz)
1561            abun = np.array(abun)[inx]
1562            abun = np.float32(abun)
1563
1564            level = scan_df_forspec.loc[scan_df_forspec.scan == i, "ms_level"].values[0]
1565
1566            # Number of rows
1567            n = len(mz)
1568
1569            # No measurements
1570            if n == 0:
1571                continue
1572
1573            # Dimension check
1574            if len(mz) != len(abun):
1575                warnings.warn("m/z and intensity array dimension mismatch")
1576                continue
1577
1578            # Scan/frame info
1579            id_dict = i
1580
1581            # Columns
1582            cols[level] = ["scan", "mz", "intensity"]
1583            m = len(cols[level])
1584
1585            # Subarray init
1586            arr = np.empty((n, m), dtype=dtype)
1587            inx = 0
1588
1589            # Populate scan/frame info
1590            arr[:, inx] = i
1591            inx += 1
1592
1593            # Populate m/z
1594            arr[:, inx] = mz
1595            inx += 1
1596
1597            # Populate intensity
1598            arr[:, inx] = abun
1599            inx += 1
1600
1601            # Initialize output container
1602            if level not in res:
1603                res[level] = np.empty((N[level], m), dtype=dtype)
1604                counter[level] = 0
1605
1606            # Insert subarray
1607            res[level][counter[level] : counter[level] + n, :] = arr
1608            counter[level] += n
1609
1610        # Construct ms1 and ms2 mz dataframes
1611        for level in res.keys():
1612            res[level] = pd.DataFrame(res[level])
1613            res[level].columns = cols[level]
1614        # rename keys in res to add 'ms' prefix
1615        res = {f"ms{key}": value for key, value in res.items()}
1616
1617        return res

Return a dictionary of mass spectra data as pandas DataFrames.

Parameters
  • spectra (str): Specifies which spectra to load (e.g., 'all', 'ms1', 'ms2')
  • scan_df (pd.DataFrame): Scan information DataFrame
  • time_range (tuple or list of tuples, optional): Retention time range(s) to filter scans. Can be:
    • Single range: (start_time, end_time) in minutes
    • Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes If None, returns all scans. Note: filtering is typically done at scan_df level.
Returns
  • dict: Dictionary of raw mass spectra data, optionally filtered by time range.
def run( self, spectra='all', scan_df=None, time_range: Union[Tuple[float, float], List[Tuple[float, float]], NoneType] = None):
1619    def run(
1620        self,
1621        spectra="all",
1622        scan_df=None,
1623        time_range: Optional[
1624            Union[Tuple[float, float], List[Tuple[float, float]]]
1625        ] = None,
1626    ):
1627        """
1628        Extracts mass spectra data from a raw file.
1629
1630        Parameters
1631        ----------
1632        spectra : str, optional
1633            Which mass spectra data to include in the output. Default is all.  Other options: none, ms1, ms2.
1634        scan_df : pandas.DataFrame, optional
1635            Scan dataframe.  If not provided, the scan dataframe is created from the mzML file.
1636        time_range : tuple or list of tuples, optional
1637            Retention time range(s) to filter scans. Can be:
1638            - Single range: (start_time, end_time) in minutes
1639            - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes
1640            If None, returns all scans.
1641
1642        Returns
1643        -------
1644        tuple
1645            A tuple containing two elements:
1646            - A dictionary containing mass spectra data, separated by MS level.
1647            - A pandas DataFrame containing scan information, including scan number, scan time, TIC, MS level,
1648                scan text, scan window lower and upper bounds, polarity, and precursor m/z (if applicable).
1649        """
1650        # Prepare scan_df
1651        if scan_df is None:
1652            scan_df = self.get_scan_df(time_range=time_range)
1653
1654        # Prepare mass spectra data
1655        if spectra != "none":
1656            res = self.get_ms_raw(
1657                spectra=spectra, scan_df=scan_df, time_range=time_range
1658            )
1659        else:
1660            res = None
1661
1662        return res, scan_df

Extracts mass spectra data from a raw file.

Parameters
  • spectra (str, optional): Which mass spectra data to include in the output. Default is all. Other options: none, ms1, ms2.
  • scan_df (pandas.DataFrame, optional): Scan dataframe. If not provided, the scan dataframe is created from the mzML file.
  • time_range (tuple or list of tuples, optional): Retention time range(s) to filter scans. Can be:
    • Single range: (start_time, end_time) in minutes
    • Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes If None, returns all scans.
Returns
  • tuple: A tuple containing two elements:
    • A dictionary containing mass spectra data, separated by MS level.
    • A pandas DataFrame containing scan information, including scan number, scan time, TIC, MS level, scan text, scan window lower and upper bounds, polarity, and precursor m/z (if applicable).
def get_mass_spectra_from_scan_list(self, scan_list, spectrum_mode, auto_process=True):
1664    def get_mass_spectra_from_scan_list(
1665        self, scan_list, spectrum_mode, auto_process=True
1666    ):
1667        """Instantiate multiple MassSpecBase objects from a list of scan numbers from the binary file.
1668
1669        Parameters
1670        ----------
1671        scan_list : list[int]
1672            A list of scan numbers to extract the mass spectra from.
1673        spectrum_mode : str
1674            The type of mass spectrum to extract.  Must be 'profile' or 'centroid'.
1675            All scans in the list must be of the same type.
1676        auto_process : bool, optional
1677            If True, perform peak picking and noise threshold calculation after creating the mass spectrum object. Default is True.
1678        """
1679        mass_spectra = []
1680        for scan in scan_list:
1681            mass_spectrum = self.get_mass_spectrum_from_scan(
1682                scan, spectrum_mode, auto_process=auto_process
1683            )
1684            mass_spectra.append(mass_spectrum)
1685
1686        return mass_spectra

Instantiate multiple MassSpecBase objects from a list of scan numbers from the binary file.

Parameters
  • scan_list (list[int]): A list of scan numbers to extract the mass spectra from.
  • spectrum_mode (str): The type of mass spectrum to extract. Must be 'profile' or 'centroid'. All scans in the list must be of the same type.
  • auto_process (bool, optional): If True, perform peak picking and noise threshold calculation after creating the mass spectrum object. Default is True.
def get_mass_spectrum_from_scan(self, scan_number, spectrum_mode, auto_process=True):
1688    def get_mass_spectrum_from_scan(
1689        self, scan_number, spectrum_mode, auto_process=True
1690    ):
1691        """Instantiate a MassSpecBase object from a single scan number from the binary file.
1692
1693        Parameters
1694        ----------
1695        scan_number : int
1696            The scan number to extract the mass spectrum from.
1697        polarity : int
1698            The polarity of the scan.  1 for positive mode, -1 for negative mode.
1699        spectrum_mode : str
1700            The type of mass spectrum to extract.  Must be 'profile' or 'centroid'.
1701        auto_process : bool, optional
1702            If True, perform peak picking and noise threshold calculation after creating the mass spectrum object. Default is True.
1703
1704        Returns
1705        -------
1706        MassSpecProfile | MassSpecCentroid
1707            The MassSpecProfile or MassSpecCentroid object containing the parsed mass spectrum.
1708        """
1709
1710        if spectrum_mode == "profile":
1711            scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber(scan_number)
1712            profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber(
1713                scan_number, scanStatistics
1714            )
1715            abun = list(profileStream.Intensities)
1716            mz = list(profileStream.Positions)
1717            data_dict = {
1718                Labels.mz: mz,
1719                Labels.abundance: abun,
1720            }
1721            d_params = self.set_metadata(
1722                firstScanNumber=scan_number,
1723                lastScanNumber=scan_number,
1724                scans_list=False,
1725                label=Labels.thermo_profile,
1726            )
1727            mass_spectrum_obj = MassSpecProfile(
1728                data_dict, d_params, auto_process=auto_process
1729            )
1730
1731        elif spectrum_mode == "centroid":
1732            centroid_scan = self.iRawDataPlus.GetCentroidStream(scan_number, False)
1733            if centroid_scan.Masses is not None:
1734                mz = list(centroid_scan.Masses)
1735                abun = list(centroid_scan.Intensities)
1736                rp = list(centroid_scan.Resolutions)
1737                magnitude = list(centroid_scan.Intensities)
1738                noise = list(centroid_scan.Noises)
1739                baselines = list(centroid_scan.Baselines)
1740                array_noise_std = (np.array(noise) - np.array(baselines)) / 3
1741                l_signal_to_noise = np.array(magnitude) / array_noise_std
1742                data_dict = {
1743                    Labels.mz: mz,
1744                    Labels.abundance: abun,
1745                    Labels.rp: rp,
1746                    Labels.s2n: list(l_signal_to_noise),
1747                }
1748            else:  # For CID MS2, the centroid data are stored in the profile data location, they do not have any associated rp or baseline data, but they should be treated as centroid data
1749                scanStatistics = self.iRawDataPlus.GetScanStatsForScanNumber(
1750                    scan_number
1751                )
1752                profileStream = self.iRawDataPlus.GetSegmentedScanFromScanNumber(
1753                    scan_number, scanStatistics
1754                )
1755                abun = list(profileStream.Intensities)
1756                mz = list(profileStream.Positions)
1757                data_dict = {
1758                    Labels.mz: mz,
1759                    Labels.abundance: abun,
1760                    Labels.rp: [np.nan] * len(mz),
1761                    Labels.s2n: [np.nan] * len(mz),
1762                }
1763            d_params = self.set_metadata(
1764                firstScanNumber=scan_number,
1765                lastScanNumber=scan_number,
1766                scans_list=False,
1767                label=Labels.thermo_centroid,
1768            )
1769            mass_spectrum_obj = MassSpecCentroid(
1770                data_dict, d_params, auto_process=auto_process
1771            )
1772
1773        return mass_spectrum_obj

Instantiate a MassSpecBase object from a single scan number from the binary file.

Parameters
  • scan_number (int): The scan number to extract the mass spectrum from.
  • polarity (int): The polarity of the scan. 1 for positive mode, -1 for negative mode.
  • spectrum_mode (str): The type of mass spectrum to extract. Must be 'profile' or 'centroid'.
  • auto_process (bool, optional): If True, perform peak picking and noise threshold calculation after creating the mass spectrum object. Default is True.
Returns
  • MassSpecProfile | MassSpecCentroid: The MassSpecProfile or MassSpecCentroid object containing the parsed mass spectrum.
def get_mass_spectra_obj( self, time_range: Union[Tuple[float, float], List[Tuple[float, float]], NoneType] = None):
1775    def get_mass_spectra_obj(
1776        self,
1777        time_range: Optional[
1778            Union[Tuple[float, float], List[Tuple[float, float]]]
1779        ] = None,
1780    ):
1781        """Instatiate a MassSpectraBase object from the binary data file file.
1782
1783        Parameters
1784        ----------
1785        time_range : tuple or list of tuples, optional
1786            Retention time range(s) to load. Can be:
1787            - Single range: (start_time, end_time) in minutes
1788            - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes
1789            If None, loads all scans. Useful for targeted workflows to improve performance.
1790
1791        Returns
1792        -------
1793        MassSpectraBase
1794            The MassSpectra object containing the parsed mass spectra.  The object is instatiated with the mzML file, analyzer, instrument, sample name, and scan dataframe.
1795        """
1796        _, scan_df = self.run(spectra="none", time_range=time_range)
1797        mass_spectra_obj = MassSpectraBase(
1798            self.file_location,
1799            self.analyzer,
1800            self.instrument_label,
1801            self.sample_name,
1802            self,
1803        )
1804        scan_df = scan_df.set_index("scan", drop=False)
1805        mass_spectra_obj.scan_df = scan_df
1806
1807        return mass_spectra_obj

Instatiate a MassSpectraBase object from the binary data file file.

Parameters
  • time_range (tuple or list of tuples, optional): Retention time range(s) to load. Can be:
    • Single range: (start_time, end_time) in minutes
    • Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes If None, loads all scans. Useful for targeted workflows to improve performance.
Returns
  • MassSpectraBase: The MassSpectra object containing the parsed mass spectra. The object is instatiated with the mzML file, analyzer, instrument, sample name, and scan dataframe.
def get_lcms_obj( self, spectra='all', time_range: Union[Tuple[float, float], List[Tuple[float, float]], NoneType] = None):
1809    def get_lcms_obj(
1810        self,
1811        spectra="all",
1812        time_range: Optional[
1813            Union[Tuple[float, float], List[Tuple[float, float]]]
1814        ] = None,
1815    ):
1816        """Instatiates a LCMSBase object from the mzML file.
1817
1818        Parameters
1819        ----------
1820        spectra : str, optional
1821            Which mass spectra data to include in the output. Default is "all".  Other options: "none", "ms1", "ms2".
1822        time_range : tuple or list of tuples, optional
1823            Retention time range(s) to load. Can be:
1824            - Single range: (start_time, end_time) in minutes
1825            - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes
1826            If None, loads all scans. Useful for targeted workflows to improve performance.
1827
1828        Returns
1829        -------
1830        LCMSBase
1831            LCMS object containing mass spectra data. The object is instatiated with the file location, analyzer, instrument, sample name, scan info, mz dataframe (as specifified), polarity, as well as the attributes holding the scans, retention times, and tics.
1832        """
1833        _, scan_df = self.run(
1834            spectra="none", time_range=time_range
1835        )  # first run it to just get scan info
1836        res, scan_df = self.run(
1837            scan_df=scan_df, spectra=spectra, time_range=time_range
1838        )  # second run to parse data
1839        lcms_obj = LCMSBase(
1840            self.file_location,
1841            self.analyzer,
1842            self.instrument_label,
1843            self.sample_name,
1844            self,
1845        )
1846        if spectra != "none":
1847            for key in res:
1848                key_int = int(key.replace("ms", ""))
1849                res[key] = res[key][res[key].intensity > 0]
1850                res[key] = (
1851                    res[key].sort_values(by=["scan", "mz"]).reset_index(drop=True)
1852                )
1853                lcms_obj._ms_unprocessed[key_int] = res[key]
1854        lcms_obj.scan_df = scan_df.set_index("scan", drop=False)
1855        # Check if polarity is mixed
1856        if len(set(scan_df.polarity)) > 1:
1857            raise ValueError("Mixed polarities detected in scan data")
1858        lcms_obj.polarity = scan_df.polarity.iloc[0]
1859        lcms_obj._scans_number_list = list(scan_df.scan)
1860        lcms_obj._retention_time_list = list(scan_df.scan_time)
1861        lcms_obj._tic_list = list(scan_df.tic)
1862
1863        return lcms_obj

Instatiates a LCMSBase object from the mzML file.

Parameters
  • spectra (str, optional): Which mass spectra data to include in the output. Default is "all". Other options: "none", "ms1", "ms2".
  • time_range (tuple or list of tuples, optional): Retention time range(s) to load. Can be:
    • Single range: (start_time, end_time) in minutes
    • Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes If None, loads all scans. Useful for targeted workflows to improve performance.
Returns
  • LCMSBase: LCMS object containing mass spectra data. The object is instatiated with the file location, analyzer, instrument, sample name, scan info, mz dataframe (as specifified), polarity, as well as the attributes holding the scans, retention times, and tics.
def get_icr_transient_times(self):
1865    def get_icr_transient_times(self):
1866        """Return a list for transient time targets for all scans, or selected scans range
1867
1868        Notes
1869        --------
1870        Resolving Power and Transient time targets based on 7T FT-ICR MS system
1871        """
1872
1873        res_trans_time = {
1874            "50": 0.384,
1875            "100000": 0.768,
1876            "200000": 1.536,
1877            "400000": 3.072,
1878            "750000": 6.144,
1879            "1000000": 12.288,
1880        }
1881
1882        firstScanNumber = self.start_scan
1883
1884        lastScanNumber = self.end_scan
1885
1886        transient_time_list = []
1887
1888        for scan in range(firstScanNumber, lastScanNumber):
1889            scan_header = self.get_scan_header(scan)
1890
1891            rp_target = scan_header["FT Resolution:"]
1892
1893            transient_time = res_trans_time.get(rp_target)
1894
1895            transient_time_list.append(transient_time)
1896
1897            # print(transient_time, rp_target)
1898
1899        return transient_time_list

Return a list for transient time targets for all scans, or selected scans range

Notes

Resolving Power and Transient time targets based on 7T FT-ICR MS system