View Javadoc
1   // ******************************************************************************
2   //
3   // Title:       Force Field X.
4   // Description: Force Field X - Software for Molecular Biophysics.
5   // Copyright:   Copyright (c) Michael J. Schnieders 2001-2025.
6   //
7   // This file is part of Force Field X.
8   //
9   // Force Field X is free software; you can redistribute it and/or modify it
10  // under the terms of the GNU General Public License version 3 as published by
11  // the Free Software Foundation.
12  //
13  // Force Field X is distributed in the hope that it will be useful, but WITHOUT
14  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15  // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16  // details.
17  //
18  // You should have received a copy of the GNU General Public License along with
19  // Force Field X; if not, write to the Free Software Foundation, Inc., 59 Temple
20  // Place, Suite 330, Boston, MA 02111-1307 USA
21  //
22  // Linking this library statically or dynamically with other modules is making a
23  // combined work based on this library. Thus, the terms and conditions of the
24  // GNU General Public License cover the whole combination.
25  //
26  // As a special exception, the copyright holders of this library give you
27  // permission to link this library with independent modules to produce an
28  // executable, regardless of the license terms of these independent modules, and
29  // to copy and distribute the resulting executable under terms of your choice,
30  // provided that you also meet, for each linked independent module, the terms
31  // and conditions of the license of that module. An independent module is a
32  // module which is not derived from or based on this library. If you modify this
33  // library, you may extend this exception to your version of the library, but
34  // you are not obligated to do so. If you do not wish to do so, delete this
35  // exception statement from your version.
36  //
37  // ******************************************************************************
38  package ffx.crystal;
39  
40  import ffx.utilities.FFXProperty;
41  import org.apache.commons.configuration2.CompositeConfiguration;
42  import org.apache.commons.math3.linear.Array2DRowRealMatrix;
43  import org.apache.commons.math3.linear.LUDecomposition;
44  import org.apache.commons.math3.linear.RealMatrix;
45  
46  import java.util.ArrayList;
47  import java.util.List;
48  import java.util.Objects;
49  import java.util.logging.Level;
50  import java.util.logging.Logger;
51  
52  import static ffx.crystal.SpaceGroupDefinitions.spaceGroupFactory;
53  import static ffx.numerics.math.DoubleMath.dot;
54  import static ffx.numerics.math.DoubleMath.length;
55  import static ffx.numerics.math.MatrixMath.mat3Mat3;
56  import static ffx.numerics.math.MatrixMath.mat3SymVec6;
57  import static ffx.numerics.math.MatrixMath.transpose3;
58  import static ffx.numerics.math.ScalarMath.mod;
59  import static ffx.utilities.Constants.AVOGADRO;
60  import static ffx.utilities.PropertyGroup.UnitCellAndSpaceGroup;
61  import static ffx.utilities.StringUtils.padRight;
62  import static java.lang.String.format;
63  import static org.apache.commons.math3.util.FastMath.abs;
64  import static org.apache.commons.math3.util.FastMath.acos;
65  import static org.apache.commons.math3.util.FastMath.cbrt;
66  import static org.apache.commons.math3.util.FastMath.cos;
67  import static org.apache.commons.math3.util.FastMath.floor;
68  import static org.apache.commons.math3.util.FastMath.min;
69  import static org.apache.commons.math3.util.FastMath.random;
70  import static org.apache.commons.math3.util.FastMath.signum;
71  import static org.apache.commons.math3.util.FastMath.sin;
72  import static org.apache.commons.math3.util.FastMath.sqrt;
73  import static org.apache.commons.math3.util.FastMath.toDegrees;
74  import static org.apache.commons.math3.util.FastMath.toRadians;
75  
76  /**
77   * The Crystal class encapsulates the lattice parameters and space group that describe the geometry
78   * and symmetry of a crystal. Methods are available to apply the minimum image convention and space
79   * group symmetry operators.
80   *
81   * @author Michael J. Schnieders
82   * @see ReplicatesCrystal
83   * @since 1.0
84   */
85  public class Crystal {
86  
87    private static final Logger logger = Logger.getLogger(Crystal.class.getName());
88  
89    /**
90     * The space group of the crystal.
91     */
92    @FFXProperty(name = "SpaceGroup", propertyGroup = UnitCellAndSpaceGroup, clazz = SpaceGroup.class, defaultValue = "P1",
93        description = """
94            This property defines the crystal space group used during structural manipulations and force field calculations.
95            """)
96    public final SpaceGroup spaceGroup;
97  
98    /**
99     * Length of the cell edge in the direction of the <b>a</b> basis vector.
100    */
101   @FFXProperty(name = "a-axis", propertyGroup = UnitCellAndSpaceGroup, defaultValue = "None",
102       description = """
103           Sets the value of the a-axis length for a crystal unit cell, or, equivalently,
104           the X-axis length for a periodic box (Angstroms).
105           """)
106   public double a;
107 
108   /**
109    * Length of the cell edge in the direction of the <b>b</b> basis vector.
110    */
111   @FFXProperty(name = "b-axis", propertyGroup = UnitCellAndSpaceGroup, defaultValue = "A-Axis",
112       description = """
113           Sets the value of the b-axis length for a crystal unit cell, or, equivalently,
114           the Y-axis length for a periodic box (Angstroms).
115           """)
116   public double b;
117 
118   /**
119    * Length of the cell edge in the direction of the <b>c</b> basis vector.
120    */
121   @FFXProperty(name = "c-axis", propertyGroup = UnitCellAndSpaceGroup, defaultValue = "A-Axis",
122       description = """
123           Sets the value of the c-axis length for a crystal unit cell, or, equivalently,
124           the Z-axis length for a periodic box (Angstroms).
125           """)
126   public double c;
127 
128   /**
129    * The interaxial lattice angle between <b>b</b> and <b>c</b>.
130    */
131   @FFXProperty(name = "alpha", propertyGroup = UnitCellAndSpaceGroup, defaultValue = "90.0",
132       description = """
133           Sets the value of the α-angle of a crystal unit cell, i.e.,
134           the angle between the b-axis and c-axis of a unit cell, or, equivalently,
135           the angle between the Y-axis and Z-axis of a periodic box.
136           """)
137   public double alpha;
138 
139   /**
140    * The interaxial lattice angle between <b>a</b> and <b>c</b>.
141    */
142   @FFXProperty(name = "beta", propertyGroup = UnitCellAndSpaceGroup, defaultValue = "Alpha",
143       description = """
144           Sets the value of the β-angle of a crystal unit cell, i.e.,
145           the angle between the a-axis and c-axis of a unit cell, or, equivalently,
146           the angle between the X-axis and Z-axis of a periodic box.
147           """)
148   public double beta;
149 
150   /**
151    * The interaxial lattice angle between <b>a</b> and <b>b</b>.
152    */
153   @FFXProperty(name = "gamma", propertyGroup = UnitCellAndSpaceGroup, defaultValue = "Alpha",
154       description = """
155           Sets the value of the γ-angle of a crystal unit cell, i.e.,
156           the angle between the a-axis and b-axis of a unit cell, or, equivalently,
157           the angle between the X-axis and Y-axis of a periodic box.
158           """)
159   public double gamma;
160 
161   /**
162    * A mask equal to 0 for X-coordinates.
163    */
164   private static final int XX = 0;
165   /**
166    * A mask equal to 1 for Y-coordinates.
167    */
168   private static final int YY = 1;
169   /**
170    * A mask equal to 2 for Z-coordinates.
171    */
172   private static final int ZZ = 2;
173 
174   /**
175    * Real space Lattice vectors.
176    * <br>A-axis vector is the first row.
177    * <br>B-axis vector is the second row.
178    * <br>C-axis vector is the third row.
179    */
180   public final double[][] Ai = new double[3][3];
181   /**
182    * The direct space metric matrix.
183    */
184   public final double[][] G = new double[3][3];
185   /**
186    * Reference to the space group crystal system.
187    */
188   private final CrystalSystem crystalSystem;
189   /**
190    * Reference to the space group lattice system.
191    */
192   private final LatticeSystem latticeSystem;
193   /**
194    * The crystal unit cell volume.
195    */
196   public double volume;
197   /**
198    * Reciprocal lattice vectors.
199    * <br>A* reciprocal space vector is the first column.
200    * <br>B* reciprocal space vector is the second column.
201    * <br>C* reciprocal space vector is the third column.
202    */
203   public double[][] A;
204   /**
205    * A* reciprocal lattice vector.
206    */
207   public double A00, A10, A20;
208   /**
209    * B* reciprocal lattice vector (A01 is zero).
210    */
211   public double A11, A21;
212   /**
213    * C* reciprocal lattice vector (A02 and A12 are zero).
214    */
215   public double A22;
216 
217   /**
218    * Interfacial radius in the direction of the A-axis.
219    */
220   public double interfacialRadiusA;
221   /**
222    * Interfacial radius in the direction of the B-axis.
223    */
224   public double interfacialRadiusB;
225   /**
226    * Interfacial radius in the direction of the C-axis.
227    */
228   public double interfacialRadiusC;
229   /**
230    * Anisotropic bulk solvent B-factor scaling (0 or 1 for each component).
231    */
232   public int[] scaleB = new int[6];
233   /**
234    * Number of bulk solvent B-factor components.
235    */
236   public int scaleN;
237   /**
238    * A-axis lattice vector in Cartesian coordinates.
239    */
240   public double Ai00;
241   /**
242    * B-axis lattice vector in Cartesian coordinates.
243    */
244   public double Ai10, Ai11;
245   /**
246    * C-axis lattice vector in Cartesian coordinates.
247    */
248   public double Ai20, Ai21, Ai22;
249   /**
250    * Change in the volume with respect to a.
251    */
252   public double dVdA;
253   /**
254    * Change in the volume with respect to b.
255    */
256   public double dVdB;
257   /**
258    * Change in the volume with respect to c.
259    */
260   public double dVdC;
261   /**
262    * Change in the volume with respect to alpha (in Radians). This is set to zero if alpha is fixed.
263    */
264   public double dVdAlpha;
265   /**
266    * Change in the volume with respect to beta (in Radians). This is set to zero if beta is fixed.
267    */
268   public double dVdBeta;
269   /**
270    * Change in the volume with respect to gamma (in Radians). This is set to zero if gamma is fixed.
271    */
272   public double dVdGamma;
273   /**
274    * For some finite-difference calculations, it's currently necessary to remove lattice system
275    * restrictions.
276    */
277   boolean checkRestrictions = true;
278   /**
279    * An atom and one of its symmetry copies within the specialPositionCutoff should be flagged to be
280    * at a special position.
281    */
282   private double specialPositionCutoff = 0.3;
283   /**
284    * Copy of symmetry operators in Cartesian coordinates.
285    */
286   private List<SymOp> symOpsCartesian;
287   /**
288    * The reciprocal space metric matrix.
289    */
290   private double[][] Gstar;
291   /**
292    * SpecialPositionCutoff squared.
293    */
294   private double specialPositionCutoff2 = specialPositionCutoff * specialPositionCutoff;
295   /**
296    * Flag to indicate an aperiodic system.
297    */
298   private boolean aperiodic;
299 
300   /**
301    * The Crystal class encapsulates the lattice parameters and space group. Methods are available to
302    * apply the minimum image convention and to apply space group operators.
303    *
304    * @param a        The a-axis length.
305    * @param b        The b-axis length.
306    * @param c        The c-axis length.
307    * @param alpha    The alpha angle.
308    * @param beta     The beta angle.
309    * @param gamma    The gamma angle.
310    * @param sgNumber The space group number.
311    */
312   public Crystal(double a, double b, double c, double alpha, double beta, double gamma, int sgNumber) {
313     this(a, b, c, alpha, beta, gamma, spaceGroupFactory(sgNumber).pdbName);
314   }
315 
316   /**
317    * The Crystal class encapsulates the lattice parameters and space group. Methods are available to
318    * apply the minimum image convention and to apply space group operators.
319    *
320    * @param a     The a-axis length.
321    * @param b     The b-axis length.
322    * @param c     The c-axis length.
323    * @param alpha The alpha angle.
324    * @param beta  The beta angle.
325    * @param gamma The gamma angle.
326    * @param sg    The space group symbol.
327    */
328   public Crystal(double a, double b, double c, double alpha, double beta, double gamma, String sg) {
329     // Crystal SpaceGroup and LatticeSystem are final variables. Temp variable to delay assigning.
330     SpaceGroup tempSG = spaceGroupFactory(sg);
331     LatticeSystem tempLS = tempSG.latticeSystem;
332     this.a = a;
333     this.b = b;
334     this.c = c;
335     this.alpha = alpha;
336     this.beta = beta;
337     this.gamma = gamma;
338     aperiodic = false;
339     if (!tempLS.validParameters(a, b, c, alpha, beta, gamma)) {
340       // Invalid parameters... Start error/warning log and try to fix.
341       StringBuilder sb = new StringBuilder(format(
342           " The %s lattice parameters do not satisfy the %s lattice system restrictions.\n",
343           tempSG.pdbName, tempLS));
344       sb.append(format("  A-axis:                              %18.15e\n", a));
345       sb.append(format("  B-axis:                              %18.15e\n", b));
346       sb.append(format("  C-axis:                              %18.15e\n", c));
347       sb.append(format("  Alpha:                               %18.15e\n", alpha));
348       sb.append(format("  Beta:                                %18.15e\n", beta));
349       sb.append(format("  Gamma:                               %18.15e\n", gamma));
350       logger.info(sb.toString());
351       sb = new StringBuilder();
352       if (tempLS == LatticeSystem.HEXAGONAL_LATTICE
353           || tempLS == LatticeSystem.RHOMBOHEDRAL_LATTICE) {
354         // Try to convert between hexagonal and rhombohedral lattices to fix crystal.
355         Crystal convertedCrystal = SpaceGroupConversions.hrConversion(a, b, c, alpha, beta, gamma,
356             tempSG);
357         this.a = convertedCrystal.a;
358         this.b = convertedCrystal.b;
359         this.c = convertedCrystal.c;
360         this.alpha = convertedCrystal.alpha;
361         this.beta = convertedCrystal.beta;
362         this.gamma = convertedCrystal.gamma;
363         spaceGroup = convertedCrystal.spaceGroup;
364         crystalSystem = spaceGroup.crystalSystem;
365         latticeSystem = spaceGroup.latticeSystem;
366         sb.append(" Converted ").append(tempSG.pdbName).append(" to ").append(spaceGroup.pdbName);
367         if (!latticeSystem.validParameters(this.a, this.b, this.c, this.alpha, this.beta,
368             this.gamma)) {
369           sb.append(format(
370               " The %s lattice parameters do not satisfy the %s lattice system restrictions.\n",
371               spaceGroup.pdbName, latticeSystem));
372           sb.append(format("  A-axis:                              %18.15e\n", this.a));
373           sb.append(format("  B-axis:                              %18.15e\n", this.b));
374           sb.append(format("  C-axis:                              %18.15e\n", this.c));
375           sb.append(format("  Alpha:                               %18.15e\n", this.alpha));
376           sb.append(format("  Beta:                                %18.15e\n", this.beta));
377           sb.append(format("  Gamma:                               %18.15e\n", this.gamma));
378           logger.severe(sb.toString());
379         } else {
380           // Successfully converted space group between hexagonal and rhombohedral.
381           logger.info(sb.toString());
382         }
383       } else {
384         // Invalid lattice parameters. Update Crystal as much as possible, then print error message.
385         this.a = a;
386         this.b = b;
387         this.c = c;
388         this.alpha = alpha;
389         this.beta = beta;
390         this.gamma = gamma;
391         spaceGroup = tempSG;
392         crystalSystem = spaceGroup.crystalSystem;
393         latticeSystem = spaceGroup.latticeSystem;
394         logger.severe(sb.toString());
395       }
396     } else {
397       // Valid parameters, update crystal and continue.
398       this.a = a;
399       this.b = b;
400       this.c = c;
401       this.alpha = alpha;
402       this.beta = beta;
403       this.gamma = gamma;
404       spaceGroup = tempSG;
405       crystalSystem = spaceGroup.crystalSystem;
406       latticeSystem = spaceGroup.latticeSystem;
407     }
408 
409     for (int i = 0; i < 6; i++) {
410       scaleB[i] = -1;
411     }
412 
413     SymOp symop;
414     double[][] rot;
415     int index = 0;
416     switch (crystalSystem) {
417       case TRICLINIC:
418         for (int i = 0; i < 6; i++) {
419           scaleB[i] = index++;
420         }
421         break;
422       case MONOCLINIC:
423         scaleB[0] = index++;
424         scaleB[1] = index++;
425         scaleB[2] = index++;
426         // determine unique axis
427         symop = spaceGroup.symOps.get(1);
428         rot = symop.rot;
429         if (rot[0][0] > 0) {
430           scaleB[5] = index++;
431         } else if (rot[1][1] > 0) {
432           scaleB[4] = index++;
433         } else {
434           scaleB[3] = index++;
435         }
436         break;
437       case ORTHORHOMBIC:
438         scaleB[0] = index++;
439         scaleB[1] = index++;
440         scaleB[2] = index++;
441         break;
442       case TETRAGONAL:
443         scaleB[0] = index++;
444         scaleB[1] = scaleB[0];
445         scaleB[2] = index++;
446         break;
447       case TRIGONAL:
448       case HEXAGONAL:
449         boolean hexagonal = false;
450         for (int i = 1; i < spaceGroup.symOps.size(); i++) {
451           symop = spaceGroup.symOps.get(i);
452           rot = symop.rot;
453           index = 0;
454           if ((rot[1][1] * rot[1][2] == -1)
455               || (rot[2][1] * rot[2][2] == -1)
456               || (rot[1][1] * rot[1][2] == 1)
457               || (rot[2][1] * rot[2][2] == 1)) {
458             scaleB[0] = index++;
459             scaleB[1] = index++;
460             scaleB[2] = scaleB[1];
461             hexagonal = true;
462           } else if ((rot[0][0] * rot[0][2] == -1)
463               || (rot[2][0] * rot[2][2] == -1)
464               || (rot[0][0] * rot[0][2] == 1)
465               || (rot[2][0] * rot[2][2] == 1)) {
466             scaleB[0] = index++;
467             scaleB[1] = index++;
468             scaleB[2] = scaleB[0];
469             hexagonal = true;
470           } else if ((rot[0][0] * rot[0][1] == -1)
471               || (rot[1][0] * rot[1][1] == -1)
472               || (rot[0][0] * rot[0][1] == 1)
473               || (rot[1][0] * rot[1][1] == 1)) {
474             scaleB[0] = index++;
475             scaleB[1] = scaleB[0];
476             scaleB[2] = index++;
477             hexagonal = true;
478           }
479           if (hexagonal) {
480             break;
481           }
482         }
483         if (!hexagonal) {
484           // rhombohedral
485           scaleB[3] = index++;
486           scaleB[4] = scaleB[3];
487           scaleB[5] = scaleB[3];
488         }
489         break;
490       case CUBIC:
491         break;
492     }
493     scaleN = index;
494 
495     updateCrystal();
496   }
497 
498   /**
499    * checkProperties
500    *
501    * @param properties a {@link org.apache.commons.configuration2.CompositeConfiguration}
502    *                   object.
503    * @return a {@link ffx.crystal.Crystal} object.
504    */
505   public static Crystal checkProperties(CompositeConfiguration properties) {
506     double a = properties.getDouble("a-axis", -1.0);
507     double b = properties.getDouble("b-axis", -1.0);
508     double c = properties.getDouble("c-axis", -1.0);
509     double alpha = properties.getDouble("alpha", -1.0);
510     double beta = properties.getDouble("beta", -1.0);
511     double gamma = properties.getDouble("gamma", -1.0);
512     String sg = properties.getString("spacegroup", null);
513 
514     sg = SpaceGroupInfo.pdb2ShortName(sg);
515 
516     if (a < 0.0 || b < 0.0 || c < 0.0 || alpha < 0.0 || beta < 0.0 || gamma < 0.0 || sg == null) {
517       return null;
518     }
519 
520     // check the space group name is valid
521     SpaceGroup spaceGroup = spaceGroupFactory(sg);
522     if (spaceGroup == null) {
523       sg = sg.replaceAll(" ", "");
524       spaceGroup = spaceGroupFactory(sg);
525       if (spaceGroup == null) {
526         return null;
527       }
528     }
529 
530     return new Crystal(a, b, c, alpha, beta, gamma, sg);
531   }
532 
533   /**
534    * invressq
535    *
536    * @param hkl a {@link ffx.crystal.HKL} object.
537    * @return a double.
538    */
539   public double invressq(HKL hkl) {
540     return hkl.quadForm(Gstar);
541   }
542 
543   /**
544    * res
545    *
546    * @param hkl a {@link ffx.crystal.HKL} object.
547    * @return a double.
548    */
549   public double res(HKL hkl) {
550     return 1.0 / sqrt(hkl.quadForm(Gstar));
551   }
552 
553   /**
554    * aperiodic
555    *
556    * @return a boolean.
557    */
558   public boolean aperiodic() {
559     return aperiodic;
560   }
561 
562   /**
563    * Apply a fractional symmetry operator to an array of Cartesian coordinates.
564    * <p>
565    * If the arrays x, y or z are null or not of length n, an exception is thrown.
566    * <p>
567    * If mateX, mateY or mateZ are null or not of length n, an exception is thrown.
568    *
569    * @param n     Number of atoms.
570    * @param x     Input fractional x-coordinates.
571    * @param y     Input fractional y-coordinates.
572    * @param z     Input fractional z-coordinates.
573    * @param mateX Output fractional x-coordinates.
574    * @param mateY Output fractional y-coordinates.
575    * @param mateZ Output fractional z-coordinates.
576    * @param symOp The fractional symmetry operator.
577    */
578   public void applySymOp(int n, double[] x, double[] y, double[] z,
579                          double[] mateX, double[] mateY, double[] mateZ, SymOp symOp) {
580     if (x == null || y == null || z == null) {
581       throw new IllegalArgumentException("The input arrays x, y and z must not be null.");
582     }
583     if (x.length < n || y.length < n || z.length < n) {
584       throw new IllegalArgumentException("The input arrays x, y and z must be of length n: " + n);
585     }
586     if (mateX == null || mateY == null || mateZ == null) {
587       throw new IllegalArgumentException("The output arrays mateX, mateY and mateZ must not be null.");
588     }
589     if (mateX.length < n || mateY.length < n || mateZ.length < n) {
590       throw new IllegalArgumentException("The output arrays mateX, mateY and mateZ must be of length n: " + n);
591     }
592     final double[][] rot = symOp.rot;
593     final double[] trans = symOp.tr;
594 
595     final double rot00 = rot[0][0];
596     final double rot10 = rot[1][0];
597     final double rot20 = rot[2][0];
598     final double rot01 = rot[0][1];
599     final double rot11 = rot[1][1];
600     final double rot21 = rot[2][1];
601     final double rot02 = rot[0][2];
602     final double rot12 = rot[1][2];
603     final double rot22 = rot[2][2];
604     final double t0 = trans[0];
605     final double t1 = trans[1];
606     final double t2 = trans[2];
607     for (int i = 0; i < n; i++) {
608       double xc = x[i];
609       double yc = y[i];
610       double zc = z[i];
611       // Convert to fractional coordinates.
612       double xi = xc * A00 + yc * A10 + zc * A20;
613       double yi = yc * A11 + zc * A21;
614       double zi = zc * A22;
615       // Apply Symmetry Operator.
616       double fx = rot00 * xi + rot01 * yi + rot02 * zi + t0;
617       double fy = rot10 * xi + rot11 * yi + rot12 * zi + t1;
618       double fz = rot20 * xi + rot21 * yi + rot22 * zi + t2;
619       // Convert back to Cartesian coordinates.
620       mateX[i] = fx * Ai00 + fy * Ai10 + fz * Ai20;
621       mateY[i] = fy * Ai11 + fz * Ai21;
622       mateZ[i] = fz * Ai22;
623     }
624   }
625 
626   /**
627    * Apply a fractional symmetry operator to one set of cartesian coordinates.
628    *
629    * @param xyz   Input cartesian coordinates.
630    * @param mate  Symmetry mate cartesian coordinates.
631    * @param symOp The fractional symmetry operator.
632    */
633   public void applySymOp(double[] xyz, double[] mate, SymOp symOp) {
634     assert (xyz.length % 3 == 0);
635     assert (xyz.length == mate.length);
636 
637     var rot = symOp.rot;
638     var r00 = rot[0][0];
639     var r01 = rot[0][1];
640     var r02 = rot[0][2];
641     var r10 = rot[1][0];
642     var r11 = rot[1][1];
643     var r12 = rot[1][2];
644     var r20 = rot[2][0];
645     var r21 = rot[2][1];
646     var r22 = rot[2][2];
647 
648     var trans = symOp.tr;
649     var t0 = trans[0];
650     var t1 = trans[1];
651     var t2 = trans[2];
652 
653     int len = xyz.length / 3;
654     for (int i = 0; i < len; i++) {
655       int index = i * 3;
656       var xc = xyz[index + XX];
657       var yc = xyz[index + YY];
658       var zc = xyz[index + ZZ];
659       // Convert to fractional coordinates.
660       var xi = xc * A00 + yc * A10 + zc * A20;
661       var yi = yc * A11 + zc * A21;
662       var zi = zc * A22;
663       // Apply Symmetry Operator.
664       var fx = r00 * xi + r01 * yi + r02 * zi + t0;
665       var fy = r10 * xi + r11 * yi + r12 * zi + t1;
666       var fz = r20 * xi + r21 * yi + r22 * zi + t2;
667       // Convert back to Cartesian coordinates.
668       mate[index + XX] = fx * Ai00 + fy * Ai10 + fz * Ai20;
669       mate[index + YY] = fy * Ai11 + fz * Ai21;
670       mate[index + ZZ] = fz * Ai22;
671     }
672   }
673 
674   /**
675    * Apply a fractional symmetry operator to one set of cartesian coordinates.
676    *
677    * @param xyz   Input cartesian coordinates.
678    * @param mate  Symmetry mate cartesian coordinates.
679    * @param symOp The fractional symmetry operator.
680    */
681   public void applySymRot(double[] xyz, double[] mate, SymOp symOp) {
682     double[][] rot = symOp.rot;
683     // Convert to fractional coordinates.
684     double xc = xyz[0];
685     double yc = xyz[1];
686     double zc = xyz[2];
687     // Convert to fractional coordinates.
688     double xi = xc * A00 + yc * A10 + zc * A20;
689     double yi = yc * A11 + zc * A21;
690     double zi = zc * A22;
691 
692     // Apply Symmetry Operator.
693     double fx = rot[0][0] * xi + rot[0][1] * yi + rot[0][2] * zi;
694     double fy = rot[1][0] * xi + rot[1][1] * yi + rot[1][2] * zi;
695     double fz = rot[2][0] * xi + rot[2][1] * yi + rot[2][2] * zi;
696 
697     // Convert back to Cartesian coordinates.
698     mate[0] = fx * Ai00 + fy * Ai10 + fz * Ai20;
699     mate[1] = fy * Ai11 + fz * Ai21;
700     mate[2] = fz * Ai22;
701   }
702 
703   /**
704    * Apply the transpose of a symmetry rotation to an array of Cartesian coordinates.
705    * <p>
706    * If the arrays x, y or z are null or not of length n, an exception is thrown.
707    * <p>
708    * If mateX, mateY or mateZ are null or not of length n, an exception is thrown.
709    *
710    * @param n      Number of atoms.
711    * @param x      Input x-coordinates.
712    * @param y      Input y-coordinates.
713    * @param z      Input z-coordinates.
714    * @param mateX  Output x-coordinates.
715    * @param mateY  Output y-coordinates.
716    * @param mateZ  Output z-coordinates.
717    * @param symOp  The symmetry operator.
718    * @param rotmat an array of double.
719    */
720   public void applyTransSymRot(
721       int n, double[] x, double[] y, double[] z,
722       double[] mateX, double[] mateY, double[] mateZ, SymOp symOp, double[][] rotmat) {
723 
724     if (x == null || y == null || z == null) {
725       throw new IllegalArgumentException("The input arrays x, y and z must not be null.");
726     }
727     if (x.length < n || y.length < n || z.length < n) {
728       throw new IllegalArgumentException("The input arrays x, y and z must be of length n: " + n);
729     }
730     if (mateX == null || mateY == null || mateZ == null) {
731       throw new IllegalArgumentException("The output arrays mateX, mateY and mateZ must not be null.");
732     }
733     if (mateX.length < n || mateY.length < n || mateZ.length < n) {
734       throw new IllegalArgumentException("The output arrays mateX, mateY and mateZ must be of length n: " + n);
735     }
736 
737     // The transformation operator R = ToCart * Rot * ToFrac
738     getTransformationOperator(symOp, rotmat);
739 
740     for (int i = 0; i < n; i++) {
741       // Apply R^T (its transpose).
742       double xc = x[i];
743       double yc = y[i];
744       double zc = z[i];
745       mateX[i] = xc * rotmat[0][0] + yc * rotmat[1][0] + zc * rotmat[2][0];
746       mateY[i] = xc * rotmat[0][1] + yc * rotmat[1][1] + zc * rotmat[2][1];
747       mateZ[i] = xc * rotmat[0][2] + yc * rotmat[1][2] + zc * rotmat[2][2];
748     }
749 
750   }
751 
752   /**
753    * averageTensor
754    *
755    * @param m an array of double.
756    * @param r an array of double.
757    */
758   public void averageTensor(double[][] m, double[][] r) {
759     int n = spaceGroup.symOps.size();
760     for (int i = 0; i < n; i++) {
761       SymOp symop = spaceGroup.symOps.get(i);
762       double[][] rot = symop.rot;
763       double[][] rt = transpose3(rot);
764       double[][] rmrt = mat3Mat3(mat3Mat3(rot, m), rt);
765       for (int j = 0; j < 3; j++) {
766         for (int k = 0; k < 3; k++) {
767           r[j][k] += rmrt[j][k];
768         }
769       }
770     }
771     for (int j = 0; j < 3; j++) {
772       for (int k = 0; k < 3; k++) {
773         r[j][k] /= 6.0;
774       }
775     }
776   }
777 
778   /**
779    * averageTensor
780    *
781    * @param v an array of double.
782    * @param r an array of double.
783    */
784   public void averageTensor(double[] v, double[][] r) {
785     int n = spaceGroup.symOps.size();
786     for (int i = 0; i < n; i++) {
787       SymOp symop = spaceGroup.symOps.get(i);
788       double[][] rot = symop.rot;
789       double[][] rt = transpose3(rot);
790       double[][] rmrt = mat3Mat3(mat3SymVec6(rot, v), rt);
791       for (int j = 0; j < 3; j++) {
792         for (int k = 0; k < 3; k++) {
793           r[j][k] += rmrt[j][k];
794         }
795       }
796     }
797     for (int j = 0; j < 3; j++) {
798       for (int k = 0; k < 3; k++) {
799         r[j][k] /= 6.0;
800       }
801     }
802   }
803 
804   /**
805    * This method should be called to update the unit cell parameters of a crystal. The proposed
806    * parameters will only be accepted if symmetry restrictions are satisfied. If so, all Crystal
807    * variables that depend on the unit cell parameters will be updated.
808    *
809    * @param a     length of the a-axis.
810    * @param b     length of the b-axis.
811    * @param c     length of the c-axis.
812    * @param alpha Angle between b-axis and c-axis.
813    * @param beta  Angle between a-axis and c-axis.
814    * @param gamma Angle between a-axis and b-axis.
815    * @return The method return true if the parameters are accepted, false otherwise.
816    */
817   public boolean changeUnitCellParameters(
818       double a, double b, double c, double alpha, double beta, double gamma) {
819     if (checkRestrictions) {
820       if (!latticeSystem.validParameters(a, b, c, alpha, beta, gamma)) {
821         if (logger.isLoggable(Level.FINE)) {
822           StringBuilder sb = new StringBuilder(
823               " The proposed lattice parameters for " + spaceGroup.pdbName
824                   + " do not satisfy the " + latticeSystem +
825                   " lattice system restrictions and were ignored.\n");
826           sb.append(format("  A-axis:                              %18.15e\n", a));
827           sb.append(format("  B-axis:                              %18.15e\n", b));
828           sb.append(format("  C-axis:                              %18.15e\n", c));
829           sb.append(format("  Alpha:                               %18.15e\n", alpha));
830           sb.append(format("  Beta:                                %18.15e\n", beta));
831           sb.append(format("  Gamma:                               %18.15e\n", gamma));
832           logger.fine(sb.toString());
833         }
834         return false;
835       }
836     }
837 
838     this.a = a;
839     this.b = b;
840     this.c = c;
841     this.alpha = alpha;
842     this.beta = beta;
843     this.gamma = gamma;
844 
845     updateCrystal();
846 
847     return true;
848   }
849 
850   /**
851    * This method should be called to update the unit cell parameters of a crystal. The proposed
852    * parameters will only be accepted if symmetry restrictions are satisfied. If so, all Crystal
853    * variables that depend on the unit cell parameters will be updated.
854    * <p>
855    * If the new parameters are accepted, the target asymmetric unit volume is achieved by uniformly
856    * scaling all lattice lengths.
857    *
858    * @param a              length of the a-axis.
859    * @param b              length of the b-axis.
860    * @param c              length of the c-axis.
861    * @param alpha          Angle between b-axis and c-axis.
862    * @param beta           Angle between a-axis and c-axis.
863    * @param gamma          Angle between a-axis and b-axis.
864    * @param targetAUVolume Target asymmetric unit volume.
865    * @return The method return true if the parameters are accepted, false otherwise.
866    */
867   public boolean changeUnitCellParametersAndVolume(
868       double a, double b, double c, double alpha, double beta, double gamma, double targetAUVolume) {
869     if (changeUnitCellParameters(a, b, c, alpha, beta, gamma)) {
870       double currentAUVolume = volume / getNumSymOps();
871       double scale = cbrt(targetAUVolume / currentAUVolume);
872       return changeUnitCellParameters(scale * a, scale * b, scale * c, alpha, beta, gamma);
873     }
874     return false;
875   }
876 
877   /**
878    * Two crystals are equal only if all unit cell parameters are exactly the same.
879    *
880    * @param o the Crystal to compare to.
881    * @return true if all unit cell parameters are exactly the same.
882    */
883   @Override
884   public boolean equals(Object o) {
885     if (this == o) {
886       return true;
887     }
888     if (o == null || getClass() != o.getClass()) {
889       return false;
890     }
891     Crystal crystal = (Crystal) o;
892     return (a == crystal.a
893         && b == crystal.b
894         && c == crystal.c
895         && alpha == crystal.alpha
896         && beta == crystal.beta
897         && gamma == crystal.gamma
898         && spaceGroup.number == crystal.spaceGroup.number);
899   }
900 
901   /**
902    * Are space group restrictions being checked.
903    *
904    * @return true if the restrictions are being checked.
905    */
906   public boolean getCheckRestrictions() {
907     return checkRestrictions;
908   }
909 
910   /**
911    * Set whether to check space group restrictions.
912    *
913    * @param checkRestrictions true to check restrictions.
914    */
915   public void setCheckRestrictions(boolean checkRestrictions) {
916     this.checkRestrictions = checkRestrictions;
917   }
918 
919   /**
920    * Compute the density of the system.
921    *
922    * @param mass The total mass of the asymmetric unit.
923    * @return The density (g/cc)
924    */
925   public double getDensity(double mass) {
926     int nSymm = spaceGroup.symOps.size();
927     return (mass * nSymm / AVOGADRO) * (1.0e24 / volume);
928   }
929 
930   /**
931    * Return the number of symmetry operators for this crystal.
932    *
933    * @return The number of symmetry operators.
934    */
935   public int getNumSymOps() {
936     return spaceGroup.getNumberOfSymOps();
937   }
938 
939   /**
940    * Create a random Cartesian translation vector.
941    *
942    * <p>First, a random fractional translation is created. Second, the random fractional operator is
943    * converted to Cartesian coordinates.
944    *
945    * @return A random Cartesian translation vector.
946    */
947   public double[] getRandomCartTranslation() {
948     double[] coords = {random(), random(), random()};
949     toCartesianCoordinates(coords, coords);
950     return coords;
951   }
952 
953   /**
954    * Getter for the field <code>specialPositionCutoff</code>.
955    *
956    * @return a double.
957    */
958   public double getSpecialPositionCutoff() {
959     return specialPositionCutoff;
960   }
961 
962   /**
963    * Getter for the field <code>specialPositionCutoff2</code>.
964    *
965    * @return a double.
966    */
967   public double getSpecialPositionCutoff2() {
968     return specialPositionCutoff2;
969   }
970 
971   /**
972    * Setter for the field <code>specialPositionCutoff</code>.
973    *
974    * @param cutoff a double.
975    */
976   public void setSpecialPositionCutoff(double cutoff) {
977     specialPositionCutoff = cutoff;
978     specialPositionCutoff2 = cutoff * cutoff;
979   }
980 
981   /**
982    * Compute the total transformation operator R = ToCart * Rot * ToFrac.
983    *
984    * @param symOp  Symmetry operator to apply.
985    * @param rotmat Resulting transformation operator R.
986    */
987   public void getTransformationOperator(SymOp symOp, double[][] rotmat) {
988     double[][] rot = symOp.rot;
989     for (int i = 0; i < 3; i++) {
990       for (int j = 0; j < 3; j++) {
991         rotmat[i][j] = 0.0;
992         for (int k = 0; k < 3; k++) {
993           for (int l = 0; l < 3; l++) {
994             rotmat[i][j] += Ai[k][i] * rot[k][l] * A[j][l];
995           }
996         }
997       }
998     }
999   }
1000 
1001   /**
1002    * The ReplicatesCrystal over-rides this method to return the unit cell rather than the
1003    * ReplicateCell.
1004    *
1005    * @return The unit cell Crystal instance.
1006    */
1007   public Crystal getUnitCell() {
1008     return this;
1009   }
1010 
1011   /**
1012    * {@inheritDoc}
1013    */
1014   @Override
1015   public int hashCode() {
1016     return Objects.hash(a, b, c, alpha, beta, gamma, spaceGroup.number);
1017   }
1018 
1019   /**
1020    * Apply the minimum image convention.
1021    *
1022    * @param xyz  the coordinates of the first atom.
1023    * @param xyz2 the coordinates of the second atom.
1024    * @return the output distance squared.
1025    */
1026   public double image(double[] xyz, double[] xyz2) {
1027     double dx = xyz[0] - xyz2[0];
1028     double dy = xyz[1] - xyz2[1];
1029     double dz = xyz[2] - xyz2[2];
1030     return image(dx, dy, dz);
1031   }
1032 
1033   /**
1034    * Apply the minimum image convention.
1035    *
1036    * @param xyz input distances that are over-written.
1037    * @return the output distance squared.
1038    */
1039   public double image(final double[] xyz) {
1040     double x = xyz[0];
1041     double y = xyz[1];
1042     double z = xyz[2];
1043     if (aperiodic) {
1044       return x * x + y * y + z * z;
1045     }
1046     double xf = x * A00 + y * A10 + z * A20;
1047     double yf = y * A11 + z * A21;
1048     double zf = z * A22;
1049     xf = floor(abs(xf) + 0.5) * signum(-xf) + xf;
1050     yf = floor(abs(yf) + 0.5) * signum(-yf) + yf;
1051     zf = floor(abs(zf) + 0.5) * signum(-zf) + zf;
1052     x = xf * Ai00 + yf * Ai10 + zf * Ai20;
1053     y = yf * Ai11 + zf * Ai21;
1054     z = zf * Ai22;
1055     xyz[0] = x;
1056     xyz[1] = y;
1057     xyz[2] = z;
1058     return x * x + y * y + z * z;
1059   }
1060 
1061   /**
1062    * Apply the minimum image convention.
1063    *
1064    * @param dx x-distance
1065    * @param dy y-distance
1066    * @param dz z-distance
1067    * @return the output distance squared.
1068    */
1069   public double image(double dx, double dy, double dz) {
1070     if (aperiodic) {
1071       return dx * dx + dy * dy + dz * dz;
1072     }
1073     double xf = dx * A00 + dy * A10 + dz * A20;
1074     double yf = dy * A11 + dz * A21;
1075     double zf = dz * A22;
1076     xf = floor(abs(xf) + 0.5) * signum(-xf) + xf;
1077     yf = floor(abs(yf) + 0.5) * signum(-yf) + yf;
1078     zf = floor(abs(zf) + 0.5) * signum(-zf) + zf;
1079     dx = xf * Ai00 + yf * Ai10 + zf * Ai20;
1080     dy = yf * Ai11 + zf * Ai21;
1081     dz = zf * Ai22;
1082     return dx * dx + dy * dy + dz * dz;
1083   }
1084 
1085   /**
1086    * Minimum distance between two coordinates over all symmetry operators.
1087    *
1088    * @param xyzA Coordinate A
1089    * @param xyzB Coordinate B
1090    * @return Minimum distance in crystal
1091    */
1092   public double minDistOverSymOps(double[] xyzA, double[] xyzB) {
1093     double dist = 0;
1094     for (int i = 0; i < 3; i++) {
1095       double dx = xyzA[i] - xyzB[i];
1096       dist += (dx * dx);
1097     }
1098     var symB = new double[3];
1099     for (SymOp symOp : spaceGroup.symOps) {
1100       applySymOp(xyzB, symB, symOp);
1101       for (int i = 0; i < 3; i++) {
1102         symB[i] -= xyzA[i];
1103       }
1104       double d = image(symB);
1105       dist = min(d, dist);
1106     }
1107     return sqrt(dist);
1108   }
1109 
1110   /**
1111    * Strain the unit cell vectors.
1112    *
1113    * @param dStrain a 3x3 matrix of unitless Strain percentages.
1114    * @return True if the perturbation of cell vectors succeeds.
1115    */
1116   public boolean perturbCellVectors(double[][] dStrain) {
1117 
1118     double[][] AA = new double[3][3];
1119     double[][] newAi = new double[3][3];
1120 
1121     for (int i = 0; i < 3; i++) {
1122       for (int j = 0; j < 3; j++) {
1123         AA[i][j] = getUnitCell().Ai[i][j];
1124       }
1125     }
1126 
1127     for (int i = 0; i < 3; i++) {
1128       for (int j = 0; j < 3; j++) {
1129         for (int k = 0; k < 3; k++) {
1130           double Kronecker = 0.0;
1131           if (j == k) {
1132             Kronecker = 1.0;
1133           }
1134           // Aij' = Sum over K ( Kron_jk + dStrain_jk ) * Aij
1135           newAi[i][j] += (Kronecker + dStrain[j][k]) * AA[i][k];
1136         }
1137       }
1138     }
1139 
1140     return setCellVectors(newAi);
1141   }
1142 
1143   /**
1144    * Randomly set the unit cell vectors respecting the specified density.
1145    *
1146    * @param density The target density.
1147    * @param mass    The total mass of the asymmetric unit.
1148    * @return True if the perturbation of cell vectors succeeds.
1149    */
1150   public boolean randomParameters(double density, double mass) {
1151     double[] params = latticeSystem.resetUnitCellParams();
1152     boolean succeed = changeUnitCellParameters(params[0], params[1], params[2], params[3], params[4], params[5]);
1153     if (succeed) {
1154       setDensity(density, mass);
1155     }
1156     return succeed;
1157   }
1158 
1159   /**
1160    * Is this a finite system without periodic boundary conditions.
1161    *
1162    * @param aperiodic a boolean.
1163    */
1164   public void setAperiodic(boolean aperiodic) {
1165     this.aperiodic = aperiodic;
1166   }
1167 
1168   /**
1169    * Set the unit cell parameters from unit cell vectors.
1170    *
1171    * @param cellVectors 3x3 matrix of unit cell vectors.
1172    * @return The six unit cell parameters (a, b, c, alpha, beta, gamma).
1173    */
1174   public double[] getCellParametersFromVectors(double[][] cellVectors) {
1175     // Update a-, b-, and c-axis lengths.
1176     double aa = length(cellVectors[0]);
1177     double bb = length(cellVectors[1]);
1178     double cc = length(cellVectors[2]);
1179 
1180     // Update alpha, beta and gamma angles.
1181     double aalpha = toDegrees(acos(dot(cellVectors[1], cellVectors[2]) / (bb * cc)));
1182     double bbeta = toDegrees(acos(dot(cellVectors[0], cellVectors[2]) / (aa * cc)));
1183     double ggamma = toDegrees(acos(dot(cellVectors[0], cellVectors[1]) / (aa * bb)));
1184 
1185     // Load and return new cell parameters.
1186     double[] params = new double[6];
1187     params[0] = aa;
1188     params[1] = bb;
1189     params[2] = cc;
1190     params[3] = aalpha;
1191     params[4] = bbeta;
1192     params[5] = ggamma;
1193     return params;
1194   }
1195 
1196   /**
1197    * Set the unit cell vectors.
1198    *
1199    * @param cellVectors 3x3 matrix of cell vectors.
1200    * @return True if the perturbation of cell vectors succeeds.
1201    */
1202   public boolean setCellVectors(double[][] cellVectors) {
1203     double[] p = getCellParametersFromVectors(cellVectors);
1204     return changeUnitCellParameters(p[0], p[1], p[2], p[3], p[4], p[5]);
1205   }
1206 
1207   /**
1208    * Set the unit cell vectors. Scale lattice lengths if necessary to hit the target volume.
1209    *
1210    * @param cellVectors    3x3 matrix of cell vectors.
1211    * @param targetAUVolume the target volume for the new cell Vectors.
1212    * @return True if the perturbation of cell vectors succeeds.
1213    */
1214   public boolean setCellVectorsAndVolume(double[][] cellVectors, double targetAUVolume) {
1215     if (setCellVectors(cellVectors)) {
1216       double currentAUVolume = volume / getNumSymOps();
1217       double scale = cbrt(targetAUVolume / currentAUVolume);
1218       return changeUnitCellParameters(scale * a, scale * b, scale * c, alpha, beta, gamma);
1219     } else {
1220       return false;
1221     }
1222   }
1223 
1224   /**
1225    * Set the unit cell vectors. Scale lattice lengths if necessary to hit the target volume.
1226    *
1227    * @param density The target density.
1228    * @param mass    The total mass of the asymmetric unit.
1229    */
1230   public void setDensity(double density, double mass) {
1231     double currentDensity = getDensity(mass);
1232     double scale = cbrt(currentDensity / density);
1233     changeUnitCellParameters(a * scale, b * scale, c * scale, alpha, beta, gamma);
1234     currentDensity = getDensity(mass);
1235     if (logger.isLoggable(Level.FINE)) {
1236       logger.fine(format(" Updated density %6.3f (g/cc) with unit cell %s.", currentDensity, toShortString()));
1237     }
1238   }
1239 
1240   /**
1241    * Return a CRYST1 record useful for writing a PDB file.
1242    *
1243    * @return The CRYST1 record.
1244    */
1245   public String toCRYST1() {
1246     return format(
1247         "CRYST1%9.3f%9.3f%9.3f%7.2f%7.2f%7.2f %10s\n",
1248         a, b, c, alpha, beta, gamma, padRight(spaceGroup.pdbName, 10));
1249   }
1250 
1251   /**
1252    * toCartesianCoordinates
1253    *
1254    * @param n  an int.
1255    * @param xf an array of double for fractional x-coordinates.
1256    * @param yf an array of double for fractional y-coordinates.
1257    * @param zf an array of double for fractional z-coordinates.
1258    * @param x  an array of double for cartesian x-coordinates.
1259    * @param y  an array of double for cartesian y-coordinates.
1260    * @param z  an array of double for cartesian z-coordinates.
1261    */
1262   public void toCartesianCoordinates(
1263       int n, double[] xf, double[] yf, double[] zf, double[] x, double[] y, double[] z) {
1264     for (int i = 0; i < n; i++) {
1265       double xi = xf[i];
1266       double yi = yf[i];
1267       double zi = zf[i];
1268       x[i] = xi * Ai00 + yi * Ai10 + zi * Ai20;
1269       y[i] = yi * Ai11 + zi * Ai21;
1270       z[i] = zi * Ai22;
1271     }
1272   }
1273 
1274   /**
1275    * toCartesianCoordinates
1276    *
1277    * @param n    an int.
1278    * @param frac an array of double for fractional coordinates.
1279    * @param cart an array of double for cartesian coordinates.
1280    */
1281   public void toCartesianCoordinates(int n, double[] frac, double[] cart) {
1282     int i3 = 0;
1283     for (int i = 0; i < n; i++) {
1284       // Convert to cartesian coordinates.
1285       int iX = i3 + XX;
1286       int iY = i3 + YY;
1287       int iZ = i3 + ZZ;
1288       i3 += 3;
1289       double xf = frac[iX];
1290       double yf = frac[iY];
1291       double zf = frac[iZ];
1292       cart[iX] = xf * Ai00 + yf * Ai10 + zf * Ai20;
1293       cart[iY] = yf * Ai11 + zf * Ai21;
1294       cart[iZ] = zf * Ai22;
1295     }
1296   }
1297 
1298   /**
1299    * toCartesianCoordinates
1300    *
1301    * @param xf an array of double for fractional coordinate.
1302    * @param x  an array of double for cartesian coordinate.
1303    */
1304   public void toCartesianCoordinates(double[] xf, double[] x) {
1305     double fx = xf[0];
1306     double fy = xf[1];
1307     double fz = xf[2];
1308     x[0] = fx * Ai00 + fy * Ai10 + fz * Ai20;
1309     x[1] = fy * Ai11 + fz * Ai21;
1310     x[2] = fz * Ai22;
1311   }
1312 
1313   /**
1314    * toFractionalCoordinates
1315    *
1316    * @param n  an int.
1317    * @param x  an array of double for cartesian x-coordinates.
1318    * @param y  an array of double for cartesian y-coordinates.
1319    * @param z  an array of double for cartesian z-coordinates.
1320    * @param xf an array of double for fractional x-coordinates.
1321    * @param yf an array of double for fractional y-coordinates.
1322    * @param zf an array of double for fractional z-coordinates.
1323    */
1324   public void toFractionalCoordinates(
1325       int n, double[] x, double[] y, double[] z, double[] xf, double[] yf, double[] zf) {
1326     for (int i = 0; i < n; i++) {
1327       double xc = x[i];
1328       double yc = y[i];
1329       double zc = z[i];
1330       xf[i] = xc * A00 + yc * A10 + zc * A20;
1331       yf[i] = yc * A11 + zc * A21;
1332       zf[i] = zc * A22;
1333     }
1334   }
1335 
1336   /**
1337    * toFractionalCoordinates
1338    *
1339    * @param n    an int.
1340    * @param cart an array of double for cartesian coordinates.
1341    * @param frac an array of double for fractional coordinates.
1342    */
1343   public void toFractionalCoordinates(int n, double[] cart, double[] frac) {
1344     int i3 = 0;
1345     for (int i = 0; i < n; i++) {
1346       // Convert to fractional coordinates.
1347       int iX = i3 + XX;
1348       int iY = i3 + YY;
1349       int iZ = i3 + ZZ;
1350       i3 += 3;
1351       double xc = cart[iX];
1352       double yc = cart[iY];
1353       double zc = cart[iZ];
1354       frac[iX] = xc * A00 + yc * A10 + zc * A20;
1355       frac[iY] = yc * A11 + zc * A21;
1356       frac[iZ] = zc * A22;
1357     }
1358   }
1359 
1360   /**
1361    * toFractionalCoordinates
1362    *
1363    * @param x  an array of double for cartesian coordinate.
1364    * @param xf an array of double for fractional coordinate.
1365    */
1366   public void toFractionalCoordinates(double[] x, double[] xf) {
1367     double xc = x[0];
1368     double yc = x[1];
1369     double zc = x[2];
1370     xf[0] = xc * A00 + yc * A10 + zc * A20;
1371     xf[1] = yc * A11 + zc * A21;
1372     xf[2] = zc * A22;
1373   }
1374 
1375   /**
1376    * toPrimaryCell
1377    *
1378    * @param in  an array of double.
1379    * @param out an array of double.
1380    */
1381   public void toPrimaryCell(double[] in, double[] out) {
1382     toFractionalCoordinates(in, out);
1383     out[0] = mod(out[0], 1.0);
1384     out[1] = mod(out[1], 1.0);
1385     out[2] = mod(out[2], 1.0);
1386     toCartesianCoordinates(out, out);
1387   }
1388 
1389   /**
1390    * A String containing the unit cell parameters.
1391    *
1392    * @return A string with the unit cell parameters.
1393    */
1394   public String toShortString() {
1395     return format("%6.2f %6.2f %6.2f %6.2f %6.2f %6.2f", a, b, c, alpha, beta, gamma);
1396   }
1397 
1398   /**
1399    * {@inheritDoc}
1400    */
1401   @Override
1402   public String toString() {
1403     StringBuilder sb = new StringBuilder("\n Unit Cell\n");
1404     sb.append(format("  A-axis length:                       %8.3f\n", a));
1405     sb.append(format("  B-axis length:                       %8.3f\n", b));
1406     sb.append(format("  C-axis length:                       %8.3f\n", c));
1407     sb.append(format("  Alpha:                               %8.3f\n", alpha));
1408     sb.append(format("  Beta:                                %8.3f\n", beta));
1409     sb.append(format("  Gamma:                               %8.3f\n", gamma));
1410     sb.append("  Space group\n");
1411     sb.append(format("   Number:                                  %3d\n", spaceGroup.number));
1412     sb.append(format("   Symbol:                             %8s\n", spaceGroup.shortName));
1413     sb.append(
1414         format("   Number of Symmetry Operators:            %3d\n", spaceGroup.getNumberOfSymOps()));
1415     sb.append("  Lattice Vectors\n");
1416     sb.append(format("   A:              %8.3f, %8.3f, %8.3f\n", Ai00, 0.0, 0.0));
1417     sb.append(format("   B:              %8.3f, %8.3f, %8.3f\n", Ai10, Ai11, 0.0));
1418     sb.append(format("   C:              %8.3f, %8.3f, %8.3f\n", Ai20, Ai21, Ai22));
1419     sb.append("  Reciprocal Lattice Vectors\n");
1420     sb.append(format("   A*:             %8.3f, %8.3f, %8.3f\n", A00, A10, A20));
1421     sb.append(format("   B*:             %8.3f, %8.3f, %8.3f\n", 0.0, A11, A21));
1422     sb.append(format("   C*:             %8.3f, %8.3f, %8.3f", 0.0, 0.0, A22));
1423     return sb.toString();
1424   }
1425 
1426   /**
1427    * Update all Crystal variables that are a function of unit cell parameters.
1428    */
1429   public final void updateCrystal() {
1430 
1431     double cos_alpha;
1432     double sin_beta;
1433     double cos_beta;
1434     double sin_gamma;
1435     double cos_gamma;
1436     double beta_term;
1437     double gamma_term;
1438 
1439     switch (crystalSystem) {
1440       default -> {
1441         // CUBIC, ORTHORHOMBIC, TETRAGONAL
1442         cos_alpha = 0.0;
1443         cos_beta = 0.0;
1444         sin_gamma = 1.0;
1445         cos_gamma = 0.0;
1446         beta_term = 0.0;
1447         gamma_term = 1.0;
1448         volume = a * b * c;
1449         dVdA = b * c;
1450         dVdB = a * c;
1451         dVdC = a * b;
1452         dVdAlpha = 0.0;
1453         dVdBeta = 0.0;
1454         dVdGamma = 0.0;
1455       }
1456       case MONOCLINIC -> {
1457         cos_alpha = 0.0;
1458         sin_beta = sin(toRadians(beta));
1459         cos_beta = cos(toRadians(beta));
1460         sin_gamma = 1.0;
1461         cos_gamma = 0.0;
1462         beta_term = 0.0;
1463         gamma_term = sin_beta;
1464         volume = sin_beta * a * b * c;
1465         dVdA = sin_beta * b * c;
1466         dVdB = sin_beta * a * c;
1467         dVdC = sin_beta * a * b;
1468         dVdAlpha = 0.0;
1469         dVdBeta = cos_beta * a * b * c;
1470         dVdGamma = 0.0;
1471       }
1472       case HEXAGONAL, TRICLINIC, TRIGONAL -> {
1473         double sin_alpha = sin(toRadians(alpha));
1474         cos_alpha = cos(toRadians(alpha));
1475         sin_beta = sin(toRadians(beta));
1476         cos_beta = cos(toRadians(beta));
1477         sin_gamma = sin(toRadians(gamma));
1478         cos_gamma = cos(toRadians(gamma));
1479         beta_term = (cos_alpha - cos_beta * cos_gamma) / sin_gamma;
1480         gamma_term = sqrt(sin_beta * sin_beta - beta_term * beta_term);
1481         volume = sin_gamma * gamma_term * a * b * c;
1482         dVdA = sin_gamma * gamma_term * b * c;
1483         dVdB = sin_gamma * gamma_term * a * c;
1484         dVdC = sin_gamma * gamma_term * a * b;
1485         double dbeta = 2.0 * sin_beta * cos_beta
1486             - (2.0 * (cos_alpha - cos_beta * cos_gamma) * sin_beta * cos_gamma)
1487             / (sin_gamma * sin_gamma);
1488         double dgamma1 = -2.0 * (cos_alpha - cos_beta * cos_gamma) * cos_beta / sin_gamma;
1489         double dgamma2 = cos_alpha - cos_beta * cos_gamma;
1490         dgamma2 *= dgamma2 * 2.0 * cos_gamma / (sin_gamma * sin_gamma * sin_gamma);
1491         dVdAlpha = (cos_alpha - cos_beta * cos_gamma) * sin_alpha
1492             / (sin_gamma * gamma_term) * a * b * c;
1493         dVdBeta = 0.5 * sin_gamma * dbeta / gamma_term * a * b * c;
1494         dVdGamma = (cos_gamma * gamma_term + 0.5 * sin_gamma * (dgamma1 + dgamma2) / gamma_term)
1495             * a * b * c;
1496       }
1497     }
1498 
1499     G[0][0] = a * a;
1500     G[0][1] = a * b * cos_gamma;
1501     G[0][2] = a * c * cos_beta;
1502     G[1][0] = G[0][1];
1503     G[1][1] = b * b;
1504     G[1][2] = b * c * cos_alpha;
1505     G[2][0] = G[0][2];
1506     G[2][1] = G[1][2];
1507     G[2][2] = c * c;
1508 
1509     // invert G to yield Gstar
1510     RealMatrix m = new Array2DRowRealMatrix(G, true);
1511     m = new LUDecomposition(m).getSolver().getInverse();
1512     Gstar = m.getData();
1513 
1514     // a is the first row of A^(-1).
1515     Ai[0][0] = a;
1516     Ai[0][1] = 0.0;
1517     Ai[0][2] = 0.0;
1518     // b is the second row of A^(-1).
1519     Ai[1][0] = b * cos_gamma;
1520     Ai[1][1] = b * sin_gamma;
1521     Ai[1][2] = 0.0;
1522     // c is the third row of A^(-1).
1523     Ai[2][0] = c * cos_beta;
1524     Ai[2][1] = c * beta_term;
1525     Ai[2][2] = c * gamma_term;
1526 
1527     // A-axis in Cartesian coordinates.
1528     Ai00 = Ai[0][0];
1529     // B-axis in Cartesian coordinates.
1530     Ai10 = Ai[1][0];
1531     Ai11 = Ai[1][1];
1532     // C-axis in Cartesian coordinates.
1533     Ai20 = Ai[2][0];
1534     Ai21 = Ai[2][1];
1535     Ai22 = Ai[2][2];
1536 
1537     // Invert A^-1 to get A
1538     m = new Array2DRowRealMatrix(Ai, true);
1539     m = new LUDecomposition(m).getSolver().getInverse();
1540     A = m.getData();
1541 
1542     // The columns of A are the reciprocal lattice vectors
1543     // A* reciprocal lattice vector.
1544     A00 = A[0][0];
1545     A10 = A[1][0];
1546     A20 = A[2][0];
1547     // B* reciprocal lattice vector (A01 is zero).
1548     A11 = A[1][1];
1549     A21 = A[2][1];
1550     // C* reciprocal lattice vector (A02 and A12 are zero).
1551     A22 = A[2][2];
1552 
1553     // Reciprocal lattice vector lengths
1554     double aStar = 1.0 / sqrt(A00 * A00 + A10 * A10 + A20 * A20);
1555     double bStar = 1.0 / sqrt(A11 * A11 + A21 * A21);
1556     double cStar = 1.0 / sqrt(A22 * A22);
1557 
1558     /*
1559       In Amber (PMEMD), interfacial diameters are defined by the dot product of the
1560       real and reciprocal vectors multiplied by half the reciprocal lengths.
1561       However, the real and reciprocal vectors are orthonormal (A dot A* = 1.0).
1562 
1563       Here, the interfacial radii are defined by reciprocal lengths divided by 2.
1564 
1565       This approach concurs with the Tinker code for triclinic cells:
1566         xlimit = volbox / (2.0d0*ybox*zbox*alpha_sin)
1567         ylimit = volbox / (2.0d0*xbox*zbox*beta_sin)
1568         zlimit = volbox / (2.0d0*xbox*ybox*gamma_sin)
1569       where volbox is the volume of the periodic box in Angstroms^3,
1570       xbox is the length of the a-axis in Angstroms,
1571       and alpha_sin is the sine of the alpha periodic box angle
1572      */
1573 
1574     // Divide by 2 to get the interfacial radii.
1575     interfacialRadiusA = aStar / 2.0;
1576     interfacialRadiusB = bStar / 2.0;
1577     interfacialRadiusC = cStar / 2.0;
1578 
1579     if (logger.isLoggable(Level.FINEST)) {
1580       // Compare the interfacial radii with the half lattice lengths.
1581       logger.finest(format(" Half lattice lengths: (%12.6f, %12.6f, %12.6f)",
1582           a / 2.0, b / 2.0, c / 2.0));
1583       logger.finest(format(" Interfacial radii:    (%12.6f, %12.6f, %12.6f)",
1584           interfacialRadiusA, interfacialRadiusB, interfacialRadiusC));
1585     }
1586 
1587     List<SymOp> symOps = spaceGroup.symOps;
1588     int nSymm = symOps.size();
1589     if (symOpsCartesian == null) {
1590       symOpsCartesian = new ArrayList<>(nSymm);
1591     } else {
1592       symOpsCartesian.clear();
1593     }
1594 
1595     RealMatrix toFrac = new Array2DRowRealMatrix(A);
1596     RealMatrix toCart = new Array2DRowRealMatrix(Ai);
1597     for (SymOp symOp : symOps) {
1598       m = new Array2DRowRealMatrix(symOp.rot);
1599       // rot_c = A^(-1).rot_f.A
1600       RealMatrix rotMat = m.preMultiply(toCart.transpose()).multiply(toFrac.transpose());
1601       // tr_c = tr_f.A^(-1)
1602       double[] tr = toCart.preMultiply(symOp.tr);
1603       symOpsCartesian.add(new SymOp(rotMat.getData(), tr));
1604     }
1605   }
1606 }