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.xtal;
022
023import javax.vecmath.Matrix3d;
024import javax.vecmath.Matrix4d;
025import javax.vecmath.Point3i;
026import javax.vecmath.Vector3d;
027import java.io.Serializable;
028import java.util.Locale;
029
030import static java.lang.Math.abs;
031
032
033/**
034 * Representation of a transformation in a crystal:
035 * - a transformation id (each of the transformations in a space group, 0 to m)
036 * - a crystal translation
037 * The transformation matrix in crystal basis is stored, representing the basic
038 * transformation together with the crystal translation.
039 * Contains methods to check for equivalent transformations.
040 *
041 *
042 * @author duarte_j
043 *
044 */
045public class CrystalTransform implements Serializable {
046
047        private static final long serialVersionUID = 1L;
048
049
050        public static final Matrix4d IDENTITY = new Matrix4d(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1);
051
052        /**
053         * The space group to which this transform belongs
054         */
055        private final SpaceGroup sg;
056
057        /**
058         * The transform id corresponding to the SpaceGroup's transform indices.
059         * From 0 (identity) to m (m=number of symmetry operations of the space group)
060         * It is unique within the unit cell but equivalent units of different crystal unit cells
061         * will have same id
062         */
063        private int transformId;
064
065        /**
066         * The 4-dimensional matrix transformation in crystal basis.
067         * Note that the translational component of this matrix is not necessarily
068         * identical to crystalTranslation since some operators have fractional
069         * translations within the cell
070         */
071        private Matrix4d matTransform;
072
073        /**
074         * The crystal translation (always integer)
075         */
076        private Point3i crystalTranslation;
077
078
079        /**
080         * Creates a new CrystalTransform representing the identity transform
081         * in cell (0,0,0)
082         */
083        public CrystalTransform(SpaceGroup sg) {
084                this.sg = sg;
085                this.transformId = 0;
086                this.matTransform = (Matrix4d)IDENTITY.clone();
087                this.crystalTranslation = new Point3i(0,0,0);
088        }
089
090        /**
091         * Represents the n-th transform
092         * @param sg
093         * @param transformId
094         */
095        public CrystalTransform(SpaceGroup sg, int transformId) {
096                this.sg = sg;
097                this.transformId = transformId;
098                if (sg==null && transformId==0) {
099                        this.matTransform = (Matrix4d)IDENTITY.clone();
100                } else if (sg==null) {
101                        throw new IllegalArgumentException("Space Group cannot be null if transformId!=0");
102                } else {
103                        this.matTransform = (Matrix4d)sg.getTransformation(transformId).clone();
104                }
105                this.crystalTranslation = new Point3i(0,0,0);
106        }
107
108        /**
109         * Copy constructor
110         * @param transform
111         */
112        public CrystalTransform(CrystalTransform transform) {
113                this.sg = transform.sg;
114                this.transformId = transform.transformId;
115                this.matTransform = new Matrix4d(transform.matTransform);
116                this.crystalTranslation = new Point3i(transform.crystalTranslation);
117        }
118
119        public Matrix4d getMatTransform() {
120                return matTransform;
121        }
122
123        public void setMatTransform(Matrix4d matTransform) {
124                this.matTransform = matTransform;
125        }
126
127        public Point3i getCrystalTranslation() {
128                return crystalTranslation;
129        }
130
131        public void translate(Point3i translation) {
132                matTransform.m03 = matTransform.m03+translation.x;
133                matTransform.m13 = matTransform.m13+translation.y;
134                matTransform.m23 = matTransform.m23+translation.z;
135
136                crystalTranslation.add(translation);
137
138        }
139
140        /**
141         * Returns true if the given CrystalTransform is equivalent to this one.
142         * Two crystal transforms are equivalent if one is the inverse of the other, i.e.
143         * their transformation matrices multiplication is equal to the identity.
144         * @param other
145         * @return
146         */
147        public boolean isEquivalent(CrystalTransform other) {
148                Matrix4d mul = new Matrix4d();
149                mul.mul(this.matTransform,other.matTransform);
150
151                if (mul.epsilonEquals(IDENTITY, 0.0001)) {
152                        return true;
153                }
154                return false;
155        }
156
157        /**
158         * Tells whether this transformation is a pure crystal lattice translation,
159         * i.e. no rotational component and an integer translation vector.
160         * @return
161         */
162        public boolean isPureCrystalTranslation() {
163                return (transformId==0 && (crystalTranslation.x!=0 || crystalTranslation.y!=0 || crystalTranslation.z!=0));
164        }
165
166        /**
167         * Tells whether this transformation is the identity: no rotation and no translation
168         * @return
169         */
170        public boolean isIdentity() {
171                return (transformId==0 && crystalTranslation.x==0 && crystalTranslation.y==0 && crystalTranslation.z==0);
172        }
173
174        /**
175         * Tells whether this transformation is a pure translation:
176         * either a pure crystal (lattice) translation or a fractional (within
177         * unit cell) translation: space groups Ixxx, Cxxx, Fxxx have operators
178         * with fractional translations within the unit cell.
179         * @return
180         */
181        public boolean isPureTranslation() {
182                if (isPureCrystalTranslation()) return true;
183                if (isPureMatrixTranslation()) return true;
184                return false;
185        }
186
187        /**
188         * This method will help check if the matrix translation is pure or not.
189         * @return boolean
190         */
191        private boolean isPureMatrixTranslation(){
192                return  SpaceGroup.deltaComp(matTransform.m00,1,SpaceGroup.DELTA) &&
193                                SpaceGroup.deltaComp(matTransform.m01,0,SpaceGroup.DELTA) &&
194                                SpaceGroup.deltaComp(matTransform.m02,0,SpaceGroup.DELTA) &&
195
196                                SpaceGroup.deltaComp(matTransform.m10,0,SpaceGroup.DELTA) &&
197                                SpaceGroup.deltaComp(matTransform.m11,1,SpaceGroup.DELTA) &&
198                                SpaceGroup.deltaComp(matTransform.m12,0,SpaceGroup.DELTA) &&
199
200                                SpaceGroup.deltaComp(matTransform.m20,0,SpaceGroup.DELTA) &&
201                                SpaceGroup.deltaComp(matTransform.m21,0,SpaceGroup.DELTA) &&
202                                SpaceGroup.deltaComp(matTransform.m22,1,SpaceGroup.DELTA) &&
203                                (Math.abs(matTransform.m03-0.0)>SpaceGroup.DELTA || Math.abs(matTransform.m13-0.0)>SpaceGroup.DELTA || Math.abs(matTransform.m23-0.0)>SpaceGroup.DELTA);
204        }
205
206        /**
207         * Tells whether this transformation contains a fractional translational
208         * component (whatever its rotational component). A fractional translation
209         * together with a rotation means a screw axis.
210         * @return
211         */
212        public boolean isFractionalTranslation() {
213                if ((Math.abs(matTransform.m03-crystalTranslation.x)>SpaceGroup.DELTA) ||
214                        (Math.abs(matTransform.m13-crystalTranslation.y)>SpaceGroup.DELTA) ||
215                        (Math.abs(matTransform.m23-crystalTranslation.z)>SpaceGroup.DELTA)) {
216                        return true;
217                }
218                return false;
219        }
220
221        /**
222         * Tells whether this transformation is a rotation disregarding the translational component,
223         * i.e. either pure rotation or screw rotation, but not improper rotation.
224         * @return
225         */
226        public boolean isRotation() {
227                // if no SG, that means a non-crystallographic entry (e.g. NMR). We return false
228                if (sg==null) return false;
229
230                // this also takes care of case <0 (improper rotations): won't be considered as rotations
231                if (sg.getAxisFoldType(this.transformId)>1) return true;
232
233                return false;
234        }
235
236        /**
237         * Returns the TransformType of this transformation: AU, crystal translation, fractional translation
238         * , 2 3 4 6-fold rotations, 2 3 4 6-fold screw rotations, -1 -3 -2 -4 -6 inversions/rotoinversions.
239         * @return
240         */
241        public TransformType getTransformType() {
242
243                // if no SG, that means a non-crystallographic entry (e.g. NMR). We return AU as type
244                if (sg==null) return TransformType.AU;
245
246                int foldType = sg.getAxisFoldType(this.transformId);
247                boolean isScrewOrGlide = false;
248                Vector3d translScrewComponent = getTranslScrewComponent();
249                if (Math.abs(translScrewComponent.x-0.0)>SpaceGroup.DELTA ||
250                        Math.abs(translScrewComponent.y-0.0)>SpaceGroup.DELTA ||
251                        Math.abs(translScrewComponent.z-0.0)>SpaceGroup.DELTA) {
252
253                        isScrewOrGlide = true;
254                }
255
256                if (foldType>1) {
257
258                        if (isScrewOrGlide) {
259                                switch (foldType) {
260                                case 2:
261                                        return TransformType.TWOFOLDSCREW;
262                                case 3:
263                                        return TransformType.THREEFOLDSCREW;
264                                case 4:
265                                        return TransformType.FOURFOLDSCREW;
266                                case 6:
267                                        return TransformType.SIXFOLDSCREW;
268                                default:
269                                        throw new NullPointerException("This transformation did not fall into any of the known types! This is most likely a bug.");
270                                }
271                        } else {
272                                switch (foldType) {
273                                case 2:
274                                        return TransformType.TWOFOLD;
275                                case 3:
276                                        return TransformType.THREEFOLD;
277                                case 4:
278                                        return TransformType.FOURFOLD;
279                                case 6:
280                                        return TransformType.SIXFOLD;
281                                default:
282                                        throw new NullPointerException("This transformation did not fall into any of the known types! This is most likely a bug.");
283                                }
284                        }
285
286                } else if (foldType<0) {
287                        switch (foldType) {
288                        case -1:
289                                return TransformType.ONEBAR;
290                        case -2:
291                                if (isScrewOrGlide) {
292                                        return TransformType.GLIDE;
293                                }
294                                return TransformType.TWOBAR;
295                        case -3:
296                                return TransformType.THREEBAR;
297                        case -4:
298                                return TransformType.FOURBAR;
299                        case -6:
300                                return TransformType.SIXBAR;
301                        default:
302                                throw new NullPointerException("This transformation did not fall into any of the known types! This is most likely a bug.");
303                        }
304                } else {
305                        if (isIdentity()) {
306                                return TransformType.AU;
307                        }
308                        if (isPureCrystalTranslation()) {
309                                return TransformType.XTALTRANSL;
310                        }
311                        if (isFractionalTranslation()) {
312                                return TransformType.CELLTRANSL;
313                        }
314                        throw new NullPointerException("This transformation did not fall into any of the known types! This is most likely a bug.");
315                }
316
317        }
318
319        public Vector3d getTranslScrewComponent() {
320
321                return getTranslScrewComponent(matTransform);
322
323        }
324
325        public int getTransformId() {
326                return transformId;
327        }
328
329        public void setTransformId(int transformId) {
330                this.transformId = transformId;
331        }
332
333        @Override
334        public String toString() {
335                return String.format("[%2d-(%s)]",transformId,toXYZString());
336        }
337
338        /**
339         * Expresses this transformation in terms of x,y,z fractional coordinates.
340         *
341         * Examples:
342         * @return
343         */
344        public String toXYZString() {
345                StringBuilder str = new StringBuilder();
346
347                for(int i=0;i<3;i++) { //for each row
348                        boolean emptyRow = true;
349
350                        double coef; // TODO work with rational numbers
351
352
353                        // X
354                        coef = matTransform.getElement(i, 0);
355
356                        // Three cases for coef: zero, one, non-one
357                        if(abs(coef) > 1e-6 ) { // Non-zero
358                                if( abs( abs(coef)-1 ) < 1e-6 ) { // +/- 1
359                                        if( coef < 0 ) {
360                                                str.append("-");
361                                        }
362                                } else {
363                                        str.append(formatCoef(coef));
364                                        str.append("*");
365                                }
366                                str.append("x");
367                                emptyRow = false;
368                        }
369
370                        // Y
371                        coef = matTransform.getElement(i, 1);
372
373                        if(abs(coef) > 1e-6 ) { // Non-zero
374                                if( abs( abs(coef)-1 ) < 1e-6 ) { // +/- 1
375                                        if( coef < 0 ) {
376                                                str.append("-");
377                                        } else if( !emptyRow ) {
378                                                str.append("+");
379                                        }
380                                } else {
381                                        if( !emptyRow && coef > 0) {
382                                                str.append("+");
383                                        }
384                                        str.append(formatCoef(coef));
385                                        str.append("*");
386                                }
387                                str.append("y");
388                                emptyRow = false;
389                        }
390
391                        // Z
392                        coef = matTransform.getElement(i, 2);
393
394                        if(abs(coef) > 1e-6 ) { // Non-zero
395                                if( abs( abs(coef)-1 ) < 1e-6 ) { // +/- 1
396                                        if( coef < 0 ) {
397                                                str.append("-");
398                                        } else if( !emptyRow ) {
399                                                str.append("+");
400                                        }
401                                } else {
402                                        if( !emptyRow && coef > 0) {
403                                                str.append("+");
404                                        }
405                                        str.append(formatCoef(coef));
406                                        str.append("*");
407                                }
408                                str.append("z");
409                                emptyRow = false;
410                        }
411
412                        // Intercept
413                        coef = matTransform.getElement(i, 3);
414
415                        if(abs(coef) > 1e-6 ) { // Non-zero
416                                if( !emptyRow && coef > 0) {
417                                        str.append("+");
418                                }
419                                str.append(formatCoef(coef));
420                        }
421
422                        if(i<2) {
423                                str.append(",");
424                        }
425                }
426
427
428                return str.toString();
429        }
430        /**
431         * helper function to format simple fractions into rationals
432         * @param coef
433         * @return
434         */
435        private String formatCoef(double coef) {
436                double tol = 1e-6; // rounding tolerance
437
438                // zero case
439                if( Math.abs(coef) < tol) {
440                        return "0";
441                }
442
443                // integer case
444                long num = Math.round(coef);
445                if( Math.abs(num - coef) < tol) {
446                        return Long.toString(num);
447                }
448
449                // Other small cases
450                for(int denom = 2; denom < 12; denom++ ) {
451                        num = Math.round(coef*denom);
452                        if( num - coef*denom < tol ) {
453                                return String.format("%d/%d",num, denom);
454                        }
455                }
456
457                // Give up and use floating point;
458                return String.format(Locale.US, "%.3f", coef);
459        }
460
461        /**
462         * Given a transformation matrix containing a rotation and translation returns the
463         * screw component of the rotation.
464         * See http://www.crystallography.fr/mathcryst/pdf/Gargnano/Aroyo_Gargnano_1.pdf
465         * @param m
466         * @return
467         */
468        public static Vector3d getTranslScrewComponent(Matrix4d m) {
469
470                int foldType = SpaceGroup.getRotAxisType(m);
471                // For reference see:
472                // http://www.crystallography.fr/mathcryst/pdf/Gargnano/Aroyo_Gargnano_1.pdf
473
474                Vector3d transl = null;
475
476                Matrix3d W =
477                                new Matrix3d(m.m00,m.m01,m.m02,
478                                                m.m10,m.m11,m.m12,
479                                                m.m20,m.m21,m.m22);
480
481                if (foldType>=0) {
482
483                        // the Y matrix: Y = W^k-1 + W^k-2 ... + W + I  ; with k the fold type
484                        Matrix3d Y = new Matrix3d(1,0,0, 0,1,0, 0,0,1);
485                        Matrix3d Wk = new Matrix3d(1,0,0, 0,1,0, 0,0,1);
486
487                        for (int k=0;k<foldType;k++) {
488                                Wk.mul(W); // k=0 Wk=W, k=1 Wk=W^2, k=2 Wk=W^3, ... k=foldType-1, Wk=W^foldType
489                                if (k!=foldType-1) Y.add(Wk);
490                        }
491
492                        transl = new Vector3d(m.m03, m.m13, m.m23);
493                        Y.transform(transl);
494
495                        transl.scale(1.0/foldType);
496
497                } else {
498
499                        if (foldType==-2) { // there are glide planes only in -2
500                                Matrix3d Y = new Matrix3d(1,0,0, 0,1,0, 0,0,1);
501                                Y.add(W);
502
503                                transl = new Vector3d(m.m03, m.m13, m.m23);
504                                Y.transform(transl);
505
506                                transl.scale(1.0/2.0);
507                        } else { // for -1, -3, -4 and -6 there's nothing to do: fill with 0s
508                                transl = new Vector3d(0,0,0);
509                        }
510                }
511
512                return transl;
513        }
514}