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 java.util.List;
41 import java.util.Objects;
42 import java.util.logging.Level;
43 import java.util.logging.Logger;
44
45 import static java.lang.String.format;
46
47 /**
48 * The ReplicatesCrystal class extends Crystal to generate additional symmetry operators needed to
49 * describe a "replicated" super-cell.
50 * <p>
51 * The replicated crystal cell edges are of length {l*a, m*b, n*c} where l, m and n are integers and
52 * a, b and c are the original unit cell edge lengths.
53 * <br>
54 * The replicates integers l, m and n are chosen large enough for the ReplicatesCrystal to allow
55 * consistent application of the minimum image convention. This is ensured by increasing l, m and/or
56 * n until a sphere of necessary radius fits entirely inside the ReplicatedCrystal.
57 * <br>
58 *
59 * @author Michael J. Schnieders
60 * @see Crystal
61 * @since 1.0
62 */
63 public class ReplicatesCrystal extends Crystal {
64
65 /**
66 * The logger.
67 */
68 private static final Logger logger = Logger.getLogger(ReplicatesCrystal.class.getName());
69 /**
70 * The base unit cell for the system being simulated.
71 */
72 private final Crystal unitCell;
73 /**
74 * The cut-off distance in Angstroms.
75 */
76 private final double cutOff;
77 /**
78 * The number of replicates along the a-axis.
79 */
80 private int l;
81 /**
82 * The number of replicates along the b-axis.
83 */
84 private int m;
85 /**
86 * The number of replicates along the c-axis.
87 */
88 private int n;
89
90 /**
91 * Constructor for a ReplicatesCrystal.
92 *
93 * @param unitCell The base unit cell.
94 * @param l Number of replicates along the a-axis.
95 * @param m Number of replicates along the b-axis.
96 * @param n Number of replicates along the c-axis.
97 * @param cutOff2 Twice the cut-off distance.
98 * @since 1.0
99 */
100 public ReplicatesCrystal(Crystal unitCell, int l, int m, int n, double cutOff2) {
101 super(unitCell.a * l, unitCell.b * m, unitCell.c * n, unitCell.alpha, unitCell.beta, unitCell.gamma, unitCell.spaceGroup.shortName);
102 this.unitCell = unitCell;
103
104 assert (l >= 1);
105 assert (m >= 1);
106 assert (n >= 1);
107 this.l = l;
108 this.m = m;
109 this.n = n;
110 this.cutOff = cutOff2 / 2.0;
111
112 /*
113 At this point, the ReplicatesCrystal references a SpaceGroup instance
114 whose symmetry operators are inconsistent. This is corrected by
115 generating symmetry operators to fill up the ReplicatesCrystal based
116 on the asymmetric unit.
117 */
118 updateReplicateOperators();
119 }
120
121 /**
122 * The number of replicates along the a-axis.
123 *
124 * @return The number of replicates along the a-axis.
125 */
126 public int getL() {
127 return l;
128 }
129
130 /**
131 * The number of replicates along the b-axis.
132 *
133 * @return The number of replicates along the b-axis.
134 */
135 public int getM() {
136 return m;
137 }
138
139 /**
140 * The number of replicates along the c-axis.
141 *
142 * @return The number of replicates along the c-axis.
143 */
144 public int getN() {
145 return n;
146 }
147
148 /**
149 * Returns a ReplicatesCrystal large enough to satisfy the minimum image convention for the
150 * specified unit cell and cutoff criteria. If the unit cell is already sufficiently large, then it
151 * is returned.
152 *
153 * @param unitCell The unit cell of the crystal.
154 * @param cutOff2 Two times the cutoff distance.
155 * @return A Crystal or ReplicatesCrystal large enough to satisfy the minimum image convention.
156 */
157 public static Crystal replicatesCrystalFactory(Crystal unitCell, double cutOff2) {
158
159 return replicatesCrystalFactory(unitCell, cutOff2, new int[3]);
160 }
161
162 /**
163 * Returns a ReplicatesCrystal large enough to satisfy the minimum image convention for the
164 * specified unit cell and cutoff criteria. If the unit cell is already sufficiently large, then it
165 * is returned.
166 *
167 * @param unitCell The unit cell of the crystal.
168 * @param cutOff2 Two times the cutoff distance.
169 * @param replicatesVector The number of replicates along the a, b, and c axes.
170 * @return A Crystal or ReplicatesCrystal large enough to satisfy the minimum image convention.
171 */
172 public static Crystal replicatesCrystalFactory(Crystal unitCell, double cutOff2, int[] replicatesVector) {
173
174 if (unitCell == null || unitCell.aperiodic()) {
175 replicatesVector[0] = 0;
176 replicatesVector[1] = 0;
177 replicatesVector[2] = 0;
178 return unitCell;
179 }
180
181 int l = 1;
182 int m = 1;
183 int n = 1;
184
185 double cutOff = cutOff2 / 2.0;
186
187 while (unitCell.interfacialRadiusA * l < cutOff) {
188 l++;
189 }
190 while (unitCell.interfacialRadiusB * m < cutOff) {
191 m++;
192 }
193 while (unitCell.interfacialRadiusC * n < cutOff) {
194 n++;
195 }
196
197 replicatesVector[0] = l;
198 replicatesVector[1] = m;
199 replicatesVector[2] = n;
200 return new ReplicatesCrystal(unitCell, l, m, n, cutOff2);
201
202 }
203
204 /**
205 * Change the cell vectors for the base unit cell, which is followed by an update of the
206 * ReplicateCrystal parameters and possibly the number of replicated cells.
207 *
208 * @param cellVectors 3x3 matrix of cell vectors.
209 * @return True if the perturbation of cell vectors succeeds.
210 */
211 public boolean setCellVectors(double[][] cellVectors) {
212
213 // First, update the parameters of the unit cell.
214 if (unitCell.setCellVectors(cellVectors)) {
215
216 // Then, update the parameters of the ReplicatesCrystal and possibly the number of replicates.
217 return updateReplicatesDimensions();
218 }
219 return false;
220 }
221
222 /**
223 * Change the cell vectors and volume for the base unit cell, which is followed by an update of the
224 * ReplicateCrystal parameters and possibly the number of replicated cells.
225 *
226 * @param cellVectors 3x3 matrix of cell vectors.
227 * @param targetAUVolume the target volume for the new cell Vectors.
228 * @return True if the perturbation of cell vectors succeeds.
229 */
230 public boolean setCellVectorsAndVolume(double[][] cellVectors, double targetAUVolume) {
231 // First, update the parameters of the unit cell.
232 if (unitCell.setCellVectorsAndVolume(cellVectors, targetAUVolume)) {
233
234 // Then, update the parameters of the ReplicatesCrystal and possibly the number of replicates.
235 return updateReplicatesDimensions();
236 }
237 return false;
238 }
239
240 /**
241 * Change the cell parameters for the base unit cell, which is followed by an update of the
242 * ReplicateCrystal parameters and possibly the number of replicated cells.
243 *
244 * @param a The length of the a-axis for the base unit cell (in Angstroms).
245 * @param b The length of the b-axis for the base unit cell (in Angstroms).
246 * @param c The length of the c-axis for the base unit cell (in Angstroms).
247 * @param alpha The angle between the b-axis and c-axis (in Degrees).
248 * @param beta The angle between the a-axis and c-axis (in Degrees).
249 * @param gamma The angle between the a-axis and b-axis (in Degrees).
250 * @return True is returned if the unit cell and replicates cell are updated successfully.
251 */
252 @Override
253 public boolean changeUnitCellParameters(double a, double b, double c, double alpha, double beta, double gamma) {
254
255 // First, update the parameters of the unit cell.
256 if (unitCell.changeUnitCellParameters(a, b, c, alpha, beta, gamma)) {
257
258 // Then, update the parameters of the ReplicatesCrystal and possibly the number of replicates.
259 return updateReplicatesDimensions();
260 }
261 return false;
262 }
263
264 /**
265 * Change the cell parameters for the base unit cell, which is followed by an update of the
266 * ReplicateCrystal parameters and possibly the number of replicated cells.
267 *
268 * @param a The length of the a-axis for the base unit cell (in Angstroms).
269 * @param b The length of the b-axis for the base unit cell (in Angstroms).
270 * @param c The length of the c-axis for the base unit cell (in Angstroms).
271 * @param alpha The angle between the b-axis and c-axis (in Degrees).
272 * @param beta The angle between the a-axis and c-axis (in Degrees).
273 * @param gamma The angle between the a-axis and b-axis (in Degrees).
274 * @param targetAUVolume the target volume for the new cell Vectors.
275 * @return True is returned if the unit cell and replicates cell are updated successfully.
276 */
277 @Override
278 public boolean changeUnitCellParametersAndVolume(double a, double b, double c, double alpha, double beta, double gamma, double targetAUVolume) {
279
280 // First, update the parameters of the unit cell.
281 if (unitCell.changeUnitCellParametersAndVolume(a, b, c, alpha, beta, gamma, targetAUVolume)) {
282
283 // Then, update the parameters of the ReplicatesCrystal and possibly the number of replicates.
284 return updateReplicatesDimensions();
285 }
286 return false;
287 }
288
289
290 /**
291 * Two crystals are equal only if all unit cell parameters are exactly the same.
292 */
293 @Override
294 public boolean equals(Object o) {
295 if (this == o) {
296 return true;
297 }
298 if (o == null || getClass() != o.getClass()) {
299 return false;
300 }
301 ReplicatesCrystal replicatesCrystal = (ReplicatesCrystal) o;
302 return (
303 Objects.equals(unitCell, replicatesCrystal.unitCell)
304 && a == replicatesCrystal.a
305 && b == replicatesCrystal.b
306 && c == replicatesCrystal.c
307 && alpha == replicatesCrystal.alpha
308 && beta == replicatesCrystal.beta
309 && gamma == replicatesCrystal.gamma
310 && l == replicatesCrystal.l
311 && m == replicatesCrystal.m
312 && n == replicatesCrystal.n
313 && spaceGroup.number == replicatesCrystal.spaceGroup.number
314 && spaceGroup.symOps.size() == replicatesCrystal.spaceGroup.symOps.size());
315 }
316
317 /**
318 * {@inheritDoc}
319 */
320 @Override
321 public boolean getCheckRestrictions() {
322 return unitCell.getCheckRestrictions();
323 }
324
325 /**
326 * {@inheritDoc}
327 */
328 @Override
329 public void setCheckRestrictions(boolean checkRestrictions) {
330 this.checkRestrictions = checkRestrictions;
331 unitCell.setCheckRestrictions(checkRestrictions);
332 }
333
334 /**
335 * Return the density of the ReplicatesCrystal.
336 *
337 * @param mass The mass of the ReplicatesCrystal.
338 * @return The density.
339 */
340 public double getDensity(double mass) {
341 return unitCell.getDensity(mass);
342 }
343
344 /**
345 * {@inheritDoc}
346 *
347 * <p>Returns the unit cell for this ReplicatesCrystal. This is useful for the reciprocal space
348 * portion of PME that operates on the unit cell even though the real space cutoff requires a
349 * ReplicatesCrystal.
350 */
351 @Override
352 public Crystal getUnitCell() {
353 return unitCell;
354 }
355
356 /**
357 * Update the ReplicatesCrystal using random parameters with the target density.
358 *
359 * @param density Target density.
360 * @param mass Mass of the ReplicatesCrystal.
361 */
362 public boolean randomParameters(double density, double mass) {
363 boolean succeed = unitCell.randomParameters(density, mass);
364 if (succeed) {
365 updateReplicatesDimensions();
366 }
367 return succeed;
368 }
369
370 /**
371 * Update the ReplicatesCrystal dimensions to the target density.
372 *
373 * @param density Target density.
374 * @param mass Mass of the ReplicatesCrystal.
375 */
376 public void setDensity(double density, double mass) {
377 unitCell.setDensity(density, mass);
378 updateReplicatesDimensions();
379 }
380
381 /**
382 * {@inheritDoc}
383 *
384 * <p>Include information about the base unit cell and replicates cell.
385 */
386 @Override
387 public String toString() {
388 StringBuilder sb = new StringBuilder(unitCell.toString());
389 // Only log the replicates cell if there is more than one replicate.
390 if (l * m * n > 1) {
391 sb.append("\n\n Replicates Cell\n");
392 sb.append(format(" Dimension: (%3d x%3d x%3d)\n", l, m, n));
393 sb.append(format(" A-axis: %8.3f\n", a));
394 sb.append(format(" B-axis: %8.3f\n", b));
395 sb.append(format(" C-axis: %8.3f\n", c));
396 sb.append(format(" Alpha: %8.3f\n", alpha));
397 sb.append(format(" Beta: %8.3f\n", beta));
398 sb.append(format(" Gamma: %8.3f\n", gamma));
399 sb.append(format(" Total Symmetry Operators: %8d", spaceGroup.getNumberOfSymOps()));
400 }
401 return sb.toString();
402 }
403
404 /**
405 * A String containing the replicated unit cell parameters.
406 *
407 * @return A string with the unit cell parameters.
408 */
409 public String toShortString() {
410 return format("%6.2f %6.2f %6.2f %6.2f %6.2f %6.2f (%3d x%3d x%3d) ", a, b, c, alpha, beta, gamma, l, m, n);
411 }
412
413 private boolean updateReplicatesDimensions() {
414 // Then, update the parameters of the ReplicatesCrystal and possibly the number of replicates.
415 int ll = 1;
416 int mm = 1;
417 int nn = 1;
418
419 while (unitCell.interfacialRadiusA * ll < cutOff) {
420 ll++;
421 }
422 while (unitCell.interfacialRadiusB * mm < cutOff) {
423 mm++;
424 }
425 while (unitCell.interfacialRadiusC * nn < cutOff) {
426 nn++;
427 }
428 if (super.changeUnitCellParameters(unitCell.a * ll, unitCell.b * mm, unitCell.c * nn, unitCell.alpha, unitCell.beta, unitCell.gamma)) {
429 l = ll;
430 m = mm;
431 n = nn;
432 updateReplicateOperators();
433 return true;
434 }
435
436 return false;
437 }
438
439 /**
440 * Update the list of symmetry operators used to generate the replicates super-cell from the
441 * asymmetric unit.
442 */
443 private void updateReplicateOperators() {
444 List<SymOp> symOps = spaceGroup.symOps;
445
446 // First, we remove the existing symmetry operators.
447 symOps.clear();
448
449 /*
450 Now create symmetry operators for each replicate. Note that the first
451 symOp is still equivalent to the asymmetric unit and the first set of
452 symOps are still equivalent to the unit cell.
453 */
454 double dX = 1.0 / (double) l;
455 double dY = 1.0 / (double) m;
456 double dZ = 1.0 / (double) n;
457 int symOpCount = 0;
458 List<SymOp> ucSymOps = unitCell.spaceGroup.symOps;
459 for (int i = 0; i < l; i++) {
460 for (int j = 0; j < m; j++) {
461 for (int k = 0; k < n; k++) {
462 int ii = 0;
463 for (SymOp symOp : ucSymOps) {
464 double[] repTrans = new double[3];
465 repTrans[0] = (symOp.tr[0] + i) * dX;
466 repTrans[1] = (symOp.tr[1] + j) * dY;
467 repTrans[2] = (symOp.tr[2] + k) * dZ;
468 SymOp repSymOp = new SymOp(symOp.rot, repTrans, new int[]{i, j, k});
469 symOps.add(repSymOp);
470 if (logger.isLoggable(Level.FINEST)) {
471 logger.finest(format("\n SymOp %d (%2d,%2d,%2d): %d", symOpCount, i, j, k, ii));
472 logger.finest(repSymOp.toString());
473 }
474 ii++;
475 symOpCount++;
476 }
477 }
478 }
479 }
480 }
481 }