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}