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-2024.
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.algorithms.dynamics;
39  
40  import ffx.crystal.Crystal;
41  import ffx.crystal.CrystalPotential;
42  import ffx.crystal.SpaceGroup;
43  import ffx.numerics.Potential;
44  import ffx.numerics.math.RunningStatistics;
45  import ffx.potential.MolecularAssembly;
46  import ffx.potential.bonded.Atom;
47  import ffx.potential.parsers.XYZFilter;
48  import org.apache.commons.io.FilenameUtils;
49  
50  import java.io.File;
51  import java.util.ArrayList;
52  import java.util.List;
53  import java.util.logging.Level;
54  import java.util.logging.Logger;
55  
56  import static ffx.crystal.LatticeSystem.check;
57  import static ffx.numerics.math.ScalarMath.mirrorDegrees;
58  import static ffx.utilities.Constants.*;
59  import static java.lang.String.format;
60  import static org.apache.commons.math3.util.FastMath.*;
61  
62  /**
63   * The Barostat class maintains constant pressure using random trial moves in lattice parameters,
64   * which are consistent with the space group.
65   *
66   * <p>Frenkel and Smit, "Understanding Molecular Simulation, 2nd Edition", Academic Press, San
67   * Diego, CA, 2002; Section 5.4
68   *
69   * @author Michael J. Schnieders
70   */
71  public class Barostat implements CrystalPotential {
72  
73    private static final Logger logger = Logger.getLogger(Barostat.class.getName());
74  
75    /**
76     * MolecularAssembly being simulated.
77     */
78    private final MolecularAssembly molecularAssembly;
79    /**
80     * Boundary conditions and symmetry operators (could be a ReplicatedCrystal).
81     */
82    private Crystal crystal;
83    /**
84     * The unit cell.
85     */
86    private Crystal unitCell;
87    /**
88     * Mass of the system.
89     */
90    private final double mass;
91    /**
92     * ForceFieldEnergy that describes the system.
93     */
94    private final CrystalPotential potential;
95    /**
96     * Atomic coordinates.
97     */
98    private final double[] x;
99    /**
100    * The number of space group symmetry operators.
101    */
102   private final int nSymm;
103   /**
104    * Number of independent molecules in the simulation cell.
105    */
106   private final SpaceGroup spaceGroup;
107   /**
108    * Number of molecules in the systems.
109    */
110   private final int nMolecules;
111   /**
112    * Ideal gas constant * temperature (kcal/mol).
113    */
114   private double kT;
115   /**
116    * Sampling pressure (atm)
117    */
118   private double pressure = 1.0;
119   /**
120    * Only isotropic MC moves.
121    */
122   private boolean isotropic = false;
123   /**
124    * Flag to turn the Barostat on or off. If false, MC moves will not be tried.
125    */
126   private boolean active = true;
127   /**
128    * Default angle move (degrees).
129    */
130   private double maxAngleMove = 0.5;
131   /**
132    * Maximum volume move (Angstroms^3).
133    */
134   private double maxVolumeMove = 1.0;
135   /**
136    * Minimum density constraint.
137    */
138   private double minDensity = 0.75;
139   /**
140    * Maximum density constraint.
141    */
142   private double maxDensity = 1.60;
143   /**
144    * Number of energy evaluations between application of MC moves.
145    */
146   private int meanBarostatInterval = 1;
147   /**
148    * A counter for the number of barostat calls.
149    */
150   private int barostatCount = 0;
151   /**
152    * Number of unit cell Monte Carlo moves attempted.
153    */
154   private final RunningStatistics unitCellMoves = new RunningStatistics();
155   /**
156    * Number of lattice axis Monte Carlo moves attempted.
157    */
158   private final RunningStatistics sideMoves = new RunningStatistics();
159   /**
160    * Number of Monte Carlo moves attempted.
161    */
162   private final RunningStatistics angleMoves = new RunningStatistics();
163   /**
164    * Energy STATE.
165    */
166   private STATE state = STATE.BOTH;
167   /**
168    * Barostat move type.
169    */
170   private MoveType moveType = MoveType.SIDE;
171   /**
172    * True when a Monte Carlo move is accepted.
173    */
174   private boolean moveAccepted = false;
175   /**
176    * Current density value.
177    */
178   private double currentDensity = 0;
179   /**
180    * Current A-axis value.
181    */
182   private double a = 0;
183   /**
184    * Current B-axis value.
185    */
186   private double b = 0;
187   /**
188    * Current C-axis value.
189    */
190   private double c = 0;
191   /**
192    * Current alpha value.
193    */
194   private double alpha = 0;
195   /**
196    * Current beta value.
197    */
198   private double beta = 0;
199   /**
200    * Current gamma value.
201    */
202   private double gamma = 0;
203   /**
204    * A-axis statistics.
205    */
206   private final RunningStatistics aStats = new RunningStatistics();
207   /**
208    * B-axis statistics.
209    */
210   private final RunningStatistics bStats = new RunningStatistics();
211   /**
212    * C-axis statistics.
213    */
214   private final RunningStatistics cStats = new RunningStatistics();
215   /**
216    * Alpha statistics.
217    */
218   private final RunningStatistics alphaStats = new RunningStatistics();
219   /**
220    * Beta statistics.
221    */
222   private final RunningStatistics betaStats = new RunningStatistics();
223   /**
224    * Gamma statistics.
225    */
226   private final RunningStatistics gammaStats = new RunningStatistics();
227   /**
228    * Density statistics.
229    */
230   private final RunningStatistics densityStats = new RunningStatistics();
231   /**
232    * Barostat print frequency.
233    */
234   private int printFrequency = 1000;
235 
236   /**
237    * Initialize the Barostat.
238    *
239    * @param molecularAssembly The molecular assembly to apply the MC barostat to.
240    * @param potential         a {@link ffx.crystal.CrystalPotential} object.
241    */
242   public Barostat(MolecularAssembly molecularAssembly, CrystalPotential potential) {
243     this(molecularAssembly, potential, 298.15);
244   }
245 
246   /**
247    * Initialize the Barostat.
248    *
249    * @param molecularAssembly The molecular assembly to apply the MC barostat to.
250    * @param potential         a {@link ffx.crystal.CrystalPotential} object.
251    * @param temperature       The Metropolis Monte Carlo temperature (Kelvin).
252    */
253   public Barostat(MolecularAssembly molecularAssembly, CrystalPotential potential,
254                   double temperature) {
255 
256     this.molecularAssembly = molecularAssembly;
257     this.potential = potential;
258     this.kT = temperature * R;
259 
260     crystal = potential.getCrystal();
261     unitCell = crystal.getUnitCell();
262     spaceGroup = unitCell.spaceGroup;
263     // Atoms in the system.
264     Atom[] atoms = molecularAssembly.getAtomArray();
265     // Number of atoms.
266     int nAtoms = atoms.length;
267     nSymm = spaceGroup.getNumberOfSymOps();
268     mass = molecularAssembly.getMass();
269     x = new double[3 * nAtoms];
270     nMolecules = molecularAssembly.fractionalCount();
271   }
272 
273   /**
274    * density.
275    *
276    * @return a double.
277    */
278   public double density() {
279     return (mass * nSymm / AVOGADRO) * (1.0e24 / unitCell.volume);
280   }
281 
282   /**
283    * {@inheritDoc}
284    */
285   @Override
286   public boolean destroy() {
287     // Nothing at this level to destroy.
288     return potential.destroy();
289   }
290 
291   /**
292    * {@inheritDoc}
293    */
294   @Override
295   public double energy(double[] x) {
296     // Do not apply the Barostat for energy only evaluations.
297     return potential.energy(x);
298   }
299 
300   /**
301    * {@inheritDoc}
302    */
303   @Override
304   public double energyAndGradient(double[] x, double[] g) {
305 
306     // Calculate the energy and gradient as usual.
307     double energy = potential.energyAndGradient(x, g);
308 
309     // Apply the barostat during computation of slowly varying forces.
310     if (active && state != STATE.FAST) {
311       if (random() < (1.0 / meanBarostatInterval)) {
312 
313         // Attempt to change the unit cell parameters.
314         moveAccepted = false;
315 
316         applyBarostat(energy);
317 
318         // Collect Statistics.
319         collectStats();
320 
321         // If a move was accepted, then re-calculate the gradient so
322         // that it's consistent with the current unit cell parameters.
323         if (moveAccepted) {
324           energy = potential.energyAndGradient(x, g);
325         }
326       }
327     }
328     return energy;
329   }
330 
331   /**
332    * Restrict the MC Barostat to isotropic moves. The lattice angles are held fixed, and lattice
333    * lengths are scaled equally.
334    *
335    * @return Returns true if only isotropic moves are allowed.
336    */
337   public boolean isIsotropic() {
338     return isotropic;
339   }
340 
341   /**
342    * Restrict the MC Barostat to isotropic moves. The lattice angles are held fixed, and lattice
343    * lengths are scaled equally.
344    *
345    * @param isotropic If true, if only isotropic moves are allowed.
346    */
347   public void setIsotropic(boolean isotropic) {
348     this.isotropic = isotropic;
349   }
350 
351   /**
352    * {@inheritDoc}
353    */
354   @Override
355   public double[] getAcceleration(double[] acceleration) {
356     return potential.getAcceleration(acceleration);
357   }
358 
359   /**
360    * {@inheritDoc}
361    */
362   @Override
363   public double[] getCoordinates(double[] parameters) {
364     return potential.getCoordinates(parameters);
365   }
366 
367   /**
368    * {@inheritDoc}
369    */
370   @Override
371   public Crystal getCrystal() {
372     return potential.getCrystal();
373   }
374 
375   /**
376    * {@inheritDoc}
377    */
378   @Override
379   public void setCrystal(Crystal crystal) {
380     potential.setCrystal(crystal);
381   }
382 
383   /**
384    * {@inheritDoc}
385    */
386   @Override
387   public STATE getEnergyTermState() {
388     return state;
389   }
390 
391   /**
392    * {@inheritDoc}
393    */
394   @Override
395   public void setEnergyTermState(STATE state) {
396     this.state = state;
397     potential.setEnergyTermState(state);
398   }
399 
400   /**
401    * {@inheritDoc}
402    */
403   @Override
404   public double[] getMass() {
405     return potential.getMass();
406   }
407 
408   /**
409    * Returns the mean number of steps between barostat applications.
410    *
411    * @return Mean steps between barostat applications.
412    */
413   public int getMeanBarostatInterval() {
414     return meanBarostatInterval;
415   }
416 
417   /**
418    * Setter for the field <code>meanBarostatInterval</code>.
419    *
420    * @param meanBarostatInterval The mean number of steps between barostat applications.
421    */
422   public void setMeanBarostatInterval(int meanBarostatInterval) {
423     this.meanBarostatInterval = meanBarostatInterval;
424   }
425 
426   /**
427    * {@inheritDoc}
428    */
429   @Override
430   public int getNumberOfVariables() {
431     return potential.getNumberOfVariables();
432   }
433 
434   /**
435    * Gets the pressure of this Barostat in atm.
436    *
437    * @return Pressure in atm.
438    */
439   public double getPressure() {
440     return pressure;
441   }
442 
443   /**
444    * Setter for the field <code>pressure</code>.
445    *
446    * @param pressure a double.
447    */
448   public void setPressure(double pressure) {
449     this.pressure = pressure;
450   }
451 
452   /**
453    * {@inheritDoc}
454    */
455   @Override
456   public double[] getPreviousAcceleration(double[] previousAcceleration) {
457     return potential.getPreviousAcceleration(previousAcceleration);
458   }
459 
460   /**
461    * {@inheritDoc}
462    */
463   @Override
464   public double[] getScaling() {
465     return potential.getScaling();
466   }
467 
468   /**
469    * {@inheritDoc}
470    */
471   @Override
472   public void setScaling(double[] scaling) {
473     potential.setScaling(scaling);
474   }
475 
476   /**
477    * {@inheritDoc}
478    */
479   @Override
480   public double getTotalEnergy() {
481     return potential.getTotalEnergy();
482   }
483 
484   @Override
485   public List<Potential> getUnderlyingPotentials() {
486     List<Potential> underlying = new ArrayList<>();
487     underlying.add(potential);
488     underlying.addAll(potential.getUnderlyingPotentials());
489     return underlying;
490   }
491 
492   /**
493    * {@inheritDoc}
494    */
495   @Override
496   public VARIABLE_TYPE[] getVariableTypes() {
497     return potential.getVariableTypes();
498   }
499 
500   /**
501    * {@inheritDoc}
502    */
503   @Override
504   public double[] getVelocity(double[] velocity) {
505     return potential.getVelocity(velocity);
506   }
507 
508   public boolean isActive() {
509     return active;
510   }
511 
512   /**
513    * Setter for the field <code>active</code>.
514    *
515    * @param active a boolean.
516    */
517   public void setActive(boolean active) {
518     this.active = active;
519   }
520 
521   /**
522    * {@inheritDoc}
523    */
524   @Override
525   public void setAcceleration(double[] acceleration) {
526     potential.setAcceleration(acceleration);
527   }
528 
529   /**
530    * setDensity.
531    *
532    * @param density a double.
533    */
534   public void setDensity(double density) {
535     molecularAssembly.computeFractionalCoordinates();
536     crystal.setDensity(density, mass);
537     potential.setCrystal(crystal);
538     molecularAssembly.moveToFractionalCoordinates();
539   }
540 
541   /**
542    * Setter for the field <code>maxAngleMove</code>.
543    *
544    * @param maxAngleMove a double.
545    */
546   public void setMaxAngleMove(double maxAngleMove) {
547     this.maxAngleMove = maxAngleMove;
548   }
549 
550   /**
551    * Setter for the field <code>maxVolumeMove</code>.
552    *
553    * @param maxVolumeMove a double.
554    */
555   public void setMaxVolumeMove(double maxVolumeMove) {
556     this.maxVolumeMove = maxVolumeMove;
557   }
558 
559   /**
560    * Setter for the field <code>maxDensity</code>.
561    *
562    * @param maxDensity a double.
563    */
564   public void setMaxDensity(double maxDensity) {
565     this.maxDensity = maxDensity;
566   }
567 
568   /**
569    * Setter for the field <code>minDensity</code>.
570    *
571    * @param minDensity a double.
572    */
573   public void setMinDensity(double minDensity) {
574     this.minDensity = minDensity;
575   }
576 
577   /**
578    * Set the Barostat print frequency.
579    *
580    * @param frequency The number of Barostat moves between print statements.
581    */
582   public void setBarostatPrintFrequency(int frequency) {
583     this.printFrequency = frequency;
584   }
585 
586   /**
587    * {@inheritDoc}
588    */
589   @Override
590   public void setPreviousAcceleration(double[] previousAcceleration) {
591     potential.setPreviousAcceleration(previousAcceleration);
592   }
593 
594   /**
595    * Set the Metropolis Monte Carlo temperature.
596    *
597    * @param temperature Temperature (Kelvin).
598    */
599   public void setTemperature(double temperature) {
600     this.kT = temperature * R;
601   }
602 
603   /**
604    * {@inheritDoc}
605    */
606   @Override
607   public void setVelocity(double[] velocity) {
608     potential.setVelocity(velocity);
609   }
610 
611   private double mcStep(double currentE, double currentV) {
612 
613     // Enforce minimum & maximum density constraints.
614     double den = density();
615     if(Double.isNaN(den) || Double.isNaN(a) || Double.isNaN(b) || Double.isNaN(c) || Double.isNaN(alpha) || Double.isNaN(beta) || Double.isNaN(gamma)){
616       logger.warning(format(" Found value of NaN: density: %7.4f a: %7.4f b: %7.4f c: %7.4f alpha: %7.4f beta: %7.4f gamma: %7.4f",
617               den, a, b, c, alpha, beta, gamma));
618       File errorFile = new File(FilenameUtils.removeExtension(molecularAssembly.getFile().getName()) + "_err.xyz");
619       XYZFilter.version(errorFile);
620       XYZFilter writeFilter = new XYZFilter(errorFile, molecularAssembly, molecularAssembly.getForceField(), molecularAssembly.getProperties());
621       writeFilter.writeFile(errorFile, true, null);
622     }
623     if (den < minDensity || den > maxDensity) {
624       if (logger.isLoggable(Level.FINE)) {
625         logger.fine(
626             format(" MC Density %10.6f is outside the range %10.6f - %10.6f.", den, minDensity,
627                 maxDensity));
628       }
629       // Reject moves outside the specified density range.
630       crystal.changeUnitCellParameters(a, b, c, alpha, beta, gamma);
631       return currentE;
632     }
633 
634     // A carbon atom cannot fit into a unit cell without interfacial radii greater than ~1.2
635     // Angstroms.
636     double minInterfacialRadius = 1.2;
637     double currentMinInterfacialRadius = min(min(unitCell.interfacialRadiusA, unitCell.interfacialRadiusB),
638         unitCell.interfacialRadiusC);
639     // Enforce minimum interfacial radii of 1.2 Angstroms.
640     if (currentMinInterfacialRadius < minInterfacialRadius) {
641       if (logger.isLoggable(Level.FINE)) {
642         logger.fine(format(" MC An interfacial radius (%10.6f,%10.6f,%10.6f) is below the minimum %10.6f",
643             unitCell.interfacialRadiusA, unitCell.interfacialRadiusB,
644             unitCell.interfacialRadiusC, minInterfacialRadius));
645       }
646       // Reject due to small interfacial radius.
647       crystal.changeUnitCellParameters(a, b, c, alpha, beta, gamma);
648       return currentE;
649     }
650 
651     // Apply the boundary condition for the proposed move. If the move is
652     // rejected, then the previous boundary conditions must be restored.
653     potential.setCrystal(crystal);
654 
655     // Update atomic coordinates to maintain molecular fractional centers of mass.
656     molecularAssembly.moveToFractionalCoordinates();
657 
658     // Save the new volume
659     double newV = unitCell.volume / nSymm;
660 
661     potential.getCoordinates(x);
662 
663     // Compute the new energy
664     double newE = potential.energy(x);
665 
666     // Compute the change in potential energy
667     double dPE = newE - currentE;
668 
669     // Compute the pressure-volume work for the asymmetric unit.
670     double dEV = pressure * (newV - currentV) / PRESCON;
671 
672     // Compute the volume entropy
673     double dES = -nMolecules * kT * log(newV / currentV);
674 
675     // Add up the contributions
676     double dE = dPE + dEV + dES;
677 
678     // Energy decreased so the move is accepted.
679     if (dE < 0.0) {
680       if (logger.isLoggable(Level.FINE)) {
681         logger.fine(format(" MC Accept: %12.6f (dPE) + %12.6f (dEV) + %12.6f (dES) = %12.6f with (V=%12.6f, E=%12.6f)",
682             dPE, dEV, dES, dE, newV, newE));
683       }
684       moveAccepted = true;
685       switch (moveType) {
686         case SIDE -> sideMoves.addValue(1.0);
687         case ANGLE -> angleMoves.addValue(1.0);
688         case UNIT -> unitCellMoves.addValue(1.0);
689       }
690       return newE;
691     }
692 
693     // Apply the Metropolis criteria.
694     double acceptanceProbability = exp(-dE / kT);
695 
696     // Draw a pseudorandom double greater than or equal to 0.0 and less than 1.0
697     // from an (approximately) uniform distribution from that range.
698     double metropolis = random();
699 
700     // Energy increase without the Metropolis criteria satisfied.
701     if (metropolis > acceptanceProbability) {
702       rejectMove();
703       if (logger.isLoggable(Level.FINE)) {
704         logger.fine(format(" MC Reject: %12.6f (dPE) + %12.6f (dEV) + %12.6f (dES) = %12.6f with (V=%12.6f, E=%12.6f)",
705             dPE, dEV, dES, dE, currentV, currentE));
706       }
707       switch (moveType) {
708         case SIDE -> sideMoves.addValue(0.0);
709         case ANGLE -> angleMoves.addValue(0.0);
710         case UNIT -> unitCellMoves.addValue(0.0);
711       }
712       return currentE;
713     }
714 
715     // Energy increase with Metropolis criteria satisfied.
716     if (logger.isLoggable(Level.FINE)) {
717       logger.fine(format(" MC Accept: %12.6f (dPE) + %12.6f (dEV) + %12.6f (dES) = %12.6f with (V=%12.6f, E=%12.6f)",
718           dPE, dEV, dES, dE, newV, newE));
719     }
720 
721     moveAccepted = true;
722     switch (moveType) {
723       case SIDE -> sideMoves.addValue(1.0);
724       case ANGLE -> angleMoves.addValue(1.0);
725       case UNIT -> unitCellMoves.addValue(1.0);
726     }
727 
728     return newE;
729   }
730 
731   /**
732    * Reset the to state prior to trial move.
733    */
734   private void rejectMove() {
735     // Reset the unit cell parameters
736     crystal.changeUnitCellParameters(a, b, c, alpha, beta, gamma);
737 
738     // Reset the potential PBC.
739     potential.setCrystal(crystal);
740 
741     // Reset the atomic coordinates to maintain molecular fractional centers of mass.
742     molecularAssembly.moveToFractionalCoordinates();
743   }
744 
745   /**
746    * Propose to change the A-axis length.
747    *
748    * @param currentE Current energy.
749    * @return Energy after the MC trial.
750    */
751   private double mcA(double currentE) {
752     moveType = MoveType.SIDE;
753     double currentAUV = unitCell.volume / nSymm;
754     double dAUVolume = maxVolumeMove * (2.0 * random() - 1.0);
755     double dVdA = unitCell.dVdA / nSymm;
756     double dA = dAUVolume / dVdA;
757     boolean succeed = crystal.changeUnitCellParameters(a + dA, b, c, alpha, beta, gamma);
758     if (succeed) {
759       if (logger.isLoggable(Level.FINE)) {
760         logger.fine(format(" Proposing MC change of the a-axis to (%6.3f) with volume change %6.3f (A^3)", a, dAUVolume));
761       }
762       return mcStep(currentE, currentAUV);
763     }
764     return currentE;
765   }
766 
767   /**
768    * Propose to change the B-axis length.
769    *
770    * @param currentE Current energy.
771    * @return Energy after the MC trial.
772    */
773   private double mcB(double currentE) {
774     moveType = MoveType.SIDE;
775     double currentAUV = unitCell.volume / nSymm;
776     double dAUVolume = maxVolumeMove * (2.0 * random() - 1.0);
777     double dVdB = unitCell.dVdB / nSymm;
778     double dB = dAUVolume / dVdB;
779     boolean succeed = crystal.changeUnitCellParameters(a, b + dB, c, alpha, beta, gamma);
780     if (succeed) {
781       if (logger.isLoggable(Level.FINE)) {
782         logger.fine(format(" Proposing MC change of the b-axis to (%6.3f) with volume change %6.3f (A^3)", b, dAUVolume));
783       }
784       return mcStep(currentE, currentAUV);
785     }
786     return currentE;
787   }
788 
789   /**
790    * Propose to change the C-axis length.
791    *
792    * @param currentE Current energy.
793    * @return Energy after the MC trial.
794    */
795   private double mcC(double currentE) {
796     moveType = MoveType.SIDE;
797     double currentAUV = unitCell.volume / nSymm;
798     double dAUVolume = maxVolumeMove * (2.0 * random() - 1.0);
799     double dVdC = unitCell.dVdC / nSymm;
800     double dC = dAUVolume / dVdC;
801     boolean succeed = crystal.changeUnitCellParameters(a, b, c + dC, alpha, beta, gamma);
802     if (succeed) {
803       if (logger.isLoggable(Level.FINE)) {
804         logger.fine(format(" Proposing MC change to the c-axis to (%6.3f) with volume change %6.3f (A^3)", c, dAUVolume));
805       }
806       return mcStep(currentE, currentAUV);
807     }
808     return currentE;
809   }
810 
811   /**
812    * Propose the same change to the A-axis and B-axis lengths.
813    *
814    * @param currentE Current energy.
815    * @return Energy after the MC trial.
816    */
817   private double mcAB(double currentE) {
818     moveType = MoveType.SIDE;
819     double currentAUV = unitCell.volume / nSymm;
820     double dAUVolume = maxVolumeMove * (2.0 * random() - 1.0);
821     double dVdAB = (unitCell.dVdA + unitCell.dVdB) / nSymm;
822     double dAB = dAUVolume / dVdAB;
823     boolean succeed = crystal.changeUnitCellParametersAndVolume(a + dAB, b + dAB, c, alpha, beta,
824         gamma, currentAUV + dAUVolume);
825     if (succeed) {
826       if (logger.isLoggable(Level.FINE)) {
827         logger.fine(format(" Proposing MC change to the a,b-axes to (%6.3f) with volume change %6.3f (A^3)", a, dAUVolume));
828       }
829       return mcStep(currentE, currentAUV);
830     }
831     return currentE;
832   }
833 
834   private double mcABC(double currentE) {
835     moveType = MoveType.SIDE;
836     double currentAUV = unitCell.volume / nSymm;
837     double dAUVolume = maxVolumeMove * (2.0 * random() - 1.0);
838     double dVdABC = (unitCell.dVdA + unitCell.dVdB + unitCell.dVdC) / nSymm;
839     double dABC = dAUVolume / dVdABC;
840     boolean succeed = crystal.changeUnitCellParametersAndVolume(a + dABC, b + dABC, c + dABC, alpha,
841         beta, gamma, currentAUV + dAUVolume);
842     if (succeed) {
843       if (logger.isLoggable(Level.FINE)) {
844         logger.fine(format(" Proposing MC change to the a,b,c-axes to (%6.3f) with volume change %6.3f (A^3)", a, dAUVolume));
845       }
846       return mcStep(currentE, currentAUV);
847     }
848     return currentE;
849   }
850 
851   /**
852    * Propose to change the Alpha angle.
853    *
854    * @param currentE Current energy.
855    * @return Energy after the MC trial.
856    */
857   private double mcAlpha(double currentE) {
858     moveType = MoveType.ANGLE;
859     double move = maxAngleMove * (2.0 * random() - 1.0);
860     double currentAUV = unitCell.volume / nSymm;
861     double newAlpha = mirrorDegrees(alpha + move);
862     boolean succeed = crystal.changeUnitCellParametersAndVolume(a, b, c, newAlpha, beta, gamma, currentAUV);
863     if (succeed) {
864       if (logger.isLoggable(Level.FINE)) {
865         logger.fine(format(" Proposing MC change of alpha to %6.3f.", newAlpha));
866       }
867       return mcStep(currentE, currentAUV);
868     }
869     return currentE;
870   }
871 
872   /**
873    * Propose to change the Beta angle.
874    *
875    * @param currentE Current energy.
876    * @return Energy after the MC trial.
877    */
878   private double mcBeta(double currentE) {
879     moveType = MoveType.ANGLE;
880     double move = maxAngleMove * (2.0 * random() - 1.0);
881     double currentAUV = unitCell.volume / nSymm;
882     double newBeta = mirrorDegrees(beta + move);
883     boolean succeed = crystal.changeUnitCellParametersAndVolume(a, b, c, alpha, newBeta, gamma, currentAUV);
884     if (succeed) {
885       if (logger.isLoggable(Level.FINE)) {
886         logger.fine(format(" Proposing MC change of beta to %6.3f.", newBeta));
887       }
888       return mcStep(currentE, currentAUV);
889     }
890     return currentE;
891   }
892 
893   /**
894    * Propose to change the Gamma angle.
895    *
896    * @param currentE Current energy.
897    * @return Energy after the MC trial.
898    */
899   private double mcGamma(double currentE) {
900     moveType = MoveType.ANGLE;
901     double move = maxAngleMove * (2.0 * random() - 1.0);
902     double currentAUV = unitCell.volume / nSymm;
903     double newGamma = mirrorDegrees(gamma + move);
904     boolean succeed = crystal.changeUnitCellParametersAndVolume(a, b, c, alpha, beta, newGamma, currentAUV);
905     if (succeed) {
906       if (logger.isLoggable(Level.FINE)) {
907         logger.fine(format(" Proposing MC change of gamma to %6.3f.", newGamma));
908       }
909       return mcStep(currentE, currentAUV);
910     }
911     return currentE;
912   }
913 
914   /**
915    * Propose the same change to all 3 angles.
916    *
917    * @param currentE Current energy.
918    * @return Energy after the MC trial.
919    */
920   private double mcABG(double currentE) {
921     moveType = MoveType.ANGLE;
922     double move = maxAngleMove * (2.0 * random() - 1.0);
923     double currentAUV = unitCell.volume / nSymm;
924     double newAlpha = mirrorDegrees(alpha + move);
925     double newBeta = mirrorDegrees(beta + move);
926     double newGamma = mirrorDegrees(gamma + move);
927     boolean succeed = crystal.changeUnitCellParametersAndVolume(a, b, c, newAlpha, newBeta, newGamma, currentAUV);
928     if (succeed) {
929       if (logger.isLoggable(Level.FINE)) {
930         logger.fine(format(" Proposing MC change to all angles to %6.3f.", newAlpha));
931       }
932       return mcStep(currentE, currentAUV);
933     }
934     return currentE;
935   }
936 
937   /**
938    * Attempt an MC move on a lattice parameter.
939    *
940    * @param currentE The current potential energy.
941    * @return The potential energy after attempting the MC move.
942    */
943   private double applyBarostat(double currentE) {
944     // Determine the current molecular centers of mass in fractional coordinates.
945     molecularAssembly.computeFractionalCoordinates();
946 
947     // Collect the current unit cell parameters.
948     crystal = potential.getCrystal();
949     unitCell = crystal.getUnitCell();
950     a = unitCell.a;
951     b = unitCell.b;
952     c = unitCell.c;
953     alpha = unitCell.alpha;
954     beta = unitCell.beta;
955     gamma = unitCell.gamma;
956 
957     if (isotropic) {
958       currentE = mcABC(currentE);
959     } else {
960       switch (spaceGroup.latticeSystem) {
961         case MONOCLINIC_LATTICE -> {
962           // alpha = gamma = 90
963           int move = (int) floor(random() * 4.0);
964           switch (move) {
965             case 0 -> currentE = mcA(currentE);
966             case 1 -> currentE = mcB(currentE);
967             case 2 -> currentE = mcC(currentE);
968             case 3 -> currentE = mcBeta(currentE);
969             default -> logger.severe(" Barostat programming error.");
970           }
971         }
972         case ORTHORHOMBIC_LATTICE -> {
973           // alpha = beta = gamma = 90
974           int move = (int) floor(random() * 3.0);
975           switch (move) {
976             case 0 -> currentE = mcA(currentE);
977             case 1 -> currentE = mcB(currentE);
978             case 2 -> currentE = mcC(currentE);
979             default -> logger.severe(" Barostat programming error.");
980           }
981         }
982         case TETRAGONAL_LATTICE -> {
983           // a = b, alpha = beta = gamma = 90
984           int move = (int) floor(random() * 2.0);
985           switch (move) {
986             case 0 -> currentE = mcAB(currentE);
987             case 1 -> currentE = mcC(currentE);
988             default -> logger.severe(" Barostat programming error.");
989           }
990         }
991         case RHOMBOHEDRAL_LATTICE -> {
992           // a = b = c, alpha = beta = gamma
993           int move = (int) floor(random() * 2.0);
994           switch (move) {
995             case 0 -> currentE = mcABC(currentE);
996             case 1 -> currentE = mcABG(currentE);
997             default -> logger.severe(" Barostat programming error.");
998           }
999         }
1000         case HEXAGONAL_LATTICE -> {
1001           // a = b, alpha = beta = 90, gamma = 120
1002           int move = (int) floor(random() * 2.0);
1003           switch (move) {
1004             case 0 -> currentE = mcAB(currentE);
1005             case 1 -> currentE = mcC(currentE);
1006             default -> logger.severe(" Barostat programming error.");
1007           }
1008         }
1009         case CUBIC_LATTICE ->
1010             // a = b = c, alpha = beta = gamma = 90
1011             currentE = mcABC(currentE);
1012         case TRICLINIC_LATTICE -> {
1013           if (check(a, b) && check(b, c) && check(alpha, 90.0) && check(beta, 90.0) && check(gamma, 90.0)) {
1014             currentE = mcABC(currentE);
1015           } else {
1016             int move = (int) floor(random() * 6.0);
1017             switch (move) {
1018               case 0 -> currentE = mcA(currentE);
1019               case 1 -> currentE = mcB(currentE);
1020               case 2 -> currentE = mcC(currentE);
1021               case 3 -> currentE = mcAlpha(currentE);
1022               case 4 -> currentE = mcBeta(currentE);
1023               case 5 -> currentE = mcGamma(currentE);
1024               default -> logger.severe(" Barostat programming error.");
1025             }
1026           }
1027         }
1028       }
1029     }
1030 
1031     currentDensity = density();
1032     if (moveAccepted) {
1033       if (logger.isLoggable(Level.FINE)) {
1034         StringBuilder sb = new StringBuilder(" MC Barostat Acceptance:");
1035         if (sideMoves.getCount() > 0) {
1036           sb.append(format(" Side %5.1f%%", sideMoves.getMean() * 100.0));
1037         }
1038         if (angleMoves.getCount() > 0) {
1039           sb.append(format(" Angle %5.1f%%", angleMoves.getMean() * 100.0));
1040         }
1041         if (unitCellMoves.getCount() > 0) {
1042           sb.append(format(" UC %5.1f%%", unitCellMoves.getMean() * 100.0));
1043         }
1044         sb.append(format("\n Density: %5.3f  UC: %s", currentDensity, unitCell.toShortString()));
1045         logger.fine(sb.toString());
1046       }
1047     } else {
1048       // Check that the unit cell parameters have not changed.
1049       if (unitCell.a != a || unitCell.b != b || unitCell.c != c || unitCell.alpha != alpha
1050           || unitCell.beta != beta || unitCell.gamma != gamma) {
1051         logger.severe(" Reversion of unit cell parameters did not succeed after failed Barostat MC move.");
1052       }
1053     }
1054 
1055     return currentE;
1056   }
1057 
1058   /**
1059    * Collect statistics on lattice parameters and density.
1060    */
1061   private void collectStats() {
1062     // Collect statistics.
1063     barostatCount++;
1064 
1065     // Sanity check for values.
1066     if(Double.isNaN(currentDensity)||Double.isNaN(a)||Double.isNaN(b)||Double.isNaN(c)||Double.isNaN(alpha)||Double.isNaN(beta)||Double.isNaN(gamma)){
1067       logger.warning(format(" Statistic Value was NaN: Density: %5.3f A: %5.3f B: %5.3f C: %5.3f Alpha: %5.3f Beta: %5.3f Gamma: %5.3f",
1068               currentDensity, a, b, c, alpha, beta, gamma));
1069     }
1070     densityStats.addValue(currentDensity);
1071     aStats.addValue(a);
1072     bStats.addValue(b);
1073     cStats.addValue(c);
1074     alphaStats.addValue(alpha);
1075     betaStats.addValue(beta);
1076     gammaStats.addValue(gamma);
1077     if (barostatCount % printFrequency == 0) {
1078       logger.info(format(" Barostat statistics for the last %d moves:", printFrequency));
1079       // Density
1080       logger.info(format("  Density: %5.3f±%5.3f with range %5.3f .. %5.3f (g/cc)", densityStats.getMean(),
1081           densityStats.getStandardDeviation(), densityStats.getMin(), densityStats.getMax()));
1082       densityStats.reset();
1083       // Lattice Parameters
1084       logger.info(format("  Lattice a-axis: %5.2f±%3.2f b-axis: %5.2f±%3.2f c-axis: %5.2f±%3.2f",
1085           aStats.getMean(), aStats.getStandardDeviation(), bStats.getMean(), bStats.getStandardDeviation(), cStats.getMean(),
1086           cStats.getStandardDeviation()));
1087       logger.info(format("          alpha:  %5.2f±%3.2f beta:   %5.2f±%3.2f gamma:  %5.2f±%3.2f",
1088           alphaStats.getMean(), alphaStats.getStandardDeviation(), betaStats.getMean(),
1089           betaStats.getStandardDeviation(), gammaStats.getMean(), gammaStats.getStandardDeviation()));
1090       aStats.reset();
1091       bStats.reset();
1092       cStats.reset();
1093       alphaStats.reset();
1094       betaStats.reset();
1095       gammaStats.reset();
1096       // MC Move Statistics
1097       if (sideMoves.getCount() > 0) {
1098         logger.info(format("  Axis length moves: %4d (%5.2f%%)", sideMoves.getCount(), sideMoves.getMean() * 100.0));
1099         sideMoves.reset();
1100       }
1101       if (angleMoves.getCount() > 0) {
1102         logger.info(format("  Angle moves:       %4d (%5.2f%%)", angleMoves.getCount(), angleMoves.getMean() * 100.0));
1103         angleMoves.reset();
1104       }
1105       if (unitCellMoves.getCount() > 0) {
1106         logger.info(format("  Unit cell moves:   %4d (%5.2f%%)", unitCellMoves.getCount(), unitCellMoves.getMean() * 100.0));
1107         unitCellMoves.reset();
1108       }
1109     }
1110   }
1111 
1112   /**
1113    * The type of Barostat move.
1114    */
1115   private enum MoveType {
1116     SIDE, ANGLE, UNIT
1117   }
1118 }