docs: public double evaluateClustering(Database db, Relation<? extends O> rel, DistanceQuery<O> dq, Clustering<?> c) {
      List<? extends Cluster<?>> clusters = c.getAllClusters();
  
      // Count ignored noise, and within-cluster distances
      int ignorednoise = 0, w = 0;
      for(Cluster<?> cluster : clusters) {
        if(cluster.size() <= 1 || cluster.isNoise()) {
          switch(noiseOption){
          case IGNORE_NOISE:
            ignorednoise += cluster.size();
            continue; // Ignore
          case TREAT_NOISE_AS_SINGLETONS:
            continue; // No within-cluster distances!
          case MERGE_NOISE:
            break; // Treat like a cluster
          default:
            LOG.warning("Unknown noise handling option: " + noiseOption);
          }
        }
        w += (cluster.size() * (cluster.size() - 1)) >>> 1;
      }
  
      // TODO: for small k=2, and balanced clusters, it may be more efficient to
      // just build a long array with all distances, and select the quantiles.
      // The heaps used below pay off in memory consumption for k > 2
  
      // Yes, maxDists is supposed to be a min heap, and the other way.
      // Because we want to replace the smallest of the current k-largest
      // distances.
      DoubleHeap maxDists = new DoubleMinHeap(w);
      DoubleHeap minDists = new DoubleMaxHeap(w);
      double theta = 0.; // Sum of within-cluster distances
  
      FiniteProgress prog = LOG.isVerbose() ? new FiniteProgress("Processing clusters for C-Index", clusters.size(), LOG) : null;
      for(int i = 0; i < clusters.size(); i++) {
        Cluster<?> cluster = clusters.get(i);
        if(cluster.size() <= 1 || cluster.isNoise()) {
          switch(noiseOption){
          case IGNORE_NOISE:
            LOG.incrementProcessed(prog);
            continue; // Ignore
          case TREAT_NOISE_AS_SINGLETONS:
            processSingleton(cluster, rel, dq, maxDists, minDists, w);
            LOG.incrementProcessed(prog);
            continue;
          case MERGE_NOISE:
            break; // Treat like a cluster, below
          }
        }
        theta += processCluster(cluster, clusters, i, dq, maxDists, minDists, w);
        LOG.incrementProcessed(prog);
      }
      LOG.ensureCompleted(prog);
  
      // Simulate best and worst cases:
      double min = 0, max = 0; // Sum of largest and smallest
      assert (minDists.size() == w);
      assert (maxDists.size() == w);
      for(DoubleHeap.UnsortedIter it = minDists.unsortedIter(); it.valid(); it.advance()) {
        min += it.get();
      }
      for(DoubleHeap.UnsortedIter it = maxDists.unsortedIter(); it.valid(); it.advance()) {
        max += it.get();
      }
      assert (max >= min);
  
      double cIndex = (max > min) ? (theta - min) / (max - min) : 1.;
  
      if(LOG.isStatistics()) {
        LOG.statistics(new StringStatistic(key + ".c-index.noise-handling", noiseOption.toString()));
        if(ignorednoise > 0) {
          LOG.statistics(new LongStatistic(key + ".c-index.ignored", ignorednoise));
        }
        LOG.statistics(new DoubleStatistic(key + ".c-index", cIndex));
      }
  
      EvaluationResult ev = EvaluationResult.findOrCreate(db.getHierarchy(), c, "Internal Clustering Evaluation", "internal evaluation");
      MeasurementGroup g = ev.findOrCreateGroup("Distance-based Evaluation");
      g.addMeasure("C-Index", cIndex, 0., 1., 0., true);
      db.getHierarchy().resultChanged(ev);
      return cIndex;
    }