def readInterval(self, pulseIdInterval, path=None):
        """ **[DEPRECATED]** Access to data by an macrobunch ID interval.
        Useful for scans that would otherwise hit the machine's memory limit.

        **Parameters**\n
        pulseIdInterval: (int, int)
            The starting and ending macrobunch IDs, [start, end).
        path: str | None (default to ``self.DATA_RAW_DIR``)
            The path to location where raw HDF5 files are stored.
        """

        # allow for using the default path, which can be redefined as class variable. leaving retrocompatibility
        print('WARNING: readInterval method is obsolete. Please use readData(pulseIdInterval=xxx).')

        if path is None:
            path = self.DATA_RAW_DIR

        self.pulseIdInterval = pulseIdInterval
        # Import the dataset
        dldPosXName = "/uncategorised/FLASH1_USER2/FLASH.FEL/HEXTOF.DAQ/DLD1:0/dset"
        dldPosYName = "/uncategorised/FLASH1_USER2/FLASH.FEL/HEXTOF.DAQ/DLD1:1/dset"
        dldTimeName = "/uncategorised/FLASH1_USER2/FLASH.FEL/HEXTOF.DAQ/DLD1:3/dset"

        dldMicrobunchIdName = "/uncategorised/FLASH1_USER2/FLASH.FEL/HEXTOF.DAQ/DLD1:2/dset"
        dldAuxName = "/uncategorised/FLASH1_USER2/FLASH.FEL/HEXTOF.DAQ/DLD1:4/dset"
        # delayStageName = "/Experiment/Pump probe laser/laser delay"
        # ENC.DELAY seems to be the wrong channel! Values appear in groups of ~10 identical values
        # -> ENC.DELAY is read out with 1 Hz
        # delayStageName = "/Experiment/Pump probe laser/delay line IK220.0/ENC.DELAY"
        # Proper channel is culumn with index 1 of ENC
        delayStageName = "/Experiment/Pump probe laser/delay line IK220.0/ENC"

        bamName = '/Electron Diagnostic/BAM/4DBC3/electron bunch arrival time (low charge)'
        bunchChargeName = '/Electron Diagnostic/Bunch charge/after undulator'
        macroBunchPulseIdName = '/Timing/Bunch train info/index 1.sts'
        opticalDiodeName = '/Experiment/PG/SIS8300 100MHz ADC/CH9/pulse energy/TD'
        gmdTunnelName = '/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel'
        gmdBdaName = '/Photon Diagnostic/GMD/Pulse resolved energy/energy BDA'

        # adc1Name = '/Experiment/PG/SIS8300 100MHz ADC/CH6/TD'
        # adc2Name = '/Experiment/PG/SIS8300 100MHz ADC/CH7/TD'

        daqAccess = BeamtimeDaqAccess.create(path)

        print('reading DAQ data')
        # ~ print("reading dldPosX")
        self.dldPosX = daqAccess.valuesOfInterval(dldPosXName, pulseIdInterval)
        # ~ print("reading dldPosY")
        self.dldPosY = daqAccess.valuesOfInterval(dldPosYName, pulseIdInterval)
        # ~ print("reading dldTime")
        self.dldTime = daqAccess.valuesOfInterval(dldTimeName, pulseIdInterval)
        # ~ print("reading dldMicrobunchId")
        self.dldMicrobunchId = daqAccess.valuesOfInterval(dldMicrobunchIdName, pulseIdInterval)
        # ~ print("reading dldAux")
        self.dldAux = daqAccess.valuesOfInterval(dldAuxName, pulseIdInterval)

        # ~ print("reading delayStage")
        self.delayStage = daqAccess.valuesOfInterval(delayStageName, pulseIdInterval)
        self.delayStage = self.delayStage[:, 1]

        # ~ print("reading BAM")
        self.bam = daqAccess.valuesOfInterval(bamName, pulseIdInterval)
        self.opticalDiode = daqAccess.valuesOfInterval(opticalDiodeName, pulseIdInterval)
        # ~ print("reading bunchCharge")
        self.bunchCharge = daqAccess.valuesOfInterval(bunchChargeName, pulseIdInterval)
        self.macroBunchPulseId = daqAccess.valuesOfInterval(macroBunchPulseIdName, pulseIdInterval)
        # self.macroBunchPulseId -= self.macroBunchPulseId[self.macroBunchPulseId > 0].min()
        self.macroBunchPulseId -= pulseIdInterval[0]
        self.gmdTunnel = daqAccess.valuesOfInterval(gmdTunnelName, pulseIdInterval)
        self.gmdBda = daqAccess.valuesOfInterval(gmdBdaName, pulseIdInterval)
        electronsToCount = self.dldPosX.copy().flatten()
        electronsToCount = np.nan_to_num(electronsToCount)
        electronsToCount = electronsToCount[electronsToCount > 0]
        electronsToCount = electronsToCount[electronsToCount < 10000]
        numOfElectrons = len(electronsToCount)
        print("Number of electrons: {0:,} ".format(numOfElectrons))
        print("Creating data frame: Please wait...")
        self.createDataframePerElectron()
        self.createDataframePerMicrobunch()
        print('dataframe created')
    def readData(self, runNumber=None, pulseIdInterval=None, path=None):
        """Read data by run number or macrobunch pulseID interval.

        Useful for scans that would otherwise hit the machine's memory limit.

        **Parameters**\n
        runNumber: int | None (default to ``self.runNumber``)
            number of the run from which to read data. If None, requires pulseIdInterval.
        pulseIdInterval: (int, int) | None (default to ``self.pulseIdInterval``)
            first and last macrobunches of selected data range. If None, the whole run
            defined by runNumber will be taken.
        path: str | None (default to ``self.DATA_RAW_DIR``)
            path to location where raw HDF5 files are stored.

        This is a union of the readRun and readInterval methods defined in previous versions.
        """

        # Update instance attributes based on input parameters
        if runNumber is None:
            runNumber = self.runNumber
        else:
            self.runNumber = runNumber

        if pulseIdInterval is None:
            pulseIdInterval = self.pulseIdInterval
        else:
            self.pulseIdInterval = pulseIdInterval

        if (pulseIdInterval is None) and (runNumber is None):
            raise ValueError('Need either runNumber or pulseIdInterval to know what data to read.')


        if path is not None:
            try:
                daqAccess = BeamtimeDaqAccess.create(path)
            except:
                self.path_to_run = misc.get_path_to_run(runNumber, path)
                daqAccess = BeamtimeDaqAccess.create(self.path_to_run)
        else:
            path = self.DATA_RAW_DIR
            self.path_to_run = misc.get_path_to_run(runNumber, path)
            daqAccess = BeamtimeDaqAccess.create(self.path_to_run)
        
        self.daqAddresses = []
        self.pulseIdInterval = self.getIds(runNumber, path)
        # Parse the settings file in the DAQ channels section for the list of
        # h5 addresses to read from raw and add to the dataframe.
        print('loading data...')

        for name, entry in self.settings['DAQ channels'].items():
            name = misc.camelCaseIt(name)
            val = str(entry)
            if daqAccess.isChannelAvailable(val, self.pulseIdInterval):
                self.daqAddresses.append(name)
                if _VERBOSE:
                    print('assigning address: {}: {}'.format(name.ljust(20), val))
                setattr(self, name, val)
            else:
                # if _VERBOSE:
                print('skipping address missing from data: {}: {}'.format(name.ljust(20), val))

        # TODO: get the available pulse id from PAH
        if pulseIdInterval is None:
            print('Reading DAQ data from run {}... Please wait...'.format(runNumber))

            for address_name in self.daqAddresses:
                if _VERBOSE:
                    print('reading address: {}'.format(address_name))
                try:
                    attrVal = getattr(self, address_name)
                    values, otherStuff = daqAccess.allValuesOfRun(attrVal, runNumber)
                except AssertionError:
                    print('Assertion error: {}'.format(address_name, attrVal, values, otherStuff ))

                setattr(self, address_name, values)
                if address_name == 'macroBunchPulseId':  # catch the value of the first macrobunchID
                    pulseIdInterval = (otherStuff[0], otherStuff[-1])
                    self.pulseIdInterval = pulseIdInterval
                    macroBunchPulseId_correction = pulseIdInterval[0]

                if address_name == 'timeStamp':  # catch the time stamps
                    startEndTime = (values[0,0], values[-1,0])
                    self.startEndTime = startEndTime

            numOfMacrobunches = pulseIdInterval[1] - pulseIdInterval[0]



        else:
            print('reading DAQ data from interval {}'.format(pulseIdInterval))
            self.pulseIdInterval = pulseIdInterval
            for address_name in self.daqAddresses:
                if _VERBOSE:
                    print('reading address: {}'.format(address_name))
                setattr(self, address_name, daqAccess.valuesOfInterval(getattr(self, address_name), pulseIdInterval))
            numOfMacrobunches = pulseIdInterval[1] - pulseIdInterval[0]
            macroBunchPulseId_correction = pulseIdInterval[0]

        # necessary corrections for specific channels:
        try:
            self.delayStage = self.delayStage[:, 1]
        except:
            try:
                self.delayStage = self.delayStage[:, 0]
                print('1030nm Laser')
            except:
                print('no delay stage')
        self.macroBunchPulseId -= macroBunchPulseId_correction
        self.dldMicrobunchId -= self.UBID_OFFSET

        if _VERBOSE:
            print('Counting electrons...')

        electronsToCount = self.dldPosX.copy().flatten()
        electronsToCount = np.nan_to_num(electronsToCount)
        electronsToCount = electronsToCount[electronsToCount > 0]
        electronsToCount = electronsToCount[electronsToCount < 10000]
        self.numOfElectrons = len(electronsToCount)
        self.electronsPerMacrobunch = int(self.numOfElectrons / numOfMacrobunches)

        self.runInfo = {
            'runNumber':self.runNumber,
            'pulseIdInterval':self.pulseIdInterval,
            'numberOfMacrobunches': numOfMacrobunches,
            'numberOfElectrons':self.numOfElectrons,
            'electronsPerMacrobunch': self.electronsPerMacrobunch,
        }
        try:
            self.runInfo['timestampStart'] = self.startEndTime[0].astype(int)
            self.runInfo['timestampStop'] = self.startEndTime[1].astype(int)
            self.runInfo['timestampDuration'] = self.startEndTime[1]-self.startEndTime[0].astype(int)
            self.runInfo['timeStart'] = datetime.utcfromtimestamp(self.startEndTime[0]).strftime('%Y-%m-%d %H:%M:%S')
            self.runInfo['timeStop'] = datetime.utcfromtimestamp(self.startEndTime[1]).strftime('%Y-%m-%d %H:%M:%S')
            self.runInfo['timeDuration'] = datetime.timedelta(self.startEndTime[1]-self.startEndTime[0])
        except:
            self.runInfo['timestampStart'] = None
            self.runInfo['timestampStop'] = None
            self.runInfo['timestampDuration'] = None
            self.runInfo['timeStart'] = None
            self.runInfo['timeStop'] = None
            self.runInfo['timeDuration'] = None

        self.printRunOverview()

        # Old Print style
        # print('Run {0} contains {1:,} Macrobunches, from {2:,} to {3:,}' \
        #       .format(runNumber, numOfMacrobunches, pulseIdInterval[0], pulseIdInterval[1]))
        # try:
        #     print("start time: {}, end time: {}, total time: {}"
        #           .format(datetime.utcfromtimestamp(startEndTime[0]).strftime('%Y-%m-%d %H:%M:%S'),
        #                   datetime.utcfromtimestamp(startEndTime[1]).strftime('%Y-%m-%d %H:%M:%S'),
        #                   datetime.utcfromtimestamp(startEndTime[1] - startEndTime[0]).strftime('%H:%M:%S')))
        # except:
        #     pass
        #
        # print("Number of electrons: {0:,}; {1:,} e/Mb ".format(self.numOfElectrons, self.electronsPerMacrobunch))

        print("Creating dataframes... Please wait...")
        with ProgressBar():
            self.createDataframePerElectron()
            print('Electron dataframe created.')
            self.createDataframePerMicrobunch()
            print('Microbunch dataframe created.')
            print('Reading Complete.')
    def readRun(self, runNumber=None, path=None):
        """ **[DEPRECATED]** Read a run. Generates dd and dd_micrubunches attributes
        as pd.DataFrame containing data from the given run.

        **Parameters**\n
        runNumber: int | None
            number corresponding to the rung to read data from. if None, it uses the value
            defined in the runNumber attribute.
        path: str | None
            path to location where raw HDF5 files are stored. If None, it uses the value from SETTINGS.ini.

        **Raise**\n
            Throws AttributeError if the run number is not given

        :Example:
        ::
        
            processor = DldFlashProcessor()
            processor.readRun(19059)

        """

        print('WARNING: readRun method is obsolete. Please use readData(runNumber=xxx).')

        if path is None:  # allow for using the default path, which can be redefined as class variable.
            path = self.DATA_RAW_DIR
        if runNumber is None:
            runNumber = self.runNumber
            assert runNumber is not None, 'No run number assigned!'
        else:
            self.runNumber = runNumber
        if self.runNumber is None:
            raise AttributeError('Run number not defined. ')
        # Import the dataset
        dldPosXName = "/uncategorised/FLASH1_USER2/FLASH.FEL/HEXTOF.DAQ/DLD1:0/dset"
        dldPosYName = "/uncategorised/FLASH1_USER2/FLASH.FEL/HEXTOF.DAQ/DLD1:1/dset"
        dldTimeName = "/uncategorised/FLASH1_USER2/FLASH.FEL/HEXTOF.DAQ/DLD1:3/dset"

        dldMicrobunchIdName = "/uncategorised/FLASH1_USER2/FLASH.FEL/HEXTOF.DAQ/DLD1:2/dset"
        dldAuxName = "/uncategorised/FLASH1_USER2/FLASH.FEL/HEXTOF.DAQ/DLD1:4/dset"
        # delayStageName = "/Experiment/Pump probe laser/laser delay"
        # ENC.DELAY seems to be the wrong channel! Values appear in groups of exactly the same value
        # delayStageName = "/Experiment/Pump probe laser/delay line IK220.0/ENC.DELAY"
        # Proper channel is column with index 1 of ENC
        delayStageName = "/Experiment/Pump probe laser/delay line IK220.0/ENC"

        bamName = '/Electron Diagnostic/BAM/4DBC3/electron bunch arrival time (low charge)'
        bunchChargeName = '/Electron Diagnostic/Bunch charge/after undulator'
        macroBunchPulseIdName = '/Timing/Bunch train info/index 1.sts'
        opticalDiodeName = '/Experiment/PG/SIS8300 100MHz ADC/CH9/pulse energy/TD'
        gmdTunnelName = '/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel'
        gmdBdaName = '/Photon Diagnostic/GMD/Pulse resolved energy/energy BDA'

        # adc1Name = '/Experiment/PG/SIS8300 100MHz ADC/CH6/TD'
        # adc2Name = '/Experiment/PG/SIS8300 100MHz ADC/CH7/TD'

        daqAccess = BeamtimeDaqAccess.create(path)

        print('reading DAQ data')
        # ~ print("reading dldPosX")
        self.dldPosX, otherStuff = daqAccess.allValuesOfRun(dldPosXName, runNumber)
        print('run contains macrobunchID from {0:,} to {1:,} \n-> {2:,} total macrobunches'.format(otherStuff[0],
                                                                                                   otherStuff[1],
                                                                                                   otherStuff[1] -
                                                                                                   otherStuff[0]))
        # ~ print("reading dldPosY")
        self.dldPosY, otherStuff = daqAccess.allValuesOfRun(dldPosYName, runNumber)
        # ~ print("reading dldTime")
        self.dldTime, otherStuff = daqAccess.allValuesOfRun(dldTimeName, runNumber)
        # ~ print("reading dldMicrobunchId")
        self.dldMicrobunchId, otherStuff = daqAccess.allValuesOfRun(dldMicrobunchIdName, runNumber)
        # ~ print("reading dldAux")
        self.dldAux, otherStuff = daqAccess.allValuesOfRun(dldAuxName, runNumber)

        # ~ print("reading delayStage")
        self.delayStage, otherStuff = daqAccess.allValuesOfRun(delayStageName, runNumber)
        self.delayStage = self.delayStage[:, 1]

        # ~ print("reading BAM")
        self.bam, otherStuff = daqAccess.allValuesOfRun(bamName, runNumber)
        self.opticalDiode, otherStuff = daqAccess.allValuesOfRun(opticalDiodeName, runNumber)
        # ~ print("reading bunchCharge")
        self.bunchCharge, otherStuff = daqAccess.allValuesOfRun(bunchChargeName, runNumber)
        self.macroBunchPulseId, otherStuff = daqAccess.allValuesOfRun(macroBunchPulseIdName, runNumber)
        self.macroBunchPulseId -= otherStuff[0]
        self.gmdTunnel, otherStuff = daqAccess.allValuesOfRun(gmdTunnelName, runNumber)
        self.gmdBda, otherStuff = daqAccess.allValuesOfRun(gmdBdaName, runNumber)
        electronsToCount = self.dldPosX.copy().flatten()
        electronsToCount = np.nan_to_num(electronsToCount)
        electronsToCount = electronsToCount[electronsToCount > 0]
        electronsToCount = electronsToCount[electronsToCount < 10000]
        numOfElectrons = len(electronsToCount)
        print("Number of electrons: {0:,} ".format(numOfElectrons))
        print("Creating data frame: Please wait...")
        self.createDataframePerElectron()
        self.createDataframePerMicrobunch()
        print('dataframe created')
    def readData(self, runNumber=None, pulseIdInterval=None, path=None):
        """Read data by run number or macrobunch pulseID interval.

        Useful for scans that would otherwise hit the machine's memory limit.

        :Parameters:
            runNumber : int | None (default to ``self.runNumber``)
                number of the run from which to read data. If None, requires pulseIdInterval.
            pulseIdInterval : (int, int) | None (default to ``self.pulseIdInterval``)
                first and last macrobunches of selected data range. If None, the whole run
                defined by runNumber will be taken.
            path : str | None (default to ``self.DATA_RAW_DIR``)
                path to location where raw HDF5 files are stored

        This is a union of the readRun and readInterval methods defined in previous versions.
        """

        # Update instance attributes based on input parameters
        if runNumber is None:
            runNumber = self.runNumber
        else:
            self.runNumber = runNumber

        if pulseIdInterval is None:
            pulseIdInterval = self.pulseIdInterval
        else:
            self.pulseIdInterval = pulseIdInterval

        if (pulseIdInterval is None) and (runNumber is None):
            raise ValueError(
                'Need either runNumber or pulseIdInterval to know what data to read.'
            )

        # parse settings and set all dataset addresses as attributes.
        settings = ConfigParser()
        if os.path.isfile(
                os.path.join(os.path.dirname(__file__), 'SETTINGS.ini')):
            settings.read(
                os.path.join(os.path.dirname(__file__), 'SETTINGS.ini'))
        else:
            settings.read(
                os.path.join(os.path.dirname(os.path.dirname(__file__)),
                             'SETTINGS.ini'))

        section = 'DAQ channels'

        print('searching for data...')
        if path is not None:
            try:
                daqAccess = BeamtimeDaqAccess.create(path)
            except:
                self.path_to_run = misc.get_path_to_run(runNumber, path)
                daqAccess = BeamtimeDaqAccess.create(self.path_to_run)
        else:
            path = self.DATA_RAW_DIR
            self.path_to_run = misc.get_path_to_run(runNumber, path)
            daqAccess = BeamtimeDaqAccess.create(self.path_to_run)

        self.daqAddresses = []
        for entry in settings[section]:
            name = misc.camelCaseIt(entry)
            val = str(settings[section][entry])
            if daqAccess.isChannelAvailable(val, self.getIds(runNumber, path)):
                self.daqAddresses.append(name)
                if _VERBOSE:
                    print('assigning address: {}: {}'.format(
                        name.ljust(20), val))
                setattr(self, name, val)
            else:
                # if _VERBOSE:
                print('skipping address missing from data: {}: {}'.format(
                    name.ljust(20), val))

        # TODO: get the available pulse id from PAH
        if pulseIdInterval is None:
            print('Reading DAQ data from run {}... Please wait...'.format(
                runNumber))

            for address_name in self.daqAddresses:
                if _VERBOSE:
                    print('reading address: {}'.format(address_name))
                try:
                    attrVal = getattr(self, address_name)
                    values, otherStuff = daqAccess.allValuesOfRun(
                        attrVal, runNumber)
                except AssertionError:
                    print('Assertion error: {}'.format(address_name, attrVal,
                                                       values, otherStuff))

                setattr(self, address_name, values)
                if address_name == 'macroBunchPulseId':  # catch the value of the first macrobunchID
                    pulseIdInterval = (otherStuff[0], otherStuff[-1])
                    self.pulseIdInterval = pulseIdInterval
                    macroBunchPulseId_correction = pulseIdInterval[0]

                if address_name == 'timeStamp':  # catch the time stamps
                    startEndTime = (values[0, 0], values[-1, 0])
                    self.startEndTime = startEndTime

            numOfMacrobunches = pulseIdInterval[1] - pulseIdInterval[0]
            print('Run {0} contains {1:,} Macrobunches, from {2:,} to {3:,}'\
                .format(runNumber, numOfMacrobunches, pulseIdInterval[0], pulseIdInterval[1]))

            print("start time: {}, end time: {}, total time: {}".format(
                datetime.utcfromtimestamp(
                    startEndTime[0]).strftime('%Y-%m-%d %H:%M:%S'),
                datetime.utcfromtimestamp(
                    startEndTime[1]).strftime('%Y-%m-%d %H:%M:%S'),
                datetime.utcfromtimestamp(
                    startEndTime[1] - startEndTime[0]).strftime('%H:%M:%S')))

        else:
            print('reading DAQ data from interval {}'.format(pulseIdInterval))
            self.pulseIdInterval = pulseIdInterval
            for address_name in self.daqAddresses:
                if _VERBOSE:
                    print('reading address: {}'.format(address_name))
                setattr(
                    self, address_name,
                    daqAccess.valuesOfInterval(getattr(self, address_name),
                                               pulseIdInterval))
            numOfMacrobunches = pulseIdInterval[1] - pulseIdInterval[0]
            macroBunchPulseId_correction = pulseIdInterval[0]

        # necessary corrections for specific channels:
        self.delayStage = self.delayStage[:, 1]
        self.macroBunchPulseId -= macroBunchPulseId_correction
        self.dldMicrobunchId -= self.UBID_OFFSET

        if _VERBOSE:
            print('Counting electrons...')

        electronsToCount = self.dldPosX.copy().flatten()
        electronsToCount = np.nan_to_num(electronsToCount)
        electronsToCount = electronsToCount[electronsToCount > 0]
        electronsToCount = electronsToCount[electronsToCount < 10000]
        self.numOfElectrons = len(electronsToCount)
        self.electronsPerMacrobunch = int(self.numOfElectrons /
                                          numOfMacrobunches)
        print("Number of electrons: {0:,}; {1:,} e/Mb ".format(
            self.numOfElectrons, self.electronsPerMacrobunch))

        print("Creating dataframes... Please wait...")
        pbar = ProgressBar()
        with pbar:
            self.createDataframePerElectron()
            print('Electron dataframe created.')
            self.createDataframePerMicrobunch()
            print('Microbunch dataframe created.')