2727import java .util .List ;
2828import java .util .Map ;
2929import java .util .Optional ;
30- import java .util .Set ;
3130import java .util .TreeMap ;
31+ import java .util .function .Function ;
3232
33- import org .biojava .nbio .core .util .SingleLinkageClusterer ;
3433import org .biojava .nbio .structure .Atom ;
34+ import org .biojava .nbio .structure .Chain ;
35+ import org .biojava .nbio .structure .EntityInfo ;
3536import org .biojava .nbio .structure .Structure ;
3637import org .biojava .nbio .structure .asa .AsaCalculator ;
3738import org .biojava .nbio .structure .xtal .CrystalBuilder ;
@@ -368,6 +369,10 @@ public List<StructureInterfaceCluster> getClusters() {
368369 * using Jaccard contact set scores to measure the similarity of interfaces.
369370 * Subsequent calls will use the cached value without recomputing the clusters.
370371 * The clusters will be assigned ids by sorting descending by {@link StructureInterfaceCluster#getTotalArea()}
372+ * <p>
373+ * Interfaces are compared by entity pairs (see {@link #ENTITY_ID_PAIR}) and grouped with the leader
374+ * algorithm described in {@link #clusterInterfaces(List, Function, double)}, thus the result depends on
375+ * the order of the list (by default descending by area, see {@link #sort()}).
371376 * @param contactOverlapScoreClusterCutoff the contact overlap score above which a pair will be
372377 * clustered
373378 * @return
@@ -377,77 +382,125 @@ public List<StructureInterfaceCluster> getClusters(double contactOverlapScoreClu
377382 return clusters ;
378383 }
379384
380- clusters = new ArrayList <>();
381-
382- // nothing to do if we have no interfaces
383- if (list .isEmpty ()) return clusters ;
384-
385- logger .debug ("Calculating all-vs-all Jaccard scores for {} interfaces" , list .size ());
386- double [][] matrix = new double [list .size ()][list .size ()];
385+ List <StructureInterfaceCluster > singletons = new ArrayList <>(list .size ());
386+ for (StructureInterface interf : list ) {
387+ StructureInterfaceCluster cluster = new StructureInterfaceCluster ();
388+ cluster .addMember (interf );
389+ singletons .add (cluster );
390+ }
387391
388- for (int i =0 ;i <list .size ();i ++) {
389- for (int j =i +1 ;j <list .size ();j ++) {
390- StructureInterface iInterf = list .get (i );
391- StructureInterface jInterf = list .get (j );
392+ clusters = clusterInterfaces (singletons , ENTITY_ID_PAIR , contactOverlapScoreClusterCutoff );
392393
393- double scoreDirect = iInterf . getContactOverlapScore ( jInterf , false );
394- double scoreInvert = iInterf . getContactOverlapScore ( jInterf , true );
394+ // now we sort by areas (descending) and assign ids based on that sorting
395+ clusters . sort (( o1 , o2 ) -> Double . compare ( o2 . getTotalArea (), o1 . getTotalArea ())); //note we invert so that sorting is descending
395396
396- double maxScore = Math .max (scoreDirect , scoreInvert );
397+ int id = 1 ;
398+ for (StructureInterfaceCluster cluster :clusters ) {
399+ cluster .setId (id );
400+ id ++;
401+ }
397402
398- matrix [ i ][ j ] = maxScore ;
399- }
403+ return clusters ;
404+ }
400405
406+ /**
407+ * Identifies each side of an interface by the entity id ({@link EntityInfo#getMolId()}) of its parent chain.
408+ * Returns null if any of the parent chains or entities can't be found.
409+ * @see #clusterInterfaces(List, Function, double)
410+ */
411+ public static final Function <StructureInterface , Pair <Integer >> ENTITY_ID_PAIR = interf -> {
412+ Pair <Chain > chains = interf .getParentChains ();
413+ if (chains == null || chains .getFirst ().getEntityInfo () == null || chains .getSecond ().getEntityInfo () == null ) {
414+ return null ;
401415 }
416+ return new Pair <>(chains .getFirst ().getEntityInfo ().getMolId (), chains .getSecond ().getEntityInfo ().getMolId ());
417+ };
402418
403- logger .debug ("Will now cluster {} interfaces based on full all-vs-all Jaccard scores matrix" , list .size ());
404- SingleLinkageClusterer slc = new SingleLinkageClusterer (matrix , true );
419+ /**
420+ * Groups interface clusters further, merging any two whose representatives (first members) have a
421+ * contact overlap score above the given cutoff.
422+ * <p>
423+ * This is the leader algorithm: the representative of the first cluster is compared to the representatives of all
424+ * subsequent clusters, merging those above cutoff into it; then the same is repeated for the next remaining cluster.
425+ * It performs at most n(n-1)/2 score calculations, and about n*k for n input and k output clusters.
426+ * Unlike single linkage clustering, similarity is not transitive through non-representative members
427+ * and the result depends on the input order: placing larger interfaces first makes them the representatives.
428+ * <p>
429+ * Two interfaces are only compared if their identifier pairs match, in the same or inverted order.
430+ * The score is then {@link StructureInterface#getContactOverlapScore(StructureInterface, boolean)} non-inverted
431+ * or inverted respectively, or the maximum of both if all 4 identifiers are equal.
432+ * <p>
433+ * The input clusters are not modified. The output clusters are new objects whose members are the
434+ * representative's cluster members followed by the members of the clusters merged into it, and whose average
435+ * score is the average of the scores that caused merges (1.0 if no merges). The cluster back-references of all
436+ * members are set to the output clusters. Ids are not assigned.
437+ * @param initialClusters the starting clusters, e.g. one singleton cluster per interface. The first member of
438+ * each is its representative
439+ * @param idPairFunction gives the pair of identifiers for the 2 sides of an interface, e.g. {@link #ENTITY_ID_PAIR}.
440+ * If it returns null the interface is not merged with any other
441+ * @param contactOverlapScoreClusterCutoff the contact overlap score above which a pair of clusters is merged
442+ * @return the merged clusters, in the order of their representatives in the input
443+ * @since 7.3.0
444+ */
445+ public static <T > List <StructureInterfaceCluster > clusterInterfaces (List <StructureInterfaceCluster > initialClusters ,
446+ Function <StructureInterface , Pair <T >> idPairFunction ,
447+ double contactOverlapScoreClusterCutoff ) {
448+ int n = initialClusters .size ();
449+ logger .debug ("Clustering {} interface clusters by contact overlap score" , n );
450+
451+ List <StructureInterface > reps = new ArrayList <>(n );
452+ List <Pair <T >> idPairs = new ArrayList <>(n );
453+ for (StructureInterfaceCluster cluster : initialClusters ) {
454+ StructureInterface rep = cluster .getMembers ().get (0 );
455+ reps .add (rep );
456+ idPairs .add (idPairFunction .apply (rep ));
457+ }
405458
406- Map <Integer , Set <Integer >> clusteredIndices = slc .getClusters (contactOverlapScoreClusterCutoff );
407- for (int clusterIdx :clusteredIndices .keySet ()) {
408- List <StructureInterface > members = new ArrayList <>();
409- for (int idx :clusteredIndices .get (clusterIdx )) {
410- members .add (list .get (idx ));
411- }
459+ List <StructureInterfaceCluster > result = new ArrayList <>();
460+ boolean [] merged = new boolean [n ];
461+ for (int i = 0 ; i < n ; i ++) {
462+ if (merged [i ]) continue ;
412463 StructureInterfaceCluster cluster = new StructureInterfaceCluster ();
413- cluster .setMembers (members );
414- double averageScore = 0.0 ;
415- int countPairs = 0 ;
416- for (int i =0 ;i <members .size ();i ++) {
417- int iIdx = list .indexOf (members .get (i ));
418- for (int j =i +1 ;j <members .size ();j ++) {
419- averageScore += matrix [iIdx ][list .indexOf (members .get (j ))];
420- countPairs ++;
464+ cluster .getMembers ().addAll (initialClusters .get (i ).getMembers ());
465+ double sumScores = 0 ;
466+ int countMerges = 0 ;
467+ // descending, so that merged members are appended in the same order as in previous implementations
468+ for (int j = n - 1 ; j > i ; j --) {
469+ if (merged [j ]) continue ;
470+ double score = getContactOverlapScore (reps .get (i ), idPairs .get (i ), reps .get (j ), idPairs .get (j ));
471+ if (score > contactOverlapScoreClusterCutoff ) {
472+ cluster .getMembers ().addAll (initialClusters .get (j ).getMembers ());
473+ merged [j ] = true ;
474+ sumScores += score ;
475+ countMerges ++;
421476 }
422477 }
423- if (countPairs >0 ) {
424- averageScore = averageScore /countPairs ;
425- } else {
426- // if only one interface in cluster we set the score to the maximum
427- averageScore = 1.0 ;
478+ cluster .setAverageScore (countMerges > 0 ? sumScores / countMerges : 1.0 );
479+ for (StructureInterface member : cluster .getMembers ()) {
480+ member .setCluster (cluster );
428481 }
429- cluster .setAverageScore (averageScore );
430- clusters .add (cluster );
482+ result .add (cluster );
431483 }
432484
433- // finally we have to set the back-references in each StructureInterface
434- for (StructureInterfaceCluster cluster :clusters ) {
435- for (StructureInterface interf :cluster .getMembers ()) {
436- interf .setCluster (cluster );
437- }
438- }
439- logger .debug ("Done clustering {} interfaces based on full all-vs-all Jaccard scores matrix. Found a total of {} clusters" , list .size (), clusters .size ());
440-
441- // now we sort by areas (descending) and assign ids based on that sorting
442- clusters .sort ((o1 , o2 ) -> Double .compare (o2 .getTotalArea (), o1 .getTotalArea ())); //note we invert so that sorting is descending
485+ logger .debug ("Found {} clusters from {} input clusters at contact overlap score cutoff {}" , result .size (), n , contactOverlapScoreClusterCutoff );
486+ return result ;
487+ }
443488
444- int id = 1 ;
445- for (StructureInterfaceCluster cluster :clusters ) {
446- cluster .setId (id );
447- id ++;
489+ private static <T > double getContactOverlapScore (StructureInterface iInterf , Pair <T > iIds , StructureInterface jInterf , Pair <T > jIds ) {
490+ if (iIds == null || jIds == null ) {
491+ return 0 ;
448492 }
449-
450- return clusters ;
493+ boolean direct = iIds .getFirst ().equals (jIds .getFirst ()) && iIds .getSecond ().equals (jIds .getSecond ());
494+ boolean inverted = iIds .getFirst ().equals (jIds .getSecond ()) && iIds .getSecond ().equals (jIds .getFirst ());
495+ if (direct && inverted ) {
496+ // all 4 ids are the same: the order of the sides is not known, so we try both
497+ return Math .max (iInterf .getContactOverlapScore (jInterf , false ), iInterf .getContactOverlapScore (jInterf , true ));
498+ } else if (direct ) {
499+ return iInterf .getContactOverlapScore (jInterf , false );
500+ } else if (inverted ) {
501+ return iInterf .getContactOverlapScore (jInterf , true );
502+ }
503+ return 0 ;
451504 }
452505
453506 @ Override
0 commit comments