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 */ 021 022package org.biojava.nbio.structure.symmetry.core; 023 024 025import javax.vecmath.AxisAngle4d; 026import javax.vecmath.Matrix4d; 027import javax.vecmath.Point3d; 028import javax.vecmath.Quat4d; 029import javax.vecmath.Vector3d; 030 031import org.biojava.nbio.structure.geometry.CalcPoint; 032import org.biojava.nbio.structure.geometry.MomentsOfInertia; 033import org.biojava.nbio.structure.geometry.UnitQuaternions; 034import org.biojava.nbio.structure.symmetry.geometry.DistanceBox; 035import org.biojava.nbio.structure.symmetry.geometry.SphereSampler; 036 037import java.util.ArrayList; 038import java.util.HashMap; 039import java.util.HashSet; 040import java.util.List; 041import java.util.Map; 042import java.util.Objects; 043import java.util.Set; 044 045 046/** 047 * 048 * @author Peter 049 */ 050public class RotationSolver implements QuatSymmetrySolver { 051 private QuatSymmetrySubunits subunits = null; 052 private QuatSymmetryParameters parameters = null; 053 054 private double distanceThreshold = 0.0f; 055 private DistanceBox<Integer> box = null; 056 private Vector3d centroid = new Vector3d(); 057 private Matrix4d centroidInverse = new Matrix4d(); 058 private Point3d[] originalCoords = null; 059 private Point3d[] transformedCoords = null; 060 // Cache whether a permutation is invalid (null) vs has been added to rotations 061 private Map<List<Integer>,Rotation> evaluatedPermutations = new HashMap<>(); 062 063 private RotationGroup rotations = new RotationGroup(); 064 065 public RotationSolver(QuatSymmetrySubunits subunits, QuatSymmetryParameters parameters) { 066 if (subunits.getSubunitCount()== 2) { 067 throw new IllegalArgumentException("RotationSolver cannot be applied to subunits with 2 centers"); 068 } 069 this.subunits = subunits; 070 this.parameters = parameters; 071 } 072 073 @Override 074 public RotationGroup getSymmetryOperations() { 075 if (rotations.getOrder() == 0) { 076 solve(); 077 completeRotationGroup(); 078 rotations.complete(); 079 } 080 081 return rotations; 082 } 083 084 private void solve() { 085 initialize(); 086 087 int maxSymOps = subunits.getSubunitCount(); 088 089 boolean isSpherical = isSpherical(); 090 091 // for cases with icosahedral symmetry n cannot be higher than 60, should check for spherical symmetry here 092 // isSpherical check added 08-04-11 093 if (maxSymOps % 60 == 0 && isSpherical) { 094 maxSymOps = 60; 095 } 096 097 AxisAngle4d sphereAngle = new AxisAngle4d(); 098 Matrix4d transformation = new Matrix4d(); 099 100 int n = subunits.getSubunitCount(); 101 102 int sphereCount = SphereSampler.getSphereCount(); 103 104 List<Double> angles = getAngles(); 105 106 for (int i = 0; i < sphereCount; i++) { 107 // Sampled orientation 108 //TODO The SphereSampler samples 4D orientation space. We really 109 // only need to sample 3D unit vectors, since we use limited 110 // angles. -SB 111 SphereSampler.getAxisAngle(i, sphereAngle); 112 113 // Each valid rotation angle 114 for (double angle : angles) { 115 // apply rotation 116 sphereAngle.angle = angle; 117 transformation.set(sphereAngle); 118 // Make sure matrix element m33 is 1.0. It's not on Linux. 119 transformation.setElement(3, 3, 1.0); 120 for (int j = 0; j < n; j++) { 121 transformedCoords[j].set(originalCoords[j]); 122 transformation.transform(transformedCoords[j]); 123 } 124 125 // get permutation of subunits and check validity/uniqueness 126 List<Integer> permutation = getPermutation(); 127 // System.out.println("Rotation Solver: permutation: " + i + ": " + permutation); 128 129 // check if novel 130 if ( evaluatedPermutations.containsKey(permutation)) { 131 continue; //either invalid or already added 132 } 133 134 Rotation newPermutation = isValidPermutation(permutation); 135 if (newPermutation != null) { 136 completeRotationGroup(newPermutation); 137 } 138 139 // check if all symmetry operations have been found. 140 if (rotations.getOrder() >= maxSymOps) { 141 return; 142 } 143 } 144 } 145 } 146 147 /** 148 * Combine current rotations to make all possible permutations. 149 * If these are all valid, add them to the rotations 150 * @param additionalRots Additional rotations we are considering adding to this.rotations 151 * @return whether the rotations were valid and added 152 */ 153 private boolean completeRotationGroup(Rotation... additionalRots) { 154 PermutationGroup g = new PermutationGroup(); 155 for (Rotation s : rotations) { 156 g.addPermutation(s.getPermutation()); 157 } 158 for( Rotation s : additionalRots) { 159 g.addPermutation(s.getPermutation()); 160 // inputs should not have been added already 161 assert evaluatedPermutations.get(s.getPermutation()) == null; 162 } 163 g.completeGroup(); 164 165 // the group is complete, nothing to do 166 if (g.getOrder() == rotations.getOrder()+additionalRots.length) { 167 for( Rotation s : additionalRots) { 168 addRotation(s); 169 } 170 return true; 171 } 172 173 // try to complete the group 174 List<Rotation> newRots = new ArrayList<>(g.getOrder()); 175 // First, quick check for whether they're allowed 176 for (List<Integer> permutation : g) { 177 if( evaluatedPermutations.containsKey(permutation)) { 178 Rotation rot = evaluatedPermutations.get(permutation); 179 if( rot == null ) { 180 return false; 181 } 182 } else { 183 if( ! isAllowedPermutation(permutation)) { 184 return false; 185 } 186 } 187 } 188 // Slower check including the superpositions 189 for (List<Integer> permutation : g) { 190 Rotation rot; 191 if( evaluatedPermutations.containsKey(permutation)) { 192 rot = evaluatedPermutations.get(permutation); 193 } else { 194 rot = isValidPermutation(permutation); 195 } 196 197 if( rot == null ) { 198 // if any induced rotation is invalid, abort 199 return false; 200 } 201 if(!evaluatedPermutations.containsKey(permutation)){ //novel 202 newRots.add(rot); 203 } 204 } 205 // Add rotations 206 for( Rotation rot : newRots) { 207 addRotation(rot); 208 } 209 return true; 210 } 211 212 private void addRotation(Rotation rot) { 213 evaluatedPermutations.put(rot.getPermutation(),rot); 214 rotations.addRotation(rot); 215 } 216 217 /** 218 * Superimpose subunits based on the given permutation. Then check whether 219 * the superposition passes RMSD thresholds and create a Rotation to 220 * represent it if so. 221 * @param permutation A list specifying which subunits should be aligned by the current transformation 222 * @return A Rotation representing the permutation, or null if the superposition did not meet thresholds. 223 */ 224 private Rotation superimposePermutation(List<Integer> permutation) { 225 // permutate subunits 226 for (int j = 0, n = subunits.getSubunitCount(); j < n; j++) { 227 transformedCoords[j].set(originalCoords[permutation.get(j)]); 228 } 229 230 int fold = PermutationGroup.getOrder(permutation); 231 232 // get optimal transformation and axisangle by subunit superposition 233 // TODO implement this piece of code using at origin superposition 234 Quat4d quat = UnitQuaternions.relativeOrientation( 235 originalCoords, transformedCoords); 236 AxisAngle4d axisAngle = new AxisAngle4d(); 237 Matrix4d transformation = new Matrix4d(); 238 239 transformation.set(quat); 240 axisAngle.set(quat); 241 242 Vector3d axis = new Vector3d(axisAngle.x, axisAngle.y, axisAngle.z); 243 if (axis.lengthSquared() < 1.0E-6) { 244 axisAngle.x = 0; 245 axisAngle.y = 0; 246 axisAngle.z = 1; 247 axisAngle.angle = 0; 248 } else { 249 axis.normalize(); 250 axisAngle.x = axis.x; 251 axisAngle.y = axis.y; 252 axisAngle.z = axis.z; 253 } 254 255 CalcPoint.transform(transformation, transformedCoords); 256 double subunitRmsd = CalcPoint.rmsd(transformedCoords, originalCoords); 257 258 if (subunitRmsd < parameters.getRmsdThreshold()) { 259 combineWithTranslation(transformation); 260 261 // evaluate superposition of CA traces 262 QuatSymmetryScores scores = QuatSuperpositionScorer.calcScores(subunits, transformation, permutation); 263 if (scores.getRmsd() < 0.0 || scores.getRmsd() > parameters.getRmsdThreshold()) { 264 return null; 265 } 266 267 scores.setRmsdCenters(subunitRmsd); 268 Rotation symmetryOperation = createSymmetryOperation(permutation, transformation, axisAngle, fold, scores); 269 return symmetryOperation; 270 } 271 return null; 272 } 273 274 /** 275 * Get valid rotation angles given the number of subunits 276 * @return The rotation angle corresponding to each fold of {@link Subunits#getFolds()} 277 */ 278 private List<Double> getAngles() { 279 int n = subunits.getSubunitCount(); 280 // for spherical symmetric cases, n cannot be higher than 60 281 if (n % 60 == 0 && isSpherical()) { 282 n = 60; 283 } 284 List<Integer> folds = subunits.getFolds(); 285 List<Double> angles = new ArrayList<>(folds.size()-1); 286 287 // note this loop starts at 1, we do ignore 1-fold symmetry, which is the first entry 288 for (int fold: folds) { 289 if (fold > 0 && fold <= n) { 290 angles.add(2* Math.PI/fold); 291 } 292 } 293 return angles; 294 } 295 296 private boolean isSpherical() { 297 MomentsOfInertia m = subunits.getMomentsOfInertia(); 298 return m.getSymmetryClass(0.05) == MomentsOfInertia.SymmetryClass.SYMMETRIC; 299 } 300 301 /** 302 * Checks if a particular permutation is allowed and superimposes well. 303 * Caches results. 304 * @param permutation 305 * @return null if invalid, or a rotation if valid 306 */ 307 private Rotation isValidPermutation(List<Integer> permutation) { 308 if (permutation.size() == 0) { 309 return null; 310 } 311 312 // cached value 313 if (evaluatedPermutations.containsKey(permutation)) { 314 return evaluatedPermutations.get(permutation); 315 } 316 317 // check if permutation is allowed 318 if (! isAllowedPermutation(permutation)) { 319 evaluatedPermutations.put(permutation, null); 320 return null; 321 } 322 323 // check if superimposes 324 Rotation rot = superimposePermutation(permutation); 325 return rot; 326 } 327 328 /** 329 * The permutation must map all subunits onto an equivalent subunit 330 * and no subunit onto itself 331 * @param permutation 332 * @return 333 */ 334 private boolean isAllowedPermutation(List<Integer> permutation) { 335 List<Integer> seqClusterId = subunits.getClusterIds(); 336 int selfaligned = 0; 337 for (int i = 0; i < permutation.size(); i++) { 338 int j = permutation.get(i); 339 if (!Objects.equals(seqClusterId.get(i), seqClusterId.get(j))) { 340 return false; 341 } 342 if(i == j ) { 343 selfaligned++; 344 } 345 } 346 // either identity (all self aligned) or all unique 347 return selfaligned == 0 || selfaligned == permutation.size(); 348 } 349 350 /** 351 * Adds translational component to rotation matrix 352 * @param rotation 353 * @return 354 */ 355 private void combineWithTranslation(Matrix4d rotation) { 356 rotation.setTranslation(centroid); 357 rotation.mul(rotation, centroidInverse); 358 } 359 360 private static Rotation createSymmetryOperation(List<Integer> permutation, Matrix4d transformation, AxisAngle4d axisAngle, int fold, QuatSymmetryScores scores) { 361 Rotation s = new Rotation(); 362 s.setPermutation(new ArrayList<Integer>(permutation)); 363 s.setTransformation(new Matrix4d(transformation)); 364 s.setAxisAngle(new AxisAngle4d(axisAngle)); 365 s.setFold(fold); 366 s.setScores(scores); 367 368 return s; 369 } 370 371 private void setupDistanceBox() { 372 distanceThreshold = calcDistanceThreshold(); 373 box = new DistanceBox<>(distanceThreshold); 374 375 for (int i = 0; i < originalCoords.length; i++) { 376 box.addPoint(originalCoords[i], i); 377 } 378 } 379 380 private double calcDistanceThreshold() { 381 double threshold = Double.MAX_VALUE; 382 int n = subunits.getSubunitCount(); 383 List<Point3d> centers = subunits.getCenters(); 384 385 for (int i = 0; i < n - 1; i++) { 386 Point3d pi = centers.get(i); 387 for (int j = i + 1; j < n; j++) { 388 Point3d pj = centers.get(j); 389 threshold = Math.min(threshold, pi.distanceSquared(pj)); 390 } 391 } 392 double distanceThreshold = Math.sqrt(threshold); 393 394 distanceThreshold = Math.max(distanceThreshold, parameters.getRmsdThreshold()); 395 396 return distanceThreshold; 397 } 398 399 /** 400 * Compare this.transformedCoords with the original coords. For each 401 * subunit, return the transformed subunit with the closest position. 402 * @return A list mapping each subunit to the closest transformed subunit 403 */ 404 private List<Integer> getPermutation() { 405 List<Integer> permutation = new ArrayList<>(transformedCoords.length); 406 double sum = 0.0f; 407 408 for (Point3d t: transformedCoords) { 409 List<Integer> neighbors = box.getNeighborsWithCache(t); 410 int closest = -1; 411 double minDist = Double.MAX_VALUE; 412 413 for (int j : neighbors) { 414 double dist = t.distanceSquared(originalCoords[j]); 415 if (dist < minDist) { 416 closest = j; 417 minDist = dist; 418 } 419 } 420 421 sum += minDist; 422 if (closest == -1) { 423 break; 424 } 425 permutation.add(closest); 426 } 427 double rmsd = Math.sqrt(sum / transformedCoords.length); 428 429 if (rmsd > distanceThreshold || permutation.size() != transformedCoords.length) { 430 permutation.clear(); 431 return permutation; 432 } 433 434 // check uniqueness of indices 435 Set<Integer> set = new HashSet<>(permutation); 436 437 // if size mismatch, clear permutation (its invalid) 438 if (set.size() != originalCoords.length) { 439 permutation.clear(); 440 } 441 442 return permutation; 443 } 444 445 private void initialize() { 446 // translation to centered coordinate system 447 centroid = new Vector3d(subunits.getCentroid()); 448 // translation back to original coordinate system 449 Vector3d reverse = new Vector3d(centroid); 450 reverse.negate(); 451 centroidInverse.set(reverse); 452 // Make sure matrix element m33 is 1.0. An old version vecmath did not set this element. 453 centroidInverse.setElement(3, 3, 1.0); 454 455 List<Point3d> centers = subunits.getCenters(); 456 int n = subunits.getSubunitCount(); 457 458 originalCoords = new Point3d[n]; 459 transformedCoords = new Point3d[n]; 460 461 for (int i = 0; i < n; i++) { 462 originalCoords[i] = centers.get(i); 463 transformedCoords[i] = new Point3d(); 464 } 465 466 setupDistanceBox(); 467 } 468}