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