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.symmetry.core;
022
023import org.biojava.nbio.structure.symmetry.geometry.MomentsOfInertia;
024import org.biojava.nbio.structure.symmetry.geometry.SuperPosition;
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028import javax.vecmath.*;
029import java.util.*;
030
031public class RotationAxisAligner extends AxisAligner{
032        
033        private static final Logger LOGGER = LoggerFactory.getLogger(RotationAxisAligner.class);
034        
035        private static final Vector3d X_AXIS = new Vector3d(1,0,0);
036        private static final Vector3d Y_AXIS = new Vector3d(0,1,0);
037        private static final Vector3d Z_AXIS = new Vector3d(0,0,1);
038
039        private Subunits subunits = null;
040        private RotationGroup rotationGroup = null;
041
042        private Matrix4d transformationMatrix = new Matrix4d();
043        private Matrix4d reverseTransformationMatrix = new Matrix4d();
044        private Vector3d referenceVector = new Vector3d();
045        private Vector3d principalRotationVector = new Vector3d();
046        private Vector3d[] principalAxesOfInertia = null;
047         List<List<Integer>> alignedOrbits = null;
048
049        private Vector3d minBoundary = new Vector3d();
050        private Vector3d maxBoundary = new Vector3d();
051        private double xyRadiusMax = Double.MIN_VALUE;
052
053        boolean modified = true;
054
055        public RotationAxisAligner(QuatSymmetryResults results) {
056                this.subunits = results.getSubunits();
057                this.rotationGroup = results.getRotationGroup();
058
059                if (subunits == null) {
060                        throw new IllegalArgumentException("AxisTransformation: Subunits are null");
061                } else if (rotationGroup == null) {
062                        throw new IllegalArgumentException("AxisTransformation: RotationGroup is null");
063                } else if (subunits.getSubunitCount() == 0) {
064                        throw new IllegalArgumentException("AxisTransformation: Subunits is empty");
065                } else if (rotationGroup.getOrder() == 0) {
066                        throw new IllegalArgumentException("AxisTransformation: RotationGroup is empty");
067                }
068        }
069
070        /* (non-Javadoc)
071         * @see org.biojava.nbio.structure.quaternary.core.AxisAligner#getTransformation()
072         */
073        @Override
074        public String getSymmetry() {
075                run();
076                return rotationGroup.getPointGroup();
077        }
078
079        @Override
080        public Matrix4d getTransformation() {
081                run();
082                return transformationMatrix;
083        }
084
085        @Override
086        public Matrix3d getRotationMatrix() {
087                run();
088                Matrix3d m = new Matrix3d();
089                transformationMatrix.getRotationScale(m);
090                return m;
091        }
092
093        @Override
094        public Matrix4d getReverseTransformation() {
095                run();
096                return reverseTransformationMatrix;
097        }
098
099        @Override
100        public Vector3d getPrincipalRotationAxis() {
101                run();
102                return principalRotationVector;
103        }
104
105        @Override
106        public Vector3d getRotationReferenceAxis() {
107                run();
108                return referenceVector;
109        }
110
111        @Override
112        public Vector3d[] getPrincipalAxesOfInertia() {
113                run();
114                return principalAxesOfInertia;
115        }
116
117        @Override
118        public Vector3d getDimension() {
119                run();
120                Vector3d dimension = new Vector3d();
121                dimension.sub(maxBoundary, minBoundary);
122                dimension.scale(0.5);
123                return dimension;
124        }
125
126        /**
127         * Returns the radius for drawing the minor rotation axis in the xy-plane
128         * @return double radius in xy-plane
129         */
130        @Override
131        public double getRadius() {
132                run();
133                return xyRadiusMax;
134        }
135
136        /**
137         * Returns a transformation matrix transform polyhedra for Cn structures.
138         * The center in this matrix is the geometric center, rather then the centroid.
139         * In Cn structures those are usually not the same.
140         * @return
141         */
142        @Override
143        public Matrix4d getGeometicCenterTransformation() {
144                run();
145
146                Matrix4d geometricCentered = new Matrix4d(reverseTransformationMatrix);
147                geometricCentered.setTranslation(new Vector3d(getGeometricCenter()));
148
149                return geometricCentered;
150        }
151
152        /**
153         * Returns the geometric center of polyhedron. In the case of the Cn
154         * point group, the centroid and geometric center are usually not
155         * identical.
156         * @return
157         */
158        @Override
159        public Point3d getGeometricCenter() {
160                run();
161
162                Point3d geometricCenter = new Point3d();
163                Vector3d translation = new Vector3d();
164                reverseTransformationMatrix.get(translation);
165
166                // calculate adjustment around z-axis and transform adjustment to
167                //  original coordinate frame with the reverse transformation
168                if (rotationGroup.getPointGroup().startsWith("C")) {
169                        Vector3d corr = new Vector3d(0,0, minBoundary.z+getDimension().z);
170                        reverseTransformationMatrix.transform(corr);
171                        geometricCenter.set(corr);
172                }
173
174                geometricCenter.add(translation);
175                return geometricCenter;
176        }
177
178        @Override
179        public Point3d getCentroid() {
180                return new Point3d(subunits.getCentroid());
181        }
182
183        @Override
184        public Subunits getSubunits() {
185                return subunits;
186        }
187
188        public RotationGroup getRotationGroup() {
189                return rotationGroup;
190        }
191
192        @Override
193        public List<List<Integer>> getOrbits() {
194                return alignedOrbits;
195        }
196
197        /**
198         * @return
199         */
200
201        private void run () {
202                if (modified) {
203                        calcPrincipalRotationVector();
204                        calcPrincipalAxes();
205                        // initial alignment with draft reference axis
206                        calcReferenceVector();
207                        calcTransformation();
208
209                        // refine ref. axis for cyclic and dihedral systems
210                        if ((rotationGroup.getPointGroup().startsWith("C") &&
211                                        !rotationGroup.getPointGroup().startsWith("C2")) ||
212                                (rotationGroup.getPointGroup().startsWith("D") &&
213                                                        !rotationGroup.getPointGroup().startsWith("D2"))
214                                        ) {
215                                refineReferenceVector();
216                                calcTransformation();
217                        }
218                        calcReverseTransformation();
219                        calcBoundaries();
220                        if (! rotationGroup.getPointGroup().equals("Helical")) {
221                                calcAlignedOrbits();
222                        }
223                        modified = false;
224                }
225        }
226        /**
227         * Returns a list of orbits (an orbit is a cyclic permutation of subunit indices that are related
228         * by a rotation around the principal rotation axis) ordered from the +z direction to the -z direction (z-depth).
229         * Within an orbit, subunit indices are ordered with a clockwise rotation around the z-axis.
230         * @return list of orbits ordered by z-depth
231         */
232        private void calcAlignedOrbits() {
233                Map<Double, List<Integer>> depthMap = new TreeMap<Double, List<Integer>>();
234                double[] depth = getSubunitZDepth();
235                alignedOrbits = calcOrbits();
236
237                // calculate the mean depth of orbit along z-axis
238                for (List<Integer> orbit: alignedOrbits) {
239                        // calculate the mean depth along z-axis for each orbit
240                        double meanDepth = 0;
241                        for (int subunit: orbit) {
242                                meanDepth += depth[subunit];
243                        }
244                        meanDepth /= orbit.size();
245
246                        if (depthMap.get(meanDepth) != null) {
247                                // System.out.println("Conflict in depthMap");
248                                meanDepth += 0.01;
249                        }
250                        depthMap.put(meanDepth, orbit);
251                }
252
253                // now fill orbits back into list ordered by depth
254                alignedOrbits.clear();
255                for (List<Integer> orbit: depthMap.values()) {
256                        // order subunit in a clockwise rotation around the z-axis
257                        /// starting at the 12 O-clock position (+y position)
258                        alignWithReferenceAxis(orbit);
259                        alignedOrbits.add(orbit);
260                }
261        }
262
263        /**
264         * Returns an ordered list of subunit ids (orbit) in such a way that the subunit
265         * indices start at the 12 o-clock (+y axis) and proceed in a clockwise direction
266         * to the 11 o-clock position to close the "orbit".
267         *
268         * @param orbit list of subunit indices that are transformed into each other by a rotation
269         * @return list of subunit indices ordered in a clockwise direction
270         */
271
272        private List<Integer> alignWithReferenceAxis(List<Integer> orbit) {
273                int n = subunits.getSubunitCount();
274                int fold = rotationGroup.getRotation(0).getFold();
275                if (fold < 2) {
276                        return orbit;
277                }
278                Vector3d probe = new Vector3d();
279
280                double dotMin1 = Double.MIN_VALUE;
281                double dotMin2 = Double.MIN_VALUE;
282                int index1 = 0;
283                int index2 = 0;
284                Vector3d Y1 = new Vector3d(0,1,0);
285                Vector3d Y2 = new Vector3d(0,1,0);
286                Matrix3d m = new Matrix3d();
287                double angle = -2*Math.PI/fold;
288                m.rotZ(0.1*angle); // add small offset, since two subunits may be equidistant to the y-axis
289                m.transform(Y1);
290                m.rotZ(1.1*angle);
291                m.transform(Y2);
292                // transform subunit centers into z-aligned position and calculate
293                // width in xy direction.
294                for (int i: orbit) {
295                        Point3d p = subunits.getCenters().get(i);
296                        probe.set(p);
297                        transformationMatrix.transform(probe);
298                        // find subunit that lines up with y-axis
299                        double dot1 = Y1.dot(probe);
300                        if (dot1 > dotMin1) {
301                                dotMin1 = dot1;
302                                index1 = i;
303                        }
304                        // find next subunit (rotated by one fold around z-axis - clockwise)
305                        double dot2 = Y2.dot(probe);
306                        if (dot2 > dotMin2) {
307                                dotMin2 = dot2;
308                                index2 = i;
309                        }
310                }
311//              System.out.println("Index1/2: " + index1 + " - " + index2);
312//              System.out.println("Orbit0: " + orbit);
313                // order subunit indices in a clockwise orientation around the z-axis
314                // bring subunit into position 0
315                for (int i = 0; i < n; i++) {
316                        if (orbit.get(0) == index1) {
317                                break;
318                        }
319                        Collections.rotate(orbit,1);
320                }
321//              System.out.println("Orbit1: " + orbit);
322                // bring second subunit  onto position 1
323                if (orbit.get(1) == index2) {
324                        return orbit;
325                }
326                Collections.reverse(orbit.subList(1,  orbit.size()));
327                if (orbit.get(1) != index2) {
328                        LOGGER.warn("alignWithReferenceAxis failed");
329                }
330//              System.out.println("Orbit2: " + orbit);
331                return orbit;
332        }
333
334
335
336        private void calcTransformation() {
337                if (rotationGroup.getPointGroup().equals("C1")) {
338                        calcTransformationByInertiaAxes();
339                } else {
340                        calcTransformationBySymmetryAxes();
341                }
342                // make sure this value is zero. On Linux this value is 0.
343                transformationMatrix.setElement(3, 3, 1.0);
344        }
345
346        private void calcReverseTransformation() {
347                reverseTransformationMatrix.invert(transformationMatrix);
348        }
349
350        private void calcTransformationBySymmetryAxes() {
351                Vector3d[] axisVectors = new Vector3d[2];
352                axisVectors[0] = new Vector3d(principalRotationVector);
353                axisVectors[1] = new Vector3d(referenceVector);
354
355                //  y,z axis centered at the centroid of the subunits
356                Vector3d[] referenceVectors = new Vector3d[2];
357                referenceVectors[0] = new Vector3d(Z_AXIS);
358                referenceVectors[1] = new Vector3d(Y_AXIS);
359
360                transformationMatrix = alignAxes(axisVectors, referenceVectors);
361
362                // combine with translation
363                Matrix4d combined = new Matrix4d();
364                combined.setIdentity();
365                Vector3d trans = new Vector3d(subunits.getCentroid());
366                trans.negate();
367                combined.setTranslation(trans);
368                transformationMatrix.mul(combined);
369
370                // for cyclic geometry, set a canonical view for the Z direction
371                if (rotationGroup.getPointGroup().startsWith("C")) {
372                        calcZDirection();
373                }
374        }
375
376        private void calcTransformationByInertiaAxes() {
377                Vector3d[] axisVectors = new Vector3d[2];
378                axisVectors[0] = new Vector3d(principalAxesOfInertia[0]);
379                axisVectors[1] = new Vector3d(principalAxesOfInertia[1]);
380
381                Vector3d[] referenceVectors = new Vector3d[2];
382                referenceVectors[0] = new Vector3d(Y_AXIS);
383                referenceVectors[1] = new Vector3d(X_AXIS);
384
385                // align inertia axes with y-x plane
386                transformationMatrix = alignAxes(axisVectors, referenceVectors);
387
388                // combine with translation
389                Matrix4d translation = new Matrix4d();
390                translation.setIdentity();
391                Vector3d trans = new Vector3d(subunits.getCentroid());
392                trans.negate();
393                translation.setTranslation(trans);
394                transformationMatrix.mul(translation);
395        }
396
397        /**
398         * Returns a transformation matrix that rotates refPoints to match
399         * coordPoints
400         * @param refPoints the points to be aligned
401         * @param referenceVectors
402         * @return
403         */
404        private Matrix4d alignAxes(Vector3d[] axisVectors, Vector3d[] referenceVectors) {
405                Matrix4d m1 = new Matrix4d();
406                AxisAngle4d a = new AxisAngle4d();
407                Vector3d axis = new Vector3d();
408
409                // calculate rotation matrix to rotate refPoints[0] into coordPoints[0]
410                Vector3d v1 = new Vector3d(axisVectors[0]);
411                Vector3d v2 = new Vector3d(referenceVectors[0]);
412                double dot = v1.dot(v2);
413                if (Math.abs(dot) < 0.999) {
414                        axis.cross(v1,v2);
415                        axis.normalize();
416                        a.set(axis, v1.angle(v2));
417                        m1.set(a);
418                        // make sure matrix element m33 is 1.0. It's 0 on Linux.
419                        m1.setElement(3,  3, 1.0);
420                } else if (dot > 0) {
421                        // parallel axis, nothing to do -> identity matrix
422                        m1.setIdentity();
423                } else if (dot < 0) {
424                        // anti-parallel axis, flip around x-axis
425                        m1.set(flipX());
426                }
427
428                // apply transformation matrix to all refPoints
429                m1.transform(axisVectors[0]);
430                m1.transform(axisVectors[1]);
431
432                // calculate rotation matrix to rotate refPoints[1] into coordPoints[1]
433                v1 = new Vector3d(axisVectors[1]);
434                v2 = new Vector3d(referenceVectors[1]);
435                Matrix4d m2 = new Matrix4d();
436                dot = v1.dot(v2);
437                if (Math.abs(dot) < 0.999) {
438                        axis.cross(v1,v2);
439                        axis.normalize();
440                        a.set(axis, v1.angle(v2));
441                        m2.set(a);
442                        // make sure matrix element m33 is 1.0. It's 0 on Linux.
443                        m2.setElement(3,  3, 1.0);
444                } else if (dot > 0) {
445                        // parallel axis, nothing to do -> identity matrix
446                        m2.setIdentity();
447                } else if (dot < 0) {
448                        // anti-parallel axis, flip around z-axis
449                        m2.set(flipZ());
450                }
451
452                // apply transformation matrix to all refPoints
453                m2.transform(axisVectors[0]);
454                m2.transform(axisVectors[1]);
455
456                // combine the two rotation matrices
457                m2.mul(m1);
458
459                // the RMSD should be close to zero
460                Point3d[] axes = new Point3d[2];
461                axes[0] = new Point3d(axisVectors[0]);
462                axes[1] = new Point3d(axisVectors[1]);
463                Point3d[] ref = new Point3d[2];
464                ref[0] = new Point3d(referenceVectors[0]);
465                ref[1] = new Point3d(referenceVectors[1]);
466                if (SuperPosition.rmsd(axes, ref) > 0.1) {
467                        LOGGER.info("AxisTransformation: axes alignment is off. RMSD: " + SuperPosition.rmsd(axes, ref));
468                }
469
470                return m2;
471        }
472
473        private void calcPrincipalAxes() {
474                MomentsOfInertia moi = new MomentsOfInertia();
475
476                for (Point3d[] list: subunits.getTraces()) {
477                        for (Point3d p: list) {
478                                moi.addPoint(p, 1.0);
479                        }
480                }
481                principalAxesOfInertia = moi.getPrincipalAxes();
482        }
483
484        /**
485         * Calculates the min and max boundaries of the structure after it has been
486         * transformed into its canonical orientation.
487         */
488        private void calcBoundaries() {
489                minBoundary.x = Double.MAX_VALUE;
490                maxBoundary.x = Double.MIN_VALUE;
491                minBoundary.y = Double.MAX_VALUE;
492                maxBoundary.x = Double.MIN_VALUE;
493                minBoundary.z = Double.MAX_VALUE;
494                maxBoundary.z = Double.MIN_VALUE;
495
496                Point3d probe = new Point3d();
497
498                for (Point3d[] list: subunits.getTraces()) {
499                        for (Point3d p: list) {
500                                probe.set(p);
501                                transformationMatrix.transform(probe);
502
503                                minBoundary.x = Math.min(minBoundary.x, probe.x);
504                                maxBoundary.x = Math.max(maxBoundary.x, probe.x);
505                                minBoundary.y = Math.min(minBoundary.y, probe.y);
506                                maxBoundary.y = Math.max(maxBoundary.y, probe.y);
507                                minBoundary.z = Math.min(minBoundary.z, probe.z);
508                                maxBoundary.z = Math.max(maxBoundary.z, probe.z);
509                                xyRadiusMax = Math.max(xyRadiusMax, Math.sqrt(probe.x*probe.x + probe.y * probe.y));
510                        }
511                }
512        }
513
514        /*
515         * Modifies the rotation part of the transformation axis for
516         * a Cn symmetric complex, so that the narrower end faces the
517         * viewer, and the wider end faces away from the viewer. Example: 3LSV
518         */
519        private void calcZDirection() {
520                calcBoundaries();
521
522                // if the longer part of the structure faces towards the back (-z direction),
523                // rotate around y-axis so the longer part faces the viewer (+z direction)
524                if (Math.abs(minBoundary.z) > Math.abs(maxBoundary.z)) {
525                        Matrix4d rot = flipY();
526                        rot.mul(transformationMatrix);
527                        transformationMatrix.set(rot);
528                }
529        }
530
531        /**
532         *
533         */
534        private List<List<Integer>> getOrbitsByXYWidth() {
535                Map<Double, List<Integer>> widthMap = new TreeMap<Double, List<Integer>>();
536                double[] width = getSubunitXYWidth();
537                List<List<Integer>> orbits = calcOrbits();
538
539                // calculate the mean width of orbits in XY-plane
540                for (List<Integer> orbit: orbits) {
541                        double meanWidth = 0;
542                        for (int subunit: orbit) {
543                                meanWidth += width[subunit];
544                        }
545                        meanWidth /= orbit.size();
546
547                        if (widthMap.get(meanWidth) != null) {
548                                meanWidth += 0.01;
549                        }
550                        widthMap.put(meanWidth, orbit);
551                }
552
553                // now fill orbits back into list ordered by width
554                orbits.clear();
555                for (List<Integer> orbit: widthMap.values()) {
556                        orbits.add(orbit);
557                }
558                return orbits;
559        }
560
561        private double[] getSubunitXYWidth() {
562                int n = subunits.getSubunitCount();
563                double[] width = new double[n];
564                Point3d probe = new Point3d();
565
566                // transform subunit centers into z-aligned position and calculate
567                // width in xy direction.
568                for (int i = 0; i < n; i++) {
569                        width[i] = Double.MIN_VALUE;
570                        for (Point3d p: subunits.getTraces().get(i)) {
571                                probe.set(p);
572                                transformationMatrix.transform(probe);
573                                width[i] = Math.max(width[i], Math.sqrt(probe.x*probe.x + probe.y*probe.y));
574                        }
575                }
576                return width;
577        }
578
579        private double[] getSubunitZDepth() {
580                int n = subunits.getSubunitCount();
581                double[] depth = new double[n];
582                Point3d probe = new Point3d();
583
584                // transform subunit centers into z-aligned position and calculate
585                // z-coordinates (depth) along the z-axis.
586                for (int i = 0; i < n; i++) {
587                        Point3d p= subunits.getCenters().get(i);
588                        probe.set(p);
589                        transformationMatrix.transform(probe);
590                        depth[i] = probe.z;
591                }
592                return depth;
593        }
594
595        /**
596         * Returns a list of list of subunit ids that form an "orbit", i.e. they
597         * are transformed into each other during a rotation around the principal symmetry axis (z-axis)
598         * @return
599         */
600        private List<List<Integer>> calcOrbits() {
601                int n = subunits.getSubunitCount();
602                int fold = rotationGroup.getRotation(0).getFold();
603
604                List<List<Integer>> orbits = new ArrayList<List<Integer>>();
605                boolean[] used = new boolean[n];
606                Arrays.fill(used, false);
607
608                for (int i = 0; i < n; i++) {
609                        if (! used[i]) {
610                                // determine the equivalent subunits
611                                List<Integer> orbit = new ArrayList<Integer>(fold);
612                                for (int j = 0; j < fold; j++) {
613                                        List<Integer> permutation = rotationGroup.getRotation(j).getPermutation();
614                                        orbit.add(permutation.get(i));
615                                        used[permutation.get(i)] = true;
616                                }
617                                orbits.add(deconvolute(orbit));
618                        }
619                }
620                return orbits;
621        }
622
623        private List<Integer> deconvolute(List<Integer> orbit) {
624                if (rotationGroup.getOrder() < 2) {
625                        return orbit;
626                }
627                List<Integer> p0 = rotationGroup.getRotation(0).getPermutation();
628                List<Integer> p1 = rotationGroup.getRotation(1).getPermutation();
629//              System.out.println("deconvolute");
630//              System.out.println("Permutation0: " + p0);
631//              System.out.println("Permutation1: " + p1);
632
633                List<Integer> inRotationOrder = new ArrayList<Integer>(orbit.size());
634                inRotationOrder.add(orbit.get(0));
635                for (int i = 1; i < orbit.size(); i++) {
636                        int current = inRotationOrder.get(i-1);
637                        int index = p0.indexOf(current);
638                        int next = p1.get(index);
639                        if (!orbit.contains(next)) {
640                                LOGGER.warn("deconvolute: inconsistency in permuation. Returning original order");
641                                return orbit;
642                        }
643                        inRotationOrder.add(next);
644                }
645//              System.out.println("In order: " + inRotationOrder);
646                return inRotationOrder;
647        }
648
649        /**
650         * Returns a vector along the principal rotation axis for the
651         * alignment of structures along the z-axis
652         * @return principal rotation vector
653         */
654        private void calcPrincipalRotationVector() {
655                Rotation rotation = rotationGroup.getRotation(0); // the rotation around the principal axis is the first rotation
656                AxisAngle4d axisAngle = rotation.getAxisAngle();
657                principalRotationVector = new Vector3d(axisAngle.x, axisAngle.y, axisAngle.z);
658        }
659
660        /**
661         * Returns a vector perpendicular to the principal rotation vector
662         * for the alignment of structures in the xy-plane
663         * @return reference vector
664         */
665        private void calcReferenceVector() {
666                referenceVector = null;
667                if (rotationGroup.getPointGroup().startsWith("C")) {
668                        referenceVector = getReferenceAxisCylic();
669                } else if (rotationGroup.getPointGroup().startsWith("D")) {
670                        referenceVector = getReferenceAxisDihedral();
671                } else if (rotationGroup.getPointGroup().equals("T")) {
672                        referenceVector = getReferenceAxisTetrahedral();
673                } else if (rotationGroup.getPointGroup().equals("O")) {
674                        referenceVector = getReferenceAxisOctahedral();
675                } else if (rotationGroup.getPointGroup().equals("I")) {
676                        referenceVector = getReferenceAxisIcosahedral();
677                } else if (rotationGroup.getPointGroup().equals("Helical")) {
678                        // TODO what should the reference vector be??
679                        referenceVector = getReferenceAxisCylic();
680                }
681
682                if (referenceVector == null) {
683                        LOGGER.warn("no reference vector found. Using y-axis.");
684                        referenceVector = new Vector3d(Y_AXIS);
685                }
686                // make sure reference vector is perpendicular principal roation vector
687                referenceVector = orthogonalize(principalRotationVector, referenceVector);
688        }
689
690        /**
691         * Returns a normalized vector that represents a minor rotation axis, except
692         * for Cn, this represents an axis orthogonal to the principal axis.
693         * @return minor rotation axis
694         */
695        private void refineReferenceVector() {
696                referenceVector = new Vector3d(Y_AXIS);
697                if (rotationGroup.getPointGroup().startsWith("C")) {
698                        referenceVector = getReferenceAxisCylicWithSubunitAlignment();
699                } else if (rotationGroup.getPointGroup().startsWith("D")) {
700                        referenceVector = getReferenceAxisDihedralWithSubunitAlignment();
701                }
702
703                referenceVector = orthogonalize(principalRotationVector, referenceVector);
704        }
705
706        private Vector3d orthogonalize(Vector3d vector1, Vector3d vector2) {
707                double dot = vector1.dot(vector2);
708                Vector3d ref = new Vector3d(vector2);
709//              System.out.println("p.r: " + dot);
710//              System.out.println("Orig refVector: " + referenceVector);
711                if (dot < 0) {
712                        vector2.negate();
713                }
714                vector2.cross(vector1, vector2);
715//              System.out.println("Intermed. refVector: " + vector2);
716                vector2.normalize();
717//              referenceVector.cross(referenceVector, principalRotationVector);
718                vector2.cross(vector1, vector2);
719                vector2.normalize();
720                if (ref.dot(vector2) < 0) {
721                        vector2.negate();
722                }
723//              System.out.println("Mod. refVector: " + vector2);
724                return vector2;
725        }
726        /**
727         * Returns the default reference vector for the alignment of Cn structures
728         * @return
729         */
730        private Vector3d getReferenceAxisCylic() {
731                if (rotationGroup.getPointGroup().equals("C2")) {
732                        Vector3d vr = new Vector3d(subunits.getOriginalCenters().get(0));
733                        vr.sub(subunits.getCentroid());
734                        vr.normalize();
735                        return vr;
736                }
737
738                // get principal axis vector that is perpendicular to the principal
739                // rotation vector
740                Vector3d vmin = null;
741                double dotMin = 1.0;
742                for (Vector3d v: principalAxesOfInertia) {
743                        if (Math.abs(principalRotationVector.dot(v)) < dotMin) {
744                                dotMin = Math.abs(principalRotationVector.dot(v));
745                                vmin = new Vector3d(v);
746                        }
747                }
748                if (principalRotationVector.dot(vmin) < 0) {
749                        vmin.negate();
750                }
751
752                return vmin;
753        }
754
755
756        /**
757         * Returns a reference vector for the alignment of Cn structures.
758         * @return reference vector
759         */
760        private Vector3d getReferenceAxisCylicWithSubunitAlignment() {
761                if (rotationGroup.getPointGroup().equals("C2")) {
762                        return referenceVector;
763                }
764
765                // find subunit that extends the most in the xy-plane
766                List<List<Integer>> orbits = getOrbitsByXYWidth();
767                // get the last orbit which is the widest
768                List<Integer> widestOrbit = orbits.get(orbits.size()-1);
769                List<Point3d> centers = subunits.getCenters();
770                int subunit = widestOrbit.get(0);
771
772                // calculate reference vector
773                Vector3d refAxis = new Vector3d();
774                refAxis.sub(centers.get(subunit), subunits.getCentroid());
775                refAxis.normalize();
776                return refAxis;
777        }
778
779        /**
780         *
781         */
782        private Vector3d getReferenceAxisDihedralWithSubunitAlignment() {
783                int maxFold = rotationGroup.getRotation(0).getFold();
784
785                double minAngle = Double.MAX_VALUE;
786                Vector3d refVec = null;
787
788                Vector3d ref = getSubunitReferenceVector();
789
790                for (int i = 0; i < rotationGroup.getOrder(); i++) {
791                        if (rotationGroup.getRotation(i).getDirection() == 1 &&
792                                        (rotationGroup.getRotation(i).getFold() < maxFold) ||
793                                        rotationGroup.getPointGroup().equals("D2")) {
794
795                                AxisAngle4d axisAngle = rotationGroup.getRotation(i).getAxisAngle();
796                                Vector3d v = new Vector3d(axisAngle.x, axisAngle.y, axisAngle.z);
797                                v.normalize();
798
799//                              System.out.println("Ref axis angle(+): " + Math.toDegrees(v.angle(ref)));
800                                double angle =  v.angle(ref);
801                                if (angle < minAngle) {
802                                        minAngle = angle;
803                                        refVec = v;
804                                }
805                                Vector3d vn = new Vector3d(v);
806                                vn.negate();
807//                              System.out.println("Ref axis angle(-): " + Math.toDegrees(vn.angle(ref)));
808                                angle =  vn.angle(ref);
809                                if (angle < minAngle) {
810                                        minAngle = angle;
811                                        refVec = vn;
812                                }
813                        }
814                }
815                refVec.normalize();
816                return refVec;
817        }
818
819        /**
820         *
821         */
822        private Vector3d getReferenceAxisDihedral() {
823                int maxFold = rotationGroup.getRotation(0).getFold();
824                // one exception: D2
825                if (maxFold == 2) {
826                        maxFold = 3;
827                }
828                // TODO how about D2, where minor axis = 2 = principal axis??
829                for (int i = 0; i < rotationGroup.getOrder(); i++) {
830                        if (rotationGroup.getRotation(i).getDirection() == 1 && rotationGroup.getRotation(i).getFold() < maxFold) {
831                                AxisAngle4d axisAngle = rotationGroup.getRotation(i).getAxisAngle();
832                                Vector3d v = new Vector3d(axisAngle.x, axisAngle.y, axisAngle.z);
833                                v.normalize();
834                                return v;
835                        }
836                }
837                return null;
838        }
839
840        private Vector3d getReferenceAxisTetrahedral() {
841                for (int i = 0; i < rotationGroup.getOrder(); i++) {
842                                AxisAngle4d axisAngle = rotationGroup.getRotation(i).getAxisAngle();
843                                Vector3d v = new Vector3d(axisAngle.x, axisAngle.y, axisAngle.z);
844                                double d = v.dot(principalRotationVector);
845                                if (rotationGroup.getRotation(i).getFold() == 3) {
846                                        // the dot product 0 is between to adjacent 3-fold axes
847                                        if (d > 0.3 && d < 0.9) {
848                                                return v;
849                                        }
850                                }
851                }
852                return null;
853        }
854
855        private Vector3d getReferenceAxisOctahedral() {
856                for (int i = 0; i < rotationGroup.getOrder(); i++) {
857                                AxisAngle4d axisAngle = rotationGroup.getRotation(i).getAxisAngle();
858                                Vector3d v = new Vector3d(axisAngle.x, axisAngle.y, axisAngle.z);
859                                double d = v.dot(principalRotationVector);
860                                if (rotationGroup.getRotation(i).getFold() == 4) {
861                                        // the dot product 0 is between to adjacent 4-fold axes
862                                        if (d > -0.1 && d < 0.1 ) {
863                                                return v;
864                                        }
865                                }
866                }
867                return null;
868        }
869
870        private Vector3d getReferenceAxisIcosahedral() {
871                for (int i = 0; i < rotationGroup.getOrder(); i++) {
872                                AxisAngle4d axisAngle = rotationGroup.getRotation(i).getAxisAngle();
873                                Vector3d v = new Vector3d(axisAngle.x, axisAngle.y, axisAngle.z);
874                                double d = v.dot(principalRotationVector);
875                                if (rotationGroup.getRotation(i).getFold() == 5) {
876                                        // the dot product of 0.447.. is between to adjacent 5-fold axes
877//                                      if (d > 0.447 && d < 0.448) {
878                                        if (d > 0.4 && d < 0.5) {
879                                                return v;
880                                        }
881                                }
882                }
883                return null;
884        }
885
886        private Vector3d getSubunitReferenceVector() {
887                        int n = subunits.getSubunitCount();
888                        Point3d probe = new Point3d();
889
890                        // transform subunit centers into z-aligned position and calculate
891                        // width in xy direction.
892                        double maxWidthSq = 0;
893                        Point3d ref = null;
894                        for (int i = 0; i < n; i++) {
895                                for (Point3d p: subunits.getTraces().get(i)) {
896                                        probe.set(p);
897                                        transformationMatrix.transform(probe);
898                                        double widthSq = probe.x*probe.x + probe.y*probe.y;
899                                        if (widthSq > maxWidthSq) {
900                                                maxWidthSq = widthSq;
901                                                ref = p;
902                                        }
903                                }
904                        }
905        //              System.out.println("width: " + maxWidthSq);
906                        Vector3d refVector = new Vector3d();
907                        refVector.sub(ref, subunits.getCentroid());
908                        refVector.normalize();
909                        return refVector;
910                }
911
912        private static Matrix4d flipX() {
913                Matrix4d rot = new Matrix4d();
914                rot.m00 = 1;
915                rot.m11 = -1;
916                rot.m22 = -1;
917                rot.m33 = 1;
918                return rot;
919        }
920
921        private static Matrix4d flipY() {
922                Matrix4d rot = new Matrix4d();
923                rot.m00 = -1;
924                rot.m11 = 1;
925                rot.m22 = -1;
926                rot.m33 = 1;
927                return rot;
928        }
929
930        private static Matrix4d flipZ() {
931                Matrix4d rot = new Matrix4d();
932                rot.m00 = -1;
933                rot.m11 = -1;
934                rot.m22 = 1;
935                rot.m33 = 1;
936                return rot;
937        }
938
939}