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}