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