docs: def run_experiment(methods, data, n_classes, true_labels, n_runs=10, use_purity=True, use_nmi=False, use_ari=False, use_nne=False, consensus=False):
      """
      runs a pre-processing + clustering experiment...
  
      exactly one of use_purity, use_nmi, or use_ari can be true
  
      Args:
          methods: list of 2-tuples. The first element is either a single Preprocess object or a list of Preprocess objects, to be applied in sequence to the data. The second element is either a single Cluster object, a list of Cluster objects, or a list of lists, where each list is a sequence of Preprocess objects with the final element being a Cluster object.
          data: genes x cells array
          true_labels: 1d array of length cells
          consensus: if true, runs a consensus on cluster results for each method at the very end.
          use_purity, use_nmi, use_ari, use_nne: which error metric to use (at most one can be True)
  
      Returns:
          purities (list of lists)
          names (list of lists)
          other (dict): keys: timing, preprocessing, clusterings
      """
      results = []
      names = []
      clusterings = {}
      other_results = {}
      other_results['timing'] = {}
      other_results['preprocessing'] = {}
      if use_purity:
          purity_method = purity
      elif use_nmi:
          purity_method = nmi
      elif use_ari:
          purity_method = ari
      elif use_nne:
          purity_method = nne
      for i in range(n_runs):
          print('run {0}'.format(i))
          purities = []
          r = 0
          method_index = 0
          for preproc, cluster in methods:
              t0 = time.time()
              if isinstance(preproc, Preprocess):
                  preprocessed, ll = preproc.run(data)
                  output_names = preproc.output_names
              else:
                  # if the input is a list, only use the first preproc result
                  p1 = data
                  output_names = ['']
                  for p in preproc:
                      p1, ll = p.run(p1)
                      p1 = p1[0]
                      if output_names[0] != '':
                          output_names[0] = output_names[0] + '_' + p.output_names[0]
                      else:
                          output_names[0] = p.output_names[0]
                  preprocessed = [p1]
              t1 = time.time() - t0
              for name, pre in zip(output_names, preprocessed):
                  starting_index = method_index
                  if isinstance(cluster, Cluster):
                      #try:
                          t0 = time.time()
                          labels = cluster.run(pre)
                          t2 = t1 + time.time() - t0
                          if use_nne:
                              purities.append(purity_method(pre, true_labels))
                          else:
                              purities.append(purity_method(labels, true_labels))
                          if i==0:
                              names.append(name + '_' + cluster.name)
                              clusterings[names[-1]] = []
                              other_results['timing'][names[-1]] = []
                          print(names[r])
                          clusterings[names[r]].append(labels)
                          print('time: ' + str(t2))
                          other_results['timing'][names[r]].append(t2)
                          print(purities[-1])
                          r += 1
                          method_index += 1
                      #except:
                      #    print('failed to do clustering')
                  elif type(cluster) == list:
                      for c in cluster:
                          if isinstance(c, list):
                              t2 = t1
                              name2 = name
                              sub_data = pre.copy()
                              for subproc in c[:-1]:
                                  t0 = time.time()
                                  subproc_out, ll = subproc.run(sub_data)
                                  sub_data = subproc_out[0]
                                  name2 = name2 + '_' + subproc.output_names[0]
                                  t2 += time.time() - t0
                              t0 = time.time()
                              labels = c[-1].run(sub_data)
                              t2 += time.time() - t0
                              if use_nne:
                                  purities.append(purity_method(sub_data, true_labels))
                              else:
                                  purities.append(purity_method(labels, true_labels))
                              if i==0:
                                  names.append(name2 + '_' + c[-1].name)
                                  clusterings[names[-1]] = []
                                  other_results['timing'][names[-1]] = []
                              print(names[r])
                              clusterings[names[r]].append(labels)
                              other_results['timing'][names[r]].append(t2)
                              print('time: ' + str(t2))
                              print(purities[-1])
                              r += 1
                              method_index += 1
                          else:
                              try:
                                  t0 = time.time()
                                  labels = c.run(pre)
                                  t2 = t1 + time.time() - t0
                                  if i==0:
                                      names.append(name + '_' + c.name)
                                      clusterings[names[-1]] = []
                                      other_results['timing'][names[-1]] = []
                                  if use_nne:
                                      purities.append(purity_method(pre, true_labels))
                                  else:
                                      purities.append(purity_method(labels, true_labels))
                                  print(names[r])
                                  clusterings[names[r]].append(labels)
                                  other_results['timing'][names[r]].append(t2)
                                  print('time: ' + str(t2))
                                  print(purities[-1])
                                  r += 1
                                  method_index += 1
                              except:
                                  print('failed to do clustering')
                  # find the highest purity for the pre-processing method
                  # save the preprocessing result with the highest NMI
                  num_clustering_results = method_index - starting_index
                  clustering_results = purities[-num_clustering_results:]
                  if i > 0 and len(clustering_results) > 0:
                      old_clustering_results = results[-1][starting_index:method_index]
                      if max(old_clustering_results) < max(clustering_results):
                          other_results['preprocessing'][name] = pre
                  else:
                      other_results['preprocessing'][name] = pre
          print('\t'.join(names))
          print('purities: ' + '\t'.join(map(str, purities)))
          results.append(purities)
      consensus_purities = []
      if consensus:
          other_results['consensus'] = {}
          k = len(np.unique(true_labels))
          for name, clusts in clusterings.items():
              print(name)
              clusts = np.vstack(clusts)
              consensus_clust = CE.cluster_ensembles(clusts, verbose=False, N_clusters_max=k)
              other_results['consensus'][name] = consensus_clust
              if use_purity:
                  consensus_purity = purity(consensus_clust.flatten(), true_labels)
                  print('consensus purity: ' + str(consensus_purity))
                  consensus_purities.append(consensus_purity)
              if use_nmi:
                  consensus_nmi = nmi(true_labels, consensus_clust)
                  print('consensus NMI: ' + str(consensus_nmi))
                  consensus_purities.append(consensus_nmi)
              if use_ari:
                  consensus_ari = ari(true_labels, consensus_clust)
                  print('consensus ARI: ' + str(consensus_ari))
                  consensus_purities.append(consensus_ari)
          print('consensus results: ' + '\t'.join(map(str, consensus_purities)))
      other_results['clusterings'] = clusterings
      return results, names, other_results