Aquire mass error data, observed vs expected m/z, for calibration of MS1
    ion m/z values. Expected m/z values can be of ambient ions with known exact
    masses and from identified precursor masses of peptide spectrum matching
    results.

    :param msrunContainer: intance of :class:`maspy.core.MsrunContainer`
    :param specfile: filename of an ms-run file, used to extract mass error data
    :param siiArrays: optional, a dictionary of numpy.arrays
        Must provide the keys "obsMz" (observed precursor m/z), "excMz" (exact
        calculated m/z of the identified peptide) and "precursorId" (the scan id
        of the MS1 that preceeded the respective MSn scan).
    :param lockMass: None, True or a list of lock mass m/z values
        If ``None`` lock masses are not used to aquire calibration data. If
        ``True`` use the seven predefined lock mass values. Else use the
        specified lock mass m/z values.
    :param scanRange: a list of positive and negative int values
        When using MSn spectrum identifications for aquiring calibration data
        the m/z value of the peptide precursor is used to find the corresponding
        ion m/z value in the preceeding MS1 scan. By adding positive and
        negative integers to the ``scanRange`` parameter the subsequent and
        preceeding MS1 scans are also used to aquire calibration data from the
        same peptide precursor. By default "[-1, 0, 1]".
    :param massTolerance: float or int
        The maximal allowed deviation for matching two masses. By default
        "10 * 1e-6" which corresponds to 10ppm.
    :param toleranceMode: "relative" or "absolute"
        Specifies how the ``massTolerance`` value is applied, by default
        "relative
    :param topIntMatches: bool, by default False
    :param useTopIntIons: bool,  by default False

    :returns: {'siId': numpy.array([str, ...]),
               'rt': numpy.array([float, ...]),
               'obsMz': numpy.array([float, ...]),
               'excMz': numpy.array([float, ...]),
               'relDev': numpy.array([float, ...]),
               'absDev': numpy.array([float, ...]),
               'int': numpy.array([float, ...])
               'source': numpy.array(['psm' or 'lock'])
               }

def aquireMs1CalibrationData(msrunContainer, specfile, siiArrays=None,
    lockMass=None, **kwargs):
    """Aquire mass error data, observed vs expected m/z, for calibration of MS1
    ion m/z values. Expected m/z values can be of ambient ions with known exact
    masses and from identified precursor masses of peptide spectrum matching
    results.

    :param msrunContainer: intance of :class:`maspy.core.MsrunContainer`
    :param specfile: filename of an ms-run file, used to extract mass error data
    :param siiArrays: optional, a dictionary of numpy.arrays
        Must provide the keys "obsMz" (observed precursor m/z), "excMz" (exact
        calculated m/z of the identified peptide) and "precursorId" (the scan id
        of the MS1 that preceeded the respective MSn scan).
    :param lockMass: None, True or a list of lock mass m/z values
        If ``None`` lock masses are not used to aquire calibration data. If
        ``True`` use the seven predefined lock mass values. Else use the
        specified lock mass m/z values.
    :param scanRange: a list of positive and negative int values
        When using MSn spectrum identifications for aquiring calibration data
        the m/z value of the peptide precursor is used to find the corresponding
        ion m/z value in the preceeding MS1 scan. By adding positive and
        negative integers to the ``scanRange`` parameter the subsequent and
        preceeding MS1 scans are also used to aquire calibration data from the
        same peptide precursor. By default "[-1, 0, 1]".
    :param massTolerance: float or int
        The maximal allowed deviation for matching two masses. By default
        "10 * 1e-6" which corresponds to 10ppm.
    :param toleranceMode: "relative" or "absolute"
        Specifies how the ``massTolerance`` value is applied, by default
        "relative
    :param topIntMatches: bool, by default False
    :param useTopIntIons: bool,  by default False

    :returns: {'siId': numpy.array([str, ...]),
               'rt': numpy.array([float, ...]),
               'obsMz': numpy.array([float, ...]),
               'excMz': numpy.array([float, ...]),
               'relDev': numpy.array([float, ...]),
               'absDev': numpy.array([float, ...]),
               'int': numpy.array([float, ...])
               'source': numpy.array(['psm' or 'lock'])
               }
    """
    scanRange = kwargs.get('scanRange', [-1, 0, 1])
    massTolerance = kwargs.get('massTolerance', 10*1e-6)
    toleranceMode = kwargs.get('toleranceMode', 'relative')
    topIntMatches = kwargs.get('topIntMatches', False)
    useTopIntIons = kwargs.get('useTopIntIons', False)

    ms1Arrays = msrunContainer.getArrays(['rt'], specfiles=specfile, sort='rt',
                                         selector=lambda si: si.msLevel==1
                                         )
    ms1Arrays['calibrationMz'] = [list() for x in range(len(ms1Arrays['id']))]

    if lockMass is not None:
        if lockMass == True: #Use default lock mass values
            lockMass = [445.12002, 519.13882, 593.15761, 667.1764,
                        536.16536, 610.18416, 684.20295]
        lockMassTuples = [(lockMassMz, lockMassMz, 'lock') for lockMassMz in lockMass]
        for ms1ArrayPos in range(len(ms1Arrays['id'])):
            ms1Arrays['calibrationMz'][ms1ArrayPos].extend(lockMassTuples)

    if siiArrays is not None:
        precursorArrayLookup = {__: _ for _, __ in enumerate(ms1Arrays['id'])}
        for obsMz, excMz, precursorId in zip(siiArrays['obsMz'],
                                             siiArrays['excMz'],
                                             siiArrays['precursorId']
                                             ):
            #find array position of the precursor scan
            precursorArrayPos = precursorArrayLookup[precursorId]
            #also look for the peptide precursor ions in earlier and later MS1
            #scans by modifying the precursorArrayPos according to the numbers
            #in "scanRange"
            for posModifier in scanRange:
                _arrayPos = precursorArrayPos + posModifier
                try:
                    ms1Arrays['calibrationMz'][_arrayPos].append((obsMz, excMz, 'psm'))
                except IndexError:
                    #An IndexError can occur because of the "scanRange"
                    #extension at the end and the beginning of the MS1 scans
                    pass

    calibrationDataMs1 = {_: [] for _ in ['siId', 'rt', 'obsMz', 'excMz', 'iit',
                                          'source', 'relDev', 'absDev', 'int'
                                          ]
                          }
    for siId, rtMs1, calibrationMz in zip(ms1Arrays['id'],
                                          ms1Arrays['rt'],
                                          ms1Arrays['calibrationMz']
                                          ):
        if len(calibrationMz) == 0:
            continue

        ionMzListMs1 = msrunContainer.saic[specfile][siId].arrays['mz']
        ionIntListMs1 = msrunContainer.saic[specfile][siId].arrays['i']

        if useTopIntIons:
            ionIntMask = ionIntListMs1.argsort()[::-1][:useTopIntIons]
            ionMzListMs1 = ionMzListMs1[ionIntMask]
            ionIntListMs1 = ionIntListMs1[ionIntMask]

        currCalibrationData = ddict(list)
        for obsMz, excMz, source in calibrationMz:
            limHigh = obsMz * (1+massTolerance)
            limLow = obsMz * (1-massTolerance)
            pL = bisect.bisect_left(ionMzListMs1, limLow)
            pH = bisect.bisect_right(ionMzListMs1, limHigh)

            if pH - pL <= 0:
                continue
            ionMatchMask = abs(ionMzListMs1[pL:pH] - obsMz).argmin()
            ms1Mz = ionMzListMs1[pL:pH][ionMatchMask]
            ms1Int = ionIntListMs1[pL:pH][ionMatchMask]

            #TODO: rel dev calculation changed, test dataFit functions!!!
            relDevObs = (1 - ms1Mz / obsMz)
            absDevObs = obsMz - ms1Mz
            relDevExc = (1 - ms1Mz / excMz)
            absDevExc = excMz - ms1Mz
            if abs(relDevObs) <= massTolerance:
                currCalibrationData['siId'].append(siId)
                currCalibrationData['rt'].append(rtMs1)
                currCalibrationData['obsMz'].append(ms1Mz)
                currCalibrationData['excMz'].append(excMz)
                currCalibrationData['relDev'].append(relDevExc)
                currCalibrationData['absDev'].append(absDevExc)
                currCalibrationData['int'].append(ms1Int)
                currCalibrationData['iit'].append(msrunContainer.sic[specfile][siId].iit)
                currCalibrationData['source'].append(source)
        if len(currCalibrationData['siId']) == 0:
            continue
        for key in currCalibrationData.keys():
            calibrationDataMs1[key].extend(currCalibrationData[key])

    # Convert calibration data into numpy arrays
    for key in calibrationDataMs1.keys():
        calibrationDataMs1[key] = numpy.array(calibrationDataMs1[key])

    return calibrationDataMs1