docs: def _pathway_feature_permutation(pathway_feature_tuples,
                                   permutation_max_iters):
      """Permute the pathways across features for one side in the
      network. Used in `permute_pathways_across_features`
  
      Parameters
      -----------
      pathway_feature_tuples : list(tup(str, int))
        a tuple list [(pathway, feature)] where the pathway, feature pairing
        indicates that a pathway was overrepresented in that feature
      permutation_max_iters : int
        specify the maximum number of iterations, limit the number of attempts
        we have to generate a permutation
  
      Returns
      -----------
      list(tup(str, int)), the list of pathway, feature pairings after the
        permutation
      """
  
      pathways, features = [list(elements_at_position)
                            for elements_at_position in
                            zip(*pathway_feature_tuples)]
  
      original_pathways = pathways[:]
  
      random.shuffle(pathways)
      feature_block_locations = {}
      i = 0
      while i < len(pathways):
          starting_index = i
          current_feature = features[i]
          pathway_set = set()
          # input is grouped by feature, so we want to keep track of the start
          # and end of a given "block" of the same feature--this corresponds
          # to all the pathways overrepresented in that feature.
          while i < len(pathways) and features[i] == current_feature:
              # check the results of the permutation. if `pathway_set` does
              # not contain the current pathway, we are maintaining the
              # necessary invariants in our permutation thus far.
              if pathways[i] not in pathway_set:
                  pathway_set.add(pathways[i])
              else:
                  k = 0
                  random_pathway = None
                  while True:
                      # select another random pathway from the list
                      # and get the feature to which it belongs
                      j = random.choice(range(0, len(pathways)))
                      random_pathway = pathways[j]
                      random_feature = features[j]
                      if (random_pathway != pathways[i] and
                              random_pathway not in pathway_set):
                          # if this is a feature we have not already seen,
                          # we are done.
                          if random_feature not in feature_block_locations:
                              break
                          # otherwise, look at the indices that correspond
                          # to that feature's block of pathways
                          feature_block_start, feature_block_end = \
                              feature_block_locations[random_feature]
                          pathway_block = pathways[feature_block_start:
                                                   feature_block_end]
                          # make sure that the current pathway is not in
                          # that block--ensures that we maintain the invariant
                          # after the swap
                          if pathways[i] not in pathway_block:
                              break
                      k += 1
                      if k > permutation_max_iters:
                          print("Permutation step: reached the maximum "
                                "number of iterations {0}.".format(
                                  permutation_max_iters))
                          return None
                  pathway_set.add(random_pathway)
                  pathways[j] = pathways[i]
                  pathways[i] = random_pathway
              i += 1
          ending_index = i
          feature_block_locations[current_feature] = (
              starting_index, ending_index)
  
      if original_pathways == pathways:
          return None
      return list(zip(pathways, features))