docs: def lfqFeatureGrouping(fiContainer, timeLimit=40, massLimit=10*1e-6,
                         eucLimit=None, timeKey='rt', massKey='mz',
                         massScalingFactor=None, categoryKey='specfile',
                         charges=None, matchArraySelector=None, specfiles=None):
      """ #TODO: docstring
  
      :param fiContainer: #TODO: docstring
      :param timeLimit: #TODO: docstring
      :param massLimit: #TODO: docstring
      :param eucLimit: #TODO: docstring
      :param timeKey: #TODO: docstring
      :param massKey: #TODO: docstring
      :param massScalingFactor: #TODO: docstring
      :param categoryKey: #TODO: docstring
      :param charges: #TODO: docstring
      :param matchArraySelector: #TODO: docstring
      :param specfiles: limit grouping to these specfiles
  
      :returns: #TODO docstring, :class:`FgiContainer`
      """
      # --- perform the whole feature grouping process --- #
      targetChargeStates = range(1, 6) if charges is None else charges
      if matchArraySelector is None:
          matchArraySelector = lambda arr: numpy.any(arr['isAnnotated'])
      if massScalingFactor is None:
          massScalingFactor = timeLimit / massLimit
      if eucLimit is None:
          eucLimit = timeLimit
      if specfiles is None:
          specfiles = sorted(viewkeys(fiContainer.info))
  
      #'massToleranceMode': 'relative'
      #'timeToleranceMode': 'absolute'
      fgiContainer = FgiContainer(specfiles)
  
      logMassLimit = log2RelativeMassLimit(massLimit)
      logMassKey = 'logMass'
  
      logToleranceFactor = massLimit / log2RelativeMassLimit(massLimit)
      logMassScalingFactor = massScalingFactor * logToleranceFactor
      """ Note: because "a" is similar to "b"
      a = (1- 400 / 400.001) * massScalingFactor
      b = (numpy.log2(400.001) - numpy.log2(400)) * logMassScalingFactor
      """
  
      fiArrayKeys = [massKey, timeKey, 'isAnnotated', 'isMatched']
  
      for _charge in targetChargeStates:
          # - Prepare feature arrays - #
          fiSelector = lambda fi: fi.charge == _charge and fi.isValid
          fiArrays = fiContainer.getArrays(fiArrayKeys, specfiles, sort=massKey,
                                           selector=fiSelector)
          fiArrays['logMass'] = numpy.log2(fiArrays[massKey])
          if listvalues(fiArrays)[0].size == 0:
              continue
  
          # - group features which are in close mass and time proximity - #
          continuousGroups = massTimeContinuityGroups(fiArrays, logMassKey,
                                                      timeKey, logMassLimit,
                                                      timeLimit
                                                      )
  
          # - perform proximity grouping - #
          matchArrayKeys = list(viewkeys(fiArrays))
  
          for groupId in range(len(continuousGroups)):
              #Grab the arrays of the current feature continuity group
              groupPositions = continuousGroups[groupId]
              matchArr = getContGroupArrays(fiArrays, groupPositions,
                                            matchArrayKeys
                                            )
              if not matchArraySelector(matchArr):
                  continue
  
              #Calculate a sorted list of all euclidean feature distances
              matchArr['mNorm'] = matchArr[logMassKey] * logMassScalingFactor
              distInfo = calcDistMatchArr(matchArr, timeKey, 'mNorm')
  
              #Group fi according to their proximity
              linkageGroups = proximityGrouping(matchArr, distInfo, eucLimit,
                                                categoryKey
                                                )
  
              #Generate feature groups from the linked features
              fgiIds = generateFeatureGroups(fgiContainer, linkageGroups,
                                             matchArr, timeKey, massKey,
                                             logMassKey, logMassScalingFactor
                                             )
  
              #Set charge manually
              for fgiId in fgiIds:
                  fgiContainer.container[fgiId].charge = _charge
  
              #Mark overlapping groups as not valid (fgi.isValid = False)
              fgiDoOverlap = findFgiOverlaps(fgiContainer, fgiIds)
  
          #Add feature intensities to the feature groups
          fgiContainer.updateIntensities(fiContainer)
      return fgiContainer