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 */ 021package org.biojava.nbio.structure.align.util; 022 023import org.biojava.nbio.structure.Atom; 024import org.biojava.nbio.structure.AtomImpl; 025import org.biojava.nbio.structure.Calc; 026import org.biojava.nbio.structure.StructureException; 027import org.biojava.nbio.structure.align.model.AFPChain; 028import org.biojava.nbio.structure.contact.Pair; 029import org.biojava.nbio.structure.geometry.Matrices; 030import org.biojava.nbio.structure.jama.Matrix; 031 032import javax.vecmath.AxisAngle4d; 033import javax.vecmath.Matrix4d; 034import javax.vecmath.Vector3d; 035 036import java.io.StringWriter; 037 038/** 039 * Calculates the rotation axis for an alignment 040 * 041 * <p>A superposition of two structures is generally represented as a rotation 042 * matrix plus a translation vector. However, it can also be represented as an 043 * axis of rotation plus some translation. 044 * 045 * <p>This class calculates the rotation axis and stores it as four properties: 046 * <ul><li>A unit vector parallel to the rotation axis ({@link #getRotationAxis()}) 047 * <li>The angle of rotation ({@link #getAngle()}) 048 * <li>A point on the rotation axis ({@link #getRotationPos()}) 049 * <li>Some translation parallel to the axis ({@link #getScrewTranslation()}) 050 * </ul> 051 * 052 * <p>The axis of rotation is poorly defined and numerically unstable for small 053 * angles. Therefore it's direction is left as null for angles less than 054 * {@link #MIN_ANGLE}. 055 * 056 * @author Spencer Bliven 057 * 058 */ 059public final class RotationAxis { 060 061 /** 062 * Minimum angle to calculate rotation axes. 5 degrees. 063 */ 064 static final double MIN_ANGLE = Math.toRadians(5.); 065 066 private double theta; 067 private Atom rotationAxis; // axis of rotation 068 private Atom rotationPos; // a point on the axis of rotation 069 private Atom screwTranslation; //translation parallel to the axis of rotation 070 private Atom otherTranslation; // translation perpendicular to the axis of rotation 071 072 /** 073 * The rotation angle 074 * @return the angle, in radians 075 */ 076 public double getAngle() { 077 return theta; 078 } 079 080 /** 081 * Get a unit vector along the rotation axis 082 * @return rotationAxis 083 */ 084 public Atom getRotationAxis() { 085 return rotationAxis; 086 } 087 088 /** 089 * Returns the rotation axis and angle in a single javax.vecmath.AxisAngle4d object 090 * @return 091 */ 092 public AxisAngle4d getAxisAngle4d() { 093 return new AxisAngle4d(rotationAxis.getX(),rotationAxis.getY(),rotationAxis.getZ(),theta); 094 } 095 096 /** 097 * Get a position on the rotation axis. 098 * 099 * Specifically, project the origin onto the rotation axis 100 * @return rotationPos 101 */ 102 public Atom getRotationPos() { 103 return rotationPos; 104 } 105 106 /** 107 * Get the component of translation parallel to the axis of rotation 108 * @return 109 */ 110 public Atom getScrewTranslation() { 111 return screwTranslation; 112 } 113 114 public Vector3d getVector3dScrewTranslation() { 115 return new Vector3d(screwTranslation.getX(),screwTranslation.getY(),screwTranslation.getZ()); 116 } 117 118 public double getTranslation() { 119 return Calc.amount(screwTranslation); 120 } 121 122 /** 123 * Calculate the rotation axis for the first block of an AFPChain 124 * @param afpChain 125 * @throws StructureException 126 * @throws NullPointerException if afpChain does not contain a valid rotation matrix and shift vector 127 */ 128 public RotationAxis(AFPChain afpChain) throws StructureException { 129 if(afpChain.getAlnLength() < 1) { 130 throw new StructureException("No aligned residues"); 131 } 132 init(afpChain.getBlockRotationMatrix()[0],afpChain.getBlockShiftVector()[0]); 133 } 134 135 /** 136 * Create a rotation axis from a vector, a point, and an angle. 137 * 138 * The result will be a pure rotation, with no screw component. 139 * @param axis A vector parallel to the axis of rotation 140 * @param pos A point on the axis of rotation 141 * @param theta The angle to rotate (radians) 142 */ 143 public RotationAxis(Atom axis, Atom pos, double theta) { 144 this.rotationAxis = Calc.unitVector(axis); 145 this.rotationPos = (Atom) pos.clone(); 146 this.theta = theta; 147 this.screwTranslation = new AtomImpl(); //zero 148 this.otherTranslation = null; //deprecated 149 } 150 151 /** 152 * Determine the location of the rotation axis based on a rotation matrix and a translation vector 153 * @param rotation 154 * @param translation 155 */ 156 public RotationAxis(Matrix rotation, Atom translation) { 157 init(rotation, translation); 158 } 159 160 /** 161 * Create a rotation axis from a Matrix4d containing a rotational 162 * component and a translational component. 163 * 164 * @param transform 165 */ 166 public RotationAxis(Matrix4d transform) { 167 168 Matrix rot = Matrices.getRotationJAMA(transform); 169 Atom transl = Calc.getTranslationVector(transform); 170 init(rot,transl); 171 } 172 173 /** 174 * Get the rotation matrix corresponding to this axis 175 * @return A 3x3 rotation matrix 176 */ 177 public Matrix getRotationMatrix() { 178 return getRotationMatrix(theta); 179 } 180 181 /** 182 * Get the rotation matrix corresponding to a rotation about this axis 183 * @param theta The amount to rotate 184 * @return A 3x3 rotation matrix 185 */ 186 public Matrix getRotationMatrix(double theta) { 187 if( rotationAxis == null) { 188 // special case for pure translational axes 189 return Matrix.identity(3, 3); 190 } 191 double x = rotationAxis.getX(); 192 double y = rotationAxis.getY(); 193 double z = rotationAxis.getZ(); 194 double cos = Math.cos(theta); 195 double sin = Math.sin(theta); 196 double com = 1 - cos; 197 return new Matrix(new double[][] { 198 {com*x*x + cos, com*x*y+sin*z, com*x*z+-sin*y}, 199 {com*x*y-sin*z, com*y*y+cos, com*y*z+sin*x}, 200 {com*x*z+sin*y, com*y*z-sin*x, com*z*z+cos}, 201 }); 202 } 203 204 /** 205 * Returns the rotation order o that gives the lowest value of {@code |2PI / o - theta}, 206 * given that the value is strictly lower than {@code threshold}, for orders {@code o=1,...,maxOrder}. 207 */ 208 public int guessOrderFromAngle(double threshold, int maxOrder) { 209 double bestDelta = threshold; 210 int bestOrder = 1; 211 for (int order = 2; order < maxOrder; order++) { 212 double delta = Math.abs(2 * Math.PI / order - theta); 213 if (delta < bestDelta) { 214 bestOrder = order; 215 bestDelta = delta; 216 } 217 } 218 return bestOrder; 219 } 220 221 222 /** 223 * Returns a matrix that describes both rotation and translation. 224 */ 225 public Matrix getFullMatrix() { 226 return null; // TODO, easy 227 } 228 229 /** 230 * Initialize variables 231 * 232 * @param rotation 233 * @param translation 234 */ 235 private void init(Matrix rotation, Atom translation) { 236 if(rotation.getColumnDimension() != 3 || rotation.getRowDimension() != 3) { 237 throw new IllegalArgumentException("Expected 3x3 rotation matrix"); 238 } 239 240 241 // Calculate angle 242 double c = (rotation.trace()-1)/2.0; //=cos(theta) 243 // c is sometimes slightly out of the [-1,1] range due to numerical instabilities 244 if( -1-1e-8 < c && c < -1 ) c = -1; 245 if( 1+1e-8 > c && c > 1 ) c = 1; 246 if( -1 > c || c > 1 ) { 247 throw new IllegalArgumentException("Input matrix is not a valid rotation matrix."); 248 } 249 this.theta = Math.acos(c); 250 251 if(theta < MIN_ANGLE) { 252 calculateTranslationalAxis(rotation,translation); 253 } else { 254 calculateRotationalAxis(rotation, translation, c); 255 } 256 } 257 258 /** 259 * Calculate the rotation axis for the normal case, where there is a 260 * significant rotation angle 261 * @param rotation 262 * @param translation 263 * @param c 264 */ 265 private void calculateRotationalAxis(Matrix rotation, Atom translation, 266 double c) { 267 // Calculate magnitude of rotationAxis components, but not signs 268 double sum=0; 269 double[] rotAx = new double[3]; 270 for(int i=0;i<3;i++) { 271 rotAx[i] = Math.sqrt(rotation.get(i, i)-c); 272 sum+=rotAx[i]*rotAx[i]; 273 } 274 for(int i=0;i<3;i++) { 275 rotAx[i] /= Math.sqrt(sum); 276 } 277 278 // Now determine signs 279 double d0 = rotation.get(2,1)-rotation.get(1,2); //=2u[0]*sin(theta) 280 double d1 = rotation.get(0,2)-rotation.get(2,0); //=2u[1]*sin(theta) 281 double d2 = rotation.get(1,0)-rotation.get(0,1); //=2u[2]*sin(theta) 282 283 double s12 = rotation.get(2,1)+rotation.get(1,2); //=2*u[1]*u[2]*(1-cos(theta)) 284 double s02 = rotation.get(0,2)+rotation.get(2,0); //=2*u[0]*u[2]*(1-cos(theta)) 285 double s01 = rotation.get(1,0)+rotation.get(0,1); //=2*u[0]*u[1]*(1-cos(theta)) 286 287 //Only use biggest d for the sign directly, for numerical stability around 180deg 288 if( Math.abs(d0) < Math.abs(d1) ) { // not d0 289 if( Math.abs(d1) < Math.abs(d2) ) { //d2 290 if(d2>=0){ //u[2] positive 291 if( s02 < 0 ) rotAx[0] = -rotAx[0]; 292 if( s12 < 0 ) rotAx[1] = -rotAx[1]; 293 } else { //u[2] negative 294 rotAx[2] = -rotAx[2]; 295 if( s02 >= 0 ) rotAx[0] = -rotAx[0]; 296 if( s12 >= 0 ) rotAx[1] = -rotAx[1]; 297 } 298 } else { //d1 299 if(d1>=0) {//u[1] positive 300 if( s01 < 0) rotAx[0] = -rotAx[0]; 301 if( s12 < 0) rotAx[2] = -rotAx[2]; 302 } else { //u[1] negative 303 rotAx[1] = -rotAx[1]; 304 if( s01 >= 0) rotAx[0] = -rotAx[0]; 305 if( s12 >= 0) rotAx[2] = -rotAx[2]; 306 } 307 } 308 } else { // not d1 309 if( Math.abs(d0) < Math.abs(d2) ) { //d2 310 if(d2>=0){ //u[2] positive 311 if( s02 < 0 ) rotAx[0] = -rotAx[0]; 312 if( s12 < 0 ) rotAx[1] = -rotAx[1]; 313 } else { //u[2] negative 314 rotAx[2] = -rotAx[2]; 315 if( s02 >= 0 ) rotAx[0] = -rotAx[0]; 316 if( s12 >= 0 ) rotAx[1] = -rotAx[1]; 317 } 318 } else { //d0 319 if(d0>=0) { //u[0] positive 320 if( s01 < 0 ) rotAx[1] = -rotAx[1]; 321 if( s02 < 0 ) rotAx[2] = -rotAx[2]; 322 } else { //u[0] negative 323 rotAx[0] = -rotAx[0]; 324 if( s01 >= 0 ) rotAx[1] = -rotAx[1]; 325 if( s02 >= 0 ) rotAx[2] = -rotAx[2]; 326 } 327 } 328 } 329 330 rotationAxis = new AtomImpl(); 331 rotationAxis.setCoords(rotAx); 332 333 // Calculate screw = (rotationAxis dot translation)*u 334 double dotProduct = Calc.scalarProduct(rotationAxis, translation); 335 336 screwTranslation = Calc.scale(rotationAxis, dotProduct); 337 otherTranslation = Calc.subtract(translation, screwTranslation); 338 339 Atom hypot = Calc.vectorProduct(otherTranslation,rotationAxis); 340 Calc.scaleEquals(hypot,.5/Math.tan(theta/2.0)); 341 342 // Calculate rotation axis position 343 rotationPos = Calc.scaleAdd(.5,otherTranslation, hypot); 344 } 345 346 /** 347 * Handle cases with small angles of rotation 348 * @param rotation 349 * @param translation 350 */ 351 private void calculateTranslationalAxis(Matrix rotation, Atom translation) { 352 // set axis parallel to translation 353 rotationAxis = Calc.scale(translation, 1./Calc.amount(translation)); 354 355 // position is undefined 356 rotationPos = null; 357 358 screwTranslation = translation; 359 otherTranslation = new AtomImpl(); 360 otherTranslation.setCoords(new double[] {0,0,0}); 361 } 362 363 /** 364 * Returns a Jmol script which will display the axis of rotation. This 365 * consists of a cyan arrow along the axis, plus an arc showing the angle 366 * of rotation. 367 * <p> 368 * As the rotation angle gets smaller, the axis of rotation becomes poorly 369 * defined and would need to get farther and farther away from the protein. 370 * This is not particularly useful, so we arbitrarily draw it parallel to 371 * the translation and omit the arc. 372 * @param atoms Some atoms from the protein, used for determining the bounds 373 * of the axis. 374 * 375 * @return The Jmol script, suitable for calls to 376 * {@link org.biojava.nbio.structure.align.gui.jmol.StructureAlignmentJmol#evalString() jmol.evalString()} 377 */ 378 public String getJmolScript(Atom[] atoms){ 379 return getJmolScript(atoms, 0); 380 } 381 382 /** 383 * Find a segment of the axis that covers the specified set of atoms. 384 * <p> 385 * Projects the input atoms onto the rotation axis and returns the bounding 386 * points. 387 * <p> 388 * In the case of a pure translational axis, the axis location is undefined 389 * so the center of mass will be used instead. 390 * @param atoms 391 * @return two points defining the axis segment 392 */ 393 public Pair<Atom> getAxisEnds(Atom[] atoms) { 394 // Project each Atom onto the rotation axis to determine limits 395 double min, max; 396 min = max = Calc.scalarProduct(rotationAxis,atoms[0]); 397 for(int i=1;i<atoms.length;i++) { 398 double prod = Calc.scalarProduct(rotationAxis,atoms[i]); 399 if(prod<min) min = prod; 400 if(prod>max) max = prod; 401 } 402 double uLen = Calc.scalarProduct(rotationAxis,rotationAxis);// Should be 1, but double check 403 min/=uLen; 404 max/=uLen; 405 406 // Project the origin onto the axis. If the axis is undefined, use the center of mass 407 Atom axialPt; 408 if(rotationPos == null) { 409 Atom center = Calc.centerOfMass(atoms); 410 411 // Project center onto the axis 412 Atom centerOnAxis = Calc.scale(rotationAxis, Calc.scalarProduct(center, rotationAxis)); 413 414 // Remainder is projection of origin onto axis 415 axialPt = Calc.subtract(center, centerOnAxis); 416 417 } else { 418 axialPt = rotationPos; 419 } 420 421 // Find end points of the rotation axis to display 422 Atom axisMin = (Atom) axialPt.clone(); 423 Calc.scaleAdd(min, rotationAxis, axisMin); 424 Atom axisMax = (Atom) axialPt.clone(); 425 Calc.scaleAdd(max, rotationAxis, axisMax); 426 427 return new Pair<>(axisMin, axisMax); 428 } 429 /** 430 * Returns a Jmol script which will display the axis of rotation. This 431 * consists of a cyan arrow along the axis, plus an arc showing the angle 432 * of rotation. 433 * <p> 434 * As the rotation angle gets smaller, the axis of rotation becomes poorly 435 * defined and would need to get farther and farther away from the protein. 436 * This is not particularly useful, so we arbitrarily draw it parallel to 437 * the translation and omit the arc. 438 * @param atoms Some atoms from the protein, used for determining the bounds 439 * of the axis. 440 * @param axisID in case of representing more than one axis in the same jmol 441 * panel, indicate the ID number. 442 * 443 * @return The Jmol script, suitable for calls to 444 * {@link org.biojava.nbio.structure.align.gui.jmol.StructureAlignmentJmol#evalString() jmol.evalString()} 445 */ 446 public String getJmolScript(Atom[] atoms, int axisID){ 447 final double width=.5;// width of JMol object 448 final String axisColor = "yellow"; //axis color 449 final String screwColor = "orange"; //screw translation color 450 451 Pair<Atom> endPoints = getAxisEnds(atoms); 452 Atom axisMin = endPoints.getFirst(); 453 Atom axisMax = endPoints.getSecond(); 454 455 StringWriter result = new StringWriter(); 456 457 // set arrow heads to a reasonable length 458 result.append("set defaultDrawArrowScale 2.0;"); 459 460 // draw axis of rotation 461 result.append( 462 String.format("draw ID rot"+axisID+" CYLINDER {%f,%f,%f} {%f,%f,%f} WIDTH %f COLOR %s ;", 463 axisMin.getX(),axisMin.getY(),axisMin.getZ(), 464 axisMax.getX(),axisMax.getY(),axisMax.getZ(), width, axisColor )); 465 466 // draw screw component 467 boolean positiveScrew = Math.signum(rotationAxis.getX()) == Math.signum(screwTranslation.getX()); 468 if( positiveScrew ) { 469 // screw is in the same direction as the axis 470 result.append( String.format( 471 "draw ID screw"+axisID+" VECTOR {%f,%f,%f} {%f,%f,%f} WIDTH %f COLOR %s ;", 472 axisMax.getX(),axisMax.getY(),axisMax.getZ(), 473 screwTranslation.getX(),screwTranslation.getY(),screwTranslation.getZ(), 474 width, screwColor )); 475 } else { 476 // screw is in the opposite direction as the axis 477 result.append( String.format( 478 "draw ID screw"+axisID+" VECTOR {%f,%f,%f} {%f,%f,%f} WIDTH %f COLOR %s ;", 479 axisMin.getX(),axisMin.getY(),axisMin.getZ(), 480 screwTranslation.getX(),screwTranslation.getY(),screwTranslation.getZ(), 481 width, screwColor )); 482 } 483 484 // draw angle of rotation 485 if(rotationPos != null) { 486 result.append(System.getProperty("line.separator")); 487 result.append(String.format("draw ID rotArc"+axisID+" ARC {%f,%f,%f} {%f,%f,%f} {0,0,0} {0,%f,%d} SCALE 500 DIAMETER %f COLOR %s;", 488 axisMin.getX(),axisMin.getY(),axisMin.getZ(), 489 axisMax.getX(),axisMax.getY(),axisMax.getZ(), 490 Math.toDegrees(theta), 491 positiveScrew ? 0 : 1 , // draw at the opposite end from the screw arrow 492 width, axisColor )); 493 } 494 495 return result.toString(); 496 } 497 498 /** 499 * Projects a given point onto the axis of rotation 500 * @param point 501 * @return An atom which lies on the axis, or null if the RotationAxis is purely translational 502 */ 503 public Atom getProjectedPoint(Atom point) { 504 if(rotationPos == null) { 505 // translation only 506 return null; 507 } 508 509 Atom localPoint = Calc.subtract(point, rotationPos); 510 double dot = Calc.scalarProduct(localPoint, rotationAxis); 511 512 Atom localProjected = Calc.scale(rotationAxis, dot); 513 Atom projected = Calc.add(localProjected, rotationPos); 514 return projected; 515 } 516 517 /** 518 * Get the distance from a point to the axis of rotation 519 * @param point 520 * @return The distance to the axis, or NaN if the RotationAxis is purely translational 521 */ 522 public double getProjectedDistance(Atom point) { 523 Atom projected = getProjectedPoint(point); 524 if( projected == null) { 525 // translation only 526 return Double.NaN; 527 } 528 529 530 return Calc.getDistance(point, projected); 531 532 } 533 534 public void rotate(Atom[] atoms, double theta) { 535 Matrix rot = getRotationMatrix(theta); 536 if(rotationPos == null) { 537 // Undefined rotation axis; do nothing 538 return; 539 } 540 Atom negPos; 541 542 negPos = Calc.invert(rotationPos); 543 544 for(Atom a: atoms) { 545 Calc.shift(a, negPos); 546 } 547 Calc.rotate(atoms, rot); 548 for(Atom a: atoms) { 549 Calc.shift(a, rotationPos); 550 } 551 } 552 553 /** 554 * Calculate the rotation angle for a structure 555 * @param afpChain 556 * @return The rotation angle, in radians 557 * @throws StructureException If the alignment doesn't contain any blocks 558 * @throws NullPointerException If the alignment doesn't have a rotation matrix set 559 */ 560 public static double getAngle(AFPChain afpChain) throws StructureException { 561 if(afpChain.getBlockNum() < 1) { 562 throw new StructureException("No aligned residues"); 563 } 564 Matrix rotation = afpChain.getBlockRotationMatrix()[0]; 565 566 if(rotation == null) { 567 throw new NullPointerException("AFPChain does not contain a rotation matrix"); 568 } 569 return getAngle(rotation); 570 } 571 /** 572 * Calculate the rotation angle for a given matrix 573 * @param rotation Rotation matrix 574 * @return The angle, in radians 575 */ 576 public static double getAngle(Matrix rotation) { 577 double c = (rotation.trace()-1)/2.0; //=cos(theta) 578 return Math.acos(c); 579 } 580 581 /** 582 * 583 * @return If the rotation axis is well defined, rather than purely translational 584 */ 585 public boolean isDefined() { 586 return rotationPos != null; 587 } 588}