docs: public Clustering<MedoidModel> run(Database db, Relation<O> relation) {
      ArrayDBIDs ids = DBIDUtil.ensureArray(relation.getDBIDs());
      final int size = ids.size();
  
      int[] assignment = new int[size];
      double[][] s = initialization.getSimilarityMatrix(db, relation, ids);
      double[][] r = new double[size][size];
      double[][] a = new double[size][size];
  
      IndefiniteProgress prog = LOG.isVerbose() ? new IndefiniteProgress("Affinity Propagation Iteration", LOG) : null;
      MutableProgress aprog = LOG.isVerbose() ? new MutableProgress("Stable assignments", size + 1, LOG) : null;
  
      int inactive = 0;
      for(int iteration = 0; iteration < maxiter && inactive < convergence; iteration++) {
        // Update responsibility matrix:
        for(int i = 0; i < size; i++) {
          double[] ai = a[i], ri = r[i], si = s[i];
          // Find the two largest values (as initially maxk == i)
          double max1 = Double.NEGATIVE_INFINITY, max2 = Double.NEGATIVE_INFINITY;
          int maxk = -1;
          for(int k = 0; k < size; k++) {
            double val = ai[k] + si[k];
            if(val > max1) {
              max2 = max1;
              max1 = val;
              maxk = k;
            }
            else if(val > max2) {
              max2 = val;
            }
          }
          // With the maximum value known, update r:
          for(int k = 0; k < size; k++) {
            double val = si[k] - ((k != maxk) ? max1 : max2);
            ri[k] = ri[k] * lambda + val * (1. - lambda);
          }
        }
        // Update availability matrix
        for(int k = 0; k < size; k++) {
          // Compute sum of max(0, r_ik) for all i.
          // For r_kk, don't apply the max.
          double colposum = 0.;
          for(int i = 0; i < size; i++) {
            if(i == k || r[i][k] > 0.) {
              colposum += r[i][k];
            }
          }
          for(int i = 0; i < size; i++) {
            double val = colposum;
            // Adjust column sum by the one extra term.
            if(i == k || r[i][k] > 0.) {
              val -= r[i][k];
            }
            if(i != k && val > 0.) { // min
              val = 0.;
            }
            a[i][k] = a[i][k] * lambda + val * (1 - lambda);
          }
        }
        int changed = 0;
        for(int i = 0; i < size; i++) {
          double[] ai = a[i], ri = r[i];
          double max = Double.NEGATIVE_INFINITY;
          int maxj = -1;
          for(int j = 0; j < size; j++) {
            double v = ai[j] + ri[j];
            if(v > max || (i == j && v >= max)) {
              max = v;
              maxj = j;
            }
          }
          if(assignment[i] != maxj) {
            changed += 1;
            assignment[i] = maxj;
          }
        }
        inactive = (changed > 0) ? 0 : (inactive + 1);
        LOG.incrementProcessed(prog);
        if(aprog != null) {
          aprog.setProcessed(size - changed, LOG);
        }
      }
      if(aprog != null) {
        aprog.setProcessed(aprog.getTotal(), LOG);
      }
      LOG.setCompleted(prog);
      // Cluster map, by lead object
      Int2ObjectOpenHashMap<ModifiableDBIDs> map = new Int2ObjectOpenHashMap<>();
      DBIDArrayIter i1 = ids.iter();
      for(int i = 0; i1.valid(); i1.advance(), i++) {
        int c = assignment[i];
        // Add to cluster members:
        ModifiableDBIDs cids = map.get(c);
        if(cids == null) {
          cids = DBIDUtil.newArray();
          map.put(c, cids);
        }
        cids.add(i1);
      }
      // If we stopped early, the cluster lead might be in a different cluster.
      for(ObjectIterator<Int2ObjectOpenHashMap.Entry<ModifiableDBIDs>> iter = map.int2ObjectEntrySet().fastIterator(); iter.hasNext();) {
        Int2ObjectOpenHashMap.Entry<ModifiableDBIDs> entry = iter.next();
        final int key = entry.getIntKey();
        int targetkey = key;
        ModifiableDBIDs tids = null;
        // Chase arrows:
        while(tids == null && assignment[targetkey] != targetkey) {
          targetkey = assignment[targetkey];
          tids = map.get(targetkey);
        }
        if(tids != null && targetkey != key) {
          tids.addDBIDs(entry.getValue());
          iter.remove();
        }
      }
  
      Clustering<MedoidModel> clustering = new Clustering<>("Affinity Propagation Clustering", "ap-clustering");
      ModifiableDBIDs noise = DBIDUtil.newArray();
      for(ObjectIterator<Int2ObjectOpenHashMap.Entry<ModifiableDBIDs>> iter = map.int2ObjectEntrySet().fastIterator(); iter.hasNext();) {
        Int2ObjectOpenHashMap.Entry<ModifiableDBIDs> entry = iter.next();
        i1.seek(entry.getIntKey());
        if(entry.getValue().size() > 1) {
          MedoidModel mod = new MedoidModel(DBIDUtil.deref(i1));
          clustering.addToplevelCluster(new Cluster<>(entry.getValue(), mod));
        }
        else {
          noise.add(i1);
        }
      }
      if(noise.size() > 0) {
        MedoidModel mod = new MedoidModel(DBIDUtil.deref(noise.iter()));
        clustering.addToplevelCluster(new Cluster<>(noise, true, mod));
      }
      return clustering;
    }