001/*
002 *                    BioJava development code
003 *
004 * This code may be freely distributed and modified under the
005 * terms of the GNU Lesser General Public Licence.  This should
006 * be distributed with the code.  If you do not have a copy,
007 * see:
008 *
009 *      http://www.gnu.org/copyleft/lesser.html
010 *
011 * Copyright for this code is held jointly by the individual
012 * authors.  These should be listed in @author doc comments.
013 *
014 * For more information on the BioJava project and its aims,
015 * or to join the biojava-l mailing list, visit the home page
016 * at:
017 *
018 *      http://www.biojava.org/
019 *
020 * Created on 2013-05-23
021 *
022 */
023package org.biojava.nbio.structure.symmetry.core;
024
025import org.biojava.nbio.structure.Calc;
026import org.biojava.nbio.structure.Structure;
027import org.biojava.nbio.structure.cluster.*;
028import org.biojava.nbio.structure.contact.BoundingBox;
029import org.biojava.nbio.structure.contact.Grid;
030import org.jgrapht.graph.SimpleGraph;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034import org.jgrapht.alg.clique.CliqueMinimalSeparatorDecomposition;
035import org.jgrapht.graph.DefaultEdge;
036import org.jgrapht.graph.AsSubgraph;
037import org.jgrapht.Graph;
038
039import javax.vecmath.Point3d;
040import java.util.*;
041import java.util.stream.Collectors;
042
043/**
044 * Detects the symmetry (global, pseudo, internal and local) of protein
045 * structures.
046 * <p>
047 * The {@link SubunitClustererParameters} determine the subunit definition and
048 * clustering, while the {@link QuatSymmetryParameters} determine the calculated
049 * symmetry results (point group and axes).
050 *
051 * @author Peter Rose
052 * @author Aleix Lafita
053 *
054 */
055public class QuatSymmetryDetector {
056
057        private static final Logger logger = LoggerFactory
058                        .getLogger(QuatSymmetryDetector.class);
059
060
061        /**
062         * Maximal distance between Calpha atoms of residues of different subunits
063         * to establish a residue contact.
064         */
065        private static final double CONTACT_GRAPH_DISTANCE_CUTOFF = 8;
066        /**
067         * The minimal number of residue contacts between subunits to consider
068         * them connected and add an edge to the contact graph.
069         */
070        private static final int CONTACT_GRAPH_MIN_CONTACTS = 5;
071
072        /** Prevent instantiation **/
073        private QuatSymmetryDetector() {
074        }
075
076        /**
077         * Calculate GLOBAL symmetry results. This means that all {@link Subunit}
078         * are included in the symmetry.
079         *
080         * @param structure
081         *            protein chains will be extracted as {@link Subunit}
082         * @param symmParams
083         *            quaternary symmetry parameters
084         * @param clusterParams
085         *            subunit clustering parameters
086         * @return GLOBAL quaternary structure symmetry results
087         */
088        public static QuatSymmetryResults calcGlobalSymmetry(Structure structure,
089                        QuatSymmetryParameters symmParams,
090                        SubunitClustererParameters clusterParams) {
091                Stoichiometry composition = SubunitClusterer.cluster(structure, clusterParams);
092                return calcGlobalSymmetry(composition, symmParams);
093        }
094
095        /**
096         * Calculate GLOBAL symmetry results. This means that all {@link Subunit}
097         * are included in the symmetry.
098         *
099         * @param subunits
100         *            list of {@link Subunit}
101         * @param symmParams
102         *            quaternary symmetry parameters
103         * @param clusterParams
104         *            subunit clustering parameters
105         * @return GLOBAL quaternary structure symmetry results
106         */
107        public static QuatSymmetryResults calcGlobalSymmetry(
108                        List<Subunit> subunits, QuatSymmetryParameters symmParams,
109                        SubunitClustererParameters clusterParams) {
110                Stoichiometry composition = SubunitClusterer.cluster(subunits,
111                                clusterParams);
112                return calcGlobalSymmetry(composition, symmParams);
113        }
114
115        /**
116         * Calculate GLOBAL symmetry results. This means that all {@link Subunit}
117         * are included in the symmetry.
118         *
119         * @param composition
120         *            {@link Stoichiometry} object that contains clustering results
121         * @param symmParams
122         *            quaternary symmetry parameters
123         * @return GLOBAL quaternary structure symmetry results
124         */
125        public static QuatSymmetryResults calcGlobalSymmetry(
126                        Stoichiometry composition, QuatSymmetryParameters symmParams) {
127                return calcQuatSymmetry(composition, symmParams);
128        }
129
130
131        /**
132         * Returns a List of LOCAL symmetry results. This means that a subset of the
133         * {@link SubunitCluster} is left out of the symmetry calculation. Each
134         * element of the List is one possible LOCAL symmetry result.
135         * <p>
136         * Determine local symmetry if global structure is: (1) asymmetric, C1; (2)
137         * heteromeric (belongs to more than 1 subunit cluster); (3) more than 2
138         * subunits (heteromers with just 2 chains cannot have local symmetry)
139         *
140         * @param structure
141         *            protein chains will be extracted as {@link Subunit}
142         * @param symmParams
143         *            quaternary symmetry parameters
144         * @param clusterParams
145         *            subunit clustering parameters
146         * @return List of LOCAL quaternary structure symmetry results. Empty if
147         *         none.
148         */
149        public static List<QuatSymmetryResults> calcLocalSymmetries(
150                        Structure structure, QuatSymmetryParameters symmParams,
151                        SubunitClustererParameters clusterParams) {
152
153                Stoichiometry composition = SubunitClusterer.cluster(structure, clusterParams);
154                return calcLocalSymmetries(composition, symmParams);
155        }
156
157        /**
158         * Returns a List of LOCAL symmetry results. This means that a subset of the
159         * {@link SubunitCluster} is left out of the symmetry calculation. Each
160         * element of the List is one possible LOCAL symmetry result.
161         * <p>
162         * Determine local symmetry if global structure is: (1) asymmetric, C1; (2)
163         * heteromeric (belongs to more than 1 subunit cluster); (3) more than 2
164         * subunits (heteromers with just 2 chains cannot have local symmetry)
165         *
166         * @param subunits
167         *            list of {@link Subunit}
168         * @param symmParams
169         *            quaternary symmetry parameters
170         * @param clusterParams
171         *            subunit clustering parameters
172         * @return List of LOCAL quaternary structure symmetry results. Empty if
173         *         none.
174         */
175        public static List<QuatSymmetryResults> calcLocalSymmetries(
176                        List<Subunit> subunits, QuatSymmetryParameters symmParams,
177                        SubunitClustererParameters clusterParams) {
178
179                Stoichiometry composition = SubunitClusterer.cluster(subunits, clusterParams);
180                return calcLocalSymmetries(composition, symmParams);
181        }
182
183        /**
184         * Returns a List of LOCAL symmetry results. This means that a subset of the
185         * {@link SubunitCluster} is left out of the symmetry calculation. Each
186         * element of the List is one possible LOCAL symmetry result.
187         * <p>
188         * Determine local symmetry if global structure is: (1) asymmetric, C1; (2)
189         * heteromeric (belongs to more than 1 subunit cluster); (3) more than 2
190         * subunits (heteromers with just 2 chains cannot have local symmetry)
191         *
192         * @param globalComposition
193         *            {@link Stoichiometry} object that contains global clustering results
194         * @param symmParams
195         *            quaternary symmetry parameters
196         * @return List of LOCAL quaternary structure symmetry results. Empty if
197         *         none.
198         */
199
200        public static List<QuatSymmetryResults> calcLocalSymmetries(Stoichiometry globalComposition, QuatSymmetryParameters symmParams) {
201
202                Set<Set<Integer>> knownCombinations = new HashSet<>();
203                List<SubunitCluster> clusters = globalComposition.getClusters();
204                //more than one subunit per cluster required for symmetry
205                List<SubunitCluster> nontrivialClusters =
206                                clusters.stream().
207                                        filter(cluster -> (cluster.size()>1)).
208                                        collect(Collectors.toList());
209
210                QuatSymmetrySubunits consideredSubunits = new QuatSymmetrySubunits(nontrivialClusters);
211                if (consideredSubunits.getSubunitCount() < 2)
212                        return new ArrayList<>();
213
214                Graph<Integer, DefaultEdge> graph = initContactGraph(nontrivialClusters);
215                Stoichiometry nontrivialComposition = new Stoichiometry(nontrivialClusters,false);
216
217                List<Integer> allSubunitIds = new ArrayList<>(graph.vertexSet());
218                Collections.sort(allSubunitIds);
219                List<Integer> allSubunitClusterIds = consideredSubunits.getClusterIds();
220
221                // since clusters are rearranged and trimmed, we need a reference to the original data
222                // to maintain consistent IDs of clusters and subunits across all solutions
223                Map<Integer, List<Integer>> clusterIdToSubunitIds =
224                                allSubunitIds.stream().
225                                        collect(Collectors.
226                                                groupingBy(allSubunitClusterIds::get, Collectors.toList()));
227
228                List<QuatSymmetryResults> redundantSymmetries = new ArrayList<>();
229                // first, find symmetries for single clusters and their groups
230                // grouping is done based on symmetries found (i.e., no exhaustive permutation search is performed)
231                if (clusters.size()>1) {
232
233                        List<QuatSymmetryResults> clusterSymmetries =
234                                        calcLocalSymmetriesCluster(nontrivialComposition, clusterIdToSubunitIds,symmParams, knownCombinations);
235                        redundantSymmetries.addAll(clusterSymmetries);
236                }
237                //find symmetries for groups based on connectivity of subunits
238                // disregarding initial clustering
239                List<QuatSymmetryResults> graphSymmetries = calcLocalSymmetriesGraph(nontrivialComposition,
240                                                                                                                                                        allSubunitClusterIds,
241                                                                                                                                                        clusterIdToSubunitIds,
242                                                                                                                                                        symmParams,
243                                                                                                                                                        knownCombinations,
244                                                                                                                                                        graph);
245
246                redundantSymmetries.addAll(graphSymmetries);
247
248                // find symmetries which are not superseded by any other symmetry
249                // e.g., we have global stoichiometry of A3B3C,
250                // the local symmetries found are C3 with stoichiometries A3, B3, A3B3;
251                // then output only A3B3.
252
253                List<QuatSymmetryResults> outputSymmetries =
254                                redundantSymmetries.stream().
255                                        filter(a -> redundantSymmetries.stream().
256                                                noneMatch(b -> a!=b && a.isSupersededBy(b))).
257                                                collect(Collectors.toList());
258
259                if(symmParams.isLocalLimitsExceeded(knownCombinations)) {
260                        logger.warn("Exceeded calculation limits for local symmetry detection. The results may be incomplete.");
261                }
262
263                return outputSymmetries;
264        }
265
266
267        private static  Graph<Integer, DefaultEdge> initContactGraph(List<SubunitCluster> clusters){
268
269                Graph<Integer, DefaultEdge> graph = new SimpleGraph<>(DefaultEdge.class);
270
271                // extract Ca coords from every subunit of every cluster.
272                // all subunit coords are used for contact evaluation,
273                // not only the aligned equivalent residues
274                List <Point3d[]> clusterSubunitCoords =
275                                clusters.stream().
276                                        flatMap(c -> c.getSubunits().stream()).
277                                                map(r -> Calc.atomsToPoints(r.getRepresentativeAtoms())).
278                                                collect(Collectors.toList());
279
280                for (int i = 0; i < clusterSubunitCoords.size(); i++) {
281                        graph.addVertex(i);
282                }
283
284                // pre-compute bounding boxes
285                List<BoundingBox> boundingBoxes = new ArrayList<>();
286                clusterSubunitCoords.forEach(c -> boundingBoxes.add(new BoundingBox(c)));
287
288                for (int i = 0; i < clusterSubunitCoords.size() - 1; i++) {
289                        Point3d[] coords1 = clusterSubunitCoords.get(i);
290                        BoundingBox bb1 = boundingBoxes.get(i);
291
292                        for (int j = i + 1; j < clusterSubunitCoords.size(); j++) {
293                                Point3d[] coords2 = clusterSubunitCoords.get(j);
294                                BoundingBox bb2 = boundingBoxes.get(j);
295                                Grid grid = new Grid(CONTACT_GRAPH_DISTANCE_CUTOFF);
296                                grid.addCoords(coords1, bb1, coords2, bb2);
297
298                                if (grid.getIndicesContacts().size() >= CONTACT_GRAPH_MIN_CONTACTS) {
299                                        graph.addEdge(i, j);
300                                }
301                        }
302                }
303                return graph;
304        }
305
306        private static List<QuatSymmetryResults> calcLocalSymmetriesCluster(Stoichiometry nontrivialComposition,
307                                                                            Map<Integer, List<Integer>> clusterIdToSubunitIds,
308                                                                            QuatSymmetryParameters symmParams,
309                                                                            Set<Set<Integer>> knownCombinations) {
310
311                List<QuatSymmetryResults> clusterSymmetries = new ArrayList<>();
312
313                // find solutions for single clusters first
314                for (int i=0;i<nontrivialComposition.numberOfComponents();i++) {
315                        QuatSymmetryResults localResult =
316                                        calcQuatSymmetry(nontrivialComposition.getComponent(i),symmParams);
317
318                        if(localResult!=null && !localResult.getSymmetry().equals("C1")) {
319                                localResult.setLocal(true);
320                                clusterSymmetries.add(localResult);
321                                Set<Integer> knownResult = new HashSet<>(clusterIdToSubunitIds.get(i));
322                                // since symmetry is found,
323                                // do not try graph decomposition of this set of subunits later
324                                knownCombinations.add(knownResult);
325                        }
326                }
327
328                // group clusters by symmetries found, in case they all share axes and have the same number of subunits
329                Map<String, Map<Integer,List<QuatSymmetryResults>>> groupedSymmetries =
330                                clusterSymmetries.stream().
331                                        collect(Collectors.
332                                                groupingBy(QuatSymmetryResults::getSymmetry,Collectors.
333                                                        groupingBy(QuatSymmetryResults::getSubunitCount,Collectors.toList())));
334
335                for (Map<Integer,List<QuatSymmetryResults>> symmetriesByGroup: groupedSymmetries.values()) {
336                        for (List<QuatSymmetryResults> symmetriesBySubunits: symmetriesByGroup.values()) {
337
338                                Stoichiometry groupComposition =
339                                                symmetriesBySubunits.stream().
340                                                        map(QuatSymmetryResults::getStoichiometry).
341                                                                reduce(Stoichiometry::combineWith).get();
342
343                                if (groupComposition.numberOfComponents() < 2) {
344                                        continue;
345                                }
346                                //check if grouped clusters also have symmetry
347                                QuatSymmetryResults localResult = calcQuatSymmetry(groupComposition,symmParams);
348
349                                if(localResult!=null && !localResult.getSymmetry().equals("C1")) {
350                                        localResult.setLocal(true);
351                                        clusterSymmetries.add(localResult);
352                                        // find subunit ids in this cluster list
353                                        Set<Integer> knownResult = new HashSet<>();
354                                        for (SubunitCluster cluster: groupComposition.getClusters()) {
355                                                int i = nontrivialComposition.getClusters().indexOf(cluster);
356                                                knownResult.addAll(clusterIdToSubunitIds.get(i));
357                                        }
358                                        // since symmetry is found,
359                                        // do not try graph decomposition of this set of subunits later
360                                        knownCombinations.add(knownResult);
361                                }
362                        }
363                }
364                return clusterSymmetries;
365        }
366
367
368        private static List<QuatSymmetryResults> calcLocalSymmetriesGraph(final Stoichiometry globalComposition,
369                                                                          final List<Integer> allSubunitClusterIds,
370                                                                          final Map<Integer, List<Integer>> clusterIdToSubunitIds,
371                                                                          QuatSymmetryParameters symmParams,
372                                                                          Set<Set<Integer>> knownCombinations,
373                                                                          Graph<Integer, DefaultEdge> graph) {
374
375                List<QuatSymmetryResults> localSymmetries = new ArrayList<>();
376
377                // do not go any deeper into recursion if over the time/combinations limit
378                if(symmParams.isLocalLimitsExceeded(knownCombinations)) {
379                        return localSymmetries;
380                }
381                // extract components of a (sub-)graph
382                CliqueMinimalSeparatorDecomposition<Integer, DefaultEdge> cmsd =
383                                new CliqueMinimalSeparatorDecomposition<>(graph);
384
385                // only consider components with more than 1 vertex (subunit)
386                Set<Set<Integer>> graphComponents =
387                                cmsd.getAtoms().stream().
388                                        filter(component -> component.size()>1).
389                                        collect(Collectors.toSet());
390
391                //do not go into what has already been explored
392                graphComponents.removeAll(knownCombinations);
393
394                for (Set<Integer> graphComponent: graphComponents) {
395                        knownCombinations.add(graphComponent);
396
397                        List<Integer> usedSubunitIds = new ArrayList<>(graphComponent);
398                        Collections.sort(usedSubunitIds);
399                        // get clusters which contain only subunits in the current component
400                        Stoichiometry localStoichiometry =
401                                        trimSubunitClusters(globalComposition, allSubunitClusterIds, clusterIdToSubunitIds, usedSubunitIds);
402
403                        if (localStoichiometry.numberOfComponents()==0) {
404                                continue;
405                        }
406
407                        //NB: usedSubunitIds might have changed when trimming clusters
408                        // if a subunit belongs to a cluster with no other subunits,
409                        // it is removed inside trimSubunitClusters
410                        Set<Integer> usedSubunitIdsSet = new HashSet<>(usedSubunitIds);
411                        if(!graphComponent.equals(usedSubunitIdsSet)) {
412                                if(knownCombinations.contains(usedSubunitIdsSet)) {
413                                        continue;
414                                } else {
415                                        knownCombinations.add(usedSubunitIdsSet);
416                                }
417                        }
418
419                        QuatSymmetryResults localResult = calcQuatSymmetry(localStoichiometry,symmParams);
420                        if(localResult!=null && !localResult.getSymmetry().equals("C1")) {
421                                localResult.setLocal(true);
422                                localSymmetries.add(localResult);
423                                continue;
424                        }
425
426                        if (usedSubunitIds.size() < 3) {
427                                // cannot decompose this component any further
428                                continue;
429                        }
430
431                        for (Integer removeSubunitId: usedSubunitIds) {
432                                // try removing subunits one by one and decompose the sub-graph recursively
433                                Set<Integer> prunedGraphVertices = new HashSet<>(usedSubunitIds);
434                                prunedGraphVertices.remove(removeSubunitId);
435                                if (knownCombinations.contains(prunedGraphVertices)) {
436                                        continue;
437                                }
438                                knownCombinations.add(prunedGraphVertices);
439
440                                Graph<Integer, DefaultEdge> subGraph = new AsSubgraph<>(graph,prunedGraphVertices);
441
442                                List<QuatSymmetryResults> localSubSymmetries = calcLocalSymmetriesGraph(globalComposition,
443                                                                                                                                                                                allSubunitClusterIds,
444                                                                                                                                                                                clusterIdToSubunitIds,
445                                                                                                                                                                                symmParams,
446                                                                                                                                                                                knownCombinations,
447                                                                                                                                                                                subGraph);
448                                localSymmetries.addAll(localSubSymmetries);
449                        }
450
451                }
452
453                return localSymmetries;
454        }
455
456        private static Stoichiometry trimSubunitClusters(Stoichiometry globalComposition,
457                                                                List<Integer> allSubunitClusterIds,
458                                                                Map<Integer, List<Integer>> clusterIdToSubunitIds,
459                                                                List<Integer> usedSubunitIds) {
460                List<SubunitCluster> globalClusters = globalComposition.getClusters();
461                List<SubunitCluster> localClusters = new ArrayList<>();
462
463                TreeSet<Integer> usedClusterIds =
464                                usedSubunitIds.stream().
465                                        map(allSubunitClusterIds::get).
466                                        distinct().
467                                        collect(Collectors.toCollection(TreeSet::new));
468
469                // for each used cluster, remove unused subunits
470                for(Integer usedClusterId:usedClusterIds) {
471                        SubunitCluster originalCluster = globalClusters.get(usedClusterId);
472                        List<Integer> allSubunitIdsInCluster = clusterIdToSubunitIds.get(usedClusterId);
473
474                        //subunit numbering is global for the entire graph
475                        // make it zero-based for the inside of a cluster
476                        int minSUValue = Collections.min(allSubunitIdsInCluster);
477                        List<Integer> usedSubunitIdsInCluster = new ArrayList<>(allSubunitIdsInCluster);
478                        usedSubunitIdsInCluster.retainAll(usedSubunitIds);
479
480                        List<Integer> subunitsToRetain =
481                                        usedSubunitIdsInCluster.stream().
482                                                map(i -> i-minSUValue).
483                                                collect(Collectors.toList());
484
485                        if (subunitsToRetain.size()>1) {
486                                SubunitCluster filteredCluster = new SubunitCluster(originalCluster, subunitsToRetain);
487                                localClusters.add(filteredCluster);
488                        } else {
489                                // if the cluster ends up having only 1 subunit, remove it from further processing
490                                usedSubunitIds.removeAll(usedSubunitIdsInCluster);
491                        }
492                }
493                return new Stoichiometry(localClusters,false);
494        }
495
496
497        private static QuatSymmetryResults calcQuatSymmetry(Stoichiometry composition, QuatSymmetryParameters parameters) {
498
499                QuatSymmetrySubunits subunits = new QuatSymmetrySubunits(composition.getClusters());
500
501                if (subunits.getSubunitCount() == 0)
502                        return null;
503
504                RotationGroup rotationGroup = null;
505                SymmetryPerceptionMethod method = null;
506                if (subunits.getFolds().size() == 1) {
507                        // no symmetry possible, create empty ("C1") rotation group
508                        method = SymmetryPerceptionMethod.NO_ROTATION;
509                        rotationGroup = new RotationGroup();
510                        rotationGroup.setC1(subunits.getSubunitCount());
511                } else if (subunits.getSubunitCount() == 2
512                                && subunits.getFolds().contains(2)) {
513                        method = SymmetryPerceptionMethod.C2_ROTATION;
514                        QuatSymmetrySolver solver = new C2RotationSolver(subunits,
515                                        parameters);
516                        rotationGroup = solver.getSymmetryOperations();
517                } else {
518                        method = SymmetryPerceptionMethod.ROTATION;
519                        QuatSymmetrySolver solver = new RotationSolver(subunits, parameters);
520                        rotationGroup = solver.getSymmetryOperations();
521                }
522
523                QuatSymmetryResults results = new QuatSymmetryResults(composition,
524                                rotationGroup, method);
525
526                String symmetry = results.getSymmetry();
527
528                // Check structures with Cn symmetry (n = 1, ...) for helical symmetry
529                if (symmetry.startsWith("C")) {
530                        HelixSolver hc = new HelixSolver(subunits,
531                                        rotationGroup.getOrder(), parameters);
532                        HelixLayers helixLayers = hc.getSymmetryOperations();
533
534                        if (helixLayers.size() > 0) {
535                                // if the complex has no symmetry (c1) or has Cn (n>=2) symmetry
536                                // and the
537                                // helical symmetry has a lower RMSD than the cyclic symmetry,
538                                // set helical symmetry
539                                // If the RMSD for helical and cyclic symmetry is similar, a
540                                // slight preference is
541                                // given to the helical symmetry by the helixRmsdThreshold
542                                // parameter.
543                                double cRmsd = rotationGroup.getScores().getRmsd();
544                                double hRmsd = helixLayers.getScores().getRmsd();
545                                // System.out.println("cRMSD: " + cRmsd + " hRMSD: " + hRmsd);
546                                double deltaRmsd = hRmsd - cRmsd;
547                                if (symmetry.equals("C1")
548                                                || (!symmetry.equals("C1") && deltaRmsd <= parameters
549                                                                .getHelixRmsdThreshold())) {
550                                        method = SymmetryPerceptionMethod.ROTO_TRANSLATION;
551                                        results = new QuatSymmetryResults(composition, helixLayers,
552                                                        method);
553                                }
554                        }
555                }
556
557                return results;
558        }
559}