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.potential.openmm;
39  
40  import com.sun.jna.ptr.PointerByReference;
41  import ffx.openmm.State;
42  import ffx.potential.bonded.Atom;
43  import ffx.potential.utils.EnergyException;
44  
45  import javax.annotation.Nullable;
46  import java.util.Arrays;
47  
48  import static edu.uiowa.jopenmm.OpenMMAmoebaLibrary.OpenMM_AngstromsPerNm;
49  import static edu.uiowa.jopenmm.OpenMMAmoebaLibrary.OpenMM_KcalPerKJ;
50  import static edu.uiowa.jopenmm.OpenMMAmoebaLibrary.OpenMM_NmPerAngstrom;
51  import static edu.uiowa.jopenmm.OpenMMLibrary.OpenMM_State_DataType.OpenMM_State_Energy;
52  import static edu.uiowa.jopenmm.OpenMMLibrary.OpenMM_State_DataType.OpenMM_State_Forces;
53  import static edu.uiowa.jopenmm.OpenMMLibrary.OpenMM_State_DataType.OpenMM_State_Positions;
54  import static edu.uiowa.jopenmm.OpenMMLibrary.OpenMM_State_DataType.OpenMM_State_Velocities;
55  import static java.lang.Double.isInfinite;
56  import static java.lang.Double.isNaN;
57  import static java.lang.String.format;
58  
59  /**
60   * Retrieve state information from an OpenMM Simulation.
61   */
62  public class OpenMMState extends State {
63  
64    /**
65     * Potential energy (kcal/mol).
66     */
67    public final double potentialEnergy;
68    /**
69     * Kinetic energy (kcal/mol).
70     */
71    public final double kineticEnergy;
72    /**
73     * Total energy (kcal/mol).
74     */
75    public final double totalEnergy;
76    /**
77     * Mask of information to retrieve.
78     */
79    private final int dataTypes;
80  
81    /**
82     * Construct an OpenMM State with the requested information.
83     *
84     * @param pointer Pointer to an OpenMM state.
85     */
86    protected OpenMMState(PointerByReference pointer) {
87      super(pointer);
88  
89      // Set the data types mask using the super class method.
90      this.dataTypes = super.getDataTypes();
91      if (stateContains(OpenMM_State_Energy)) {
92        // Set the energy fields using the super class method and convert units.
93        potentialEnergy = super.getPotentialEnergy() * OpenMM_KcalPerKJ;
94        kineticEnergy = super.getKineticEnergy() * OpenMM_KcalPerKJ;
95        totalEnergy = potentialEnergy + kineticEnergy;
96      } else {
97        potentialEnergy = 0.0;
98        kineticEnergy = 0.0;
99        totalEnergy = 0.0;
100     }
101   }
102 
103   /**
104    * The acceleration array will contain the acceleration information for all atoms. This
105    * method will convert the OpenMM forces to accelerations using the atom masses.
106    *
107    * @param a     Acceleration components for all atoms.
108    * @param atoms The array of atoms, which is needed to convert from force to acceleration.
109    * @return The acceleration for each atom in units of Angstroms per picosecond squared.
110    */
111   public double[] getAccelerations(@Nullable double[] a, Atom[] atoms) {
112     // Check if the state contains forces.
113     if (!stateContains(OpenMM_State_Forces)) {
114       return a;
115     }
116     double[] forces = getForces();
117     int n = forces.length;
118 
119     // Validate the atoms array exists and is not empty.
120     if (atoms == null || atoms.length == 0) {
121       throw new IllegalArgumentException("Atoms array must not be null or empty.");
122     }
123     // Validate the number of degrees of freedom.
124     if (atoms.length * 3 != n) {
125       String message = format(" The number of atoms (%d) does not match the number of degrees of freedom (%d).", atoms.length, n);
126       throw new IllegalArgumentException(message);
127     }
128     if (a == null || a.length != n) {
129       a = new double[n];
130     }
131 
132     int index = 0;
133     for (Atom atom : atoms) {
134       double mass = atom.getMass();
135       double xx = forces[index] * OpenMM_AngstromsPerNm / mass;
136       double yy = forces[index + 1] * OpenMM_AngstromsPerNm / mass;
137       double zz = forces[index + 2] * OpenMM_AngstromsPerNm / mass;
138       a[index] = xx;
139       a[index + 1] = yy;
140       a[index + 2] = zz;
141       index += 3;
142     }
143     return a;
144   }
145 
146   /**
147    * The acceleration array will contain the acceleration information for all atoms. This
148    * method will convert the OpenMM forces to accelerations using the atom masses.
149    *
150    * @param a     Acceleration components for all atoms.
151    * @param atoms The array of atoms, which is needed to convert from force to acceleration.
152    * @return The acceleration for each atom in units of Angstroms per picosecond squared.
153    */
154   public double[] getActiveAccelerations(@Nullable double[] a, Atom[] atoms) {
155     if (!stateContains(OpenMM_State_Forces)) {
156       return a;
157     }
158     return filterToActive(getAccelerations(null, atoms), a, atoms);
159   }
160 
161   /**
162    * The force array contains the OpenMM force information for all atoms.
163    *
164    * @param g Gradient array to use.
165    * @return The gradient for all atoms in units of kcal/mol/Angstrom (in a new array if g is null or the wrong size).
166    */
167   public double[] getGradient(@Nullable double[] g) {
168     // Check if the state contains forces.
169     if (!stateContains(OpenMM_State_Forces)) {
170       return g;
171     }
172     double[] forces = getForces();
173     int n = forces.length;
174 
175     // Validate the gradient array exists and is the correct size.
176     if (g == null || g.length != n) {
177       g = new double[n];
178     }
179 
180     for (int i = 0; i < n; i++) {
181       double xx = -forces[i] * OpenMM_NmPerAngstrom * OpenMM_KcalPerKJ;
182       if (isNaN(xx) || isInfinite(xx)) {
183         throw new EnergyException(
184             format(" The gradient of degree of freedom %d is %8.3f.", i, xx));
185       }
186       g[i] = xx;
187     }
188     return g;
189   }
190 
191   /**
192    * The force array contains the OpenMM force information for active atoms.
193    *
194    * @param g Gradient array to use.
195    * @return The gradient for all atoms in units of kcal/mol/Angstrom (in a new array if g is null or the wrong size).
196    */
197   public double[] getActiveGradient(@Nullable double[] g, Atom[] atoms) {
198     if (!stateContains(OpenMM_State_Forces)) {
199       return g;
200     }
201     return filterToActive(getGradient(null), g, atoms);
202   }
203 
204   /**
205    * Read the periodic lattice vectors from a state.
206    *
207    * <p>The crystal instance will be updated, and passed to the ForceFieldEnergy instance.
208    */
209   public double[][] getPeriodicBoxVectors() {
210     if (!stateContains(OpenMM_State_Positions)) {
211       return null;
212     }
213 
214     double[][] latticeVectors = super.getPeriodicBoxVectors();
215     latticeVectors[0][0] *= OpenMM_AngstromsPerNm;
216     latticeVectors[0][1] *= OpenMM_AngstromsPerNm;
217     latticeVectors[0][2] *= OpenMM_AngstromsPerNm;
218     latticeVectors[1][0] *= OpenMM_AngstromsPerNm;
219     latticeVectors[1][1] *= OpenMM_AngstromsPerNm;
220     latticeVectors[1][2] *= OpenMM_AngstromsPerNm;
221     latticeVectors[2][0] *= OpenMM_AngstromsPerNm;
222     latticeVectors[2][1] *= OpenMM_AngstromsPerNm;
223     latticeVectors[2][2] *= OpenMM_AngstromsPerNm;
224     return latticeVectors;
225   }
226 
227   /**
228    * The position array contains the OpenMM atomic position information for all atoms. The
229    * returned array x is in units of Angstroms.
230    *
231    * @param x Atomic coordinates array to use.
232    * @return The atomic coordinates (in a new array if x is null or the wrong size).
233    */
234   public double[] getPositions(@Nullable double[] x) {
235     // Check if the state contains positions.
236     if (!stateContains(OpenMM_State_Positions)) {
237       return x;
238     }
239 
240     double[] pos = getPositions();
241     int n = pos.length;
242 
243     // Allocate x if null or the wrong size.
244     if (x == null || x.length != n) {
245       x = new double[n];
246     }
247 
248     for (int i = 0; i < n; i++) {
249       x[i] = pos[i] * OpenMM_AngstromsPerNm;
250     }
251 
252     return x;
253   }
254 
255   /**
256    * The position array contains the OpenMM atomic position information for active atoms.
257    * The returned array x is in units of Angstroms.
258    *
259    * @param x Atomic coordinates array to use.
260    * @return The atomic coordinates (in a new array if x is null or the wrong size).
261    */
262   public double[] getActivePositions(@Nullable double[] x, Atom[] atoms) {
263     if (!stateContains(OpenMM_State_Positions)) {
264       return x;
265     }
266     return filterToActive(getPositions(null), x, atoms);
267   }
268 
269   /**
270    * The velocity array contains the OpenMM atomic position information for all atoms. The
271    * returned array v is in units of Angstroms per picosecond.
272    *
273    * @param v Atomic velocity array to use.
274    * @return The atomic velocities (in a new array if v is null or the wrong size).
275    */
276   public double[] getVelocities(@Nullable double[] v) {
277     if (!stateContains(OpenMM_State_Velocities)) {
278       return v;
279     }
280 
281     double[] vel = getVelocities();
282     int n = vel.length;
283 
284     // Validate the velocity array exists and is the correct size.
285     if (v == null || v.length != n) {
286       v = new double[n];
287     }
288 
289     for (int i = 0; i < n; i++) {
290       v[i] = vel[i] * OpenMM_AngstromsPerNm;
291     }
292 
293     return v;
294   }
295 
296   /**
297    * The velocity array contains the OpenMM atomic position information for active atoms.
298    * The returned array v is in units of Angstroms per picosecond.
299    *
300    * @param v Atomic velocity array to use.
301    * @return The atomic velocities (in a new array if v is null or the wrong size).
302    */
303   public double[] getActiveVelocities(@Nullable double[] v, Atom[] atoms) {
304     if (!stateContains(OpenMM_State_Velocities)) {
305       return v;
306     }
307     return filterToActive(getVelocities(null), v, atoms);
308   }
309 
310   /**
311    * Get the periodic box volume.
312    *
313    * @return The periodic box volume.
314    */
315   @Override
316   public double getPeriodicBoxVolume() {
317     return super.getPeriodicBoxVolume()
318         * OpenMM_AngstromsPerNm * OpenMM_AngstromsPerNm * OpenMM_AngstromsPerNm;
319   }
320 
321   /**
322    * Get the potential energy. This field will be zero if the dataTypes mask did not include the energy.
323    *
324    * @return The potential energy.
325    */
326   @Override
327   public double getPotentialEnergy() {
328     return potentialEnergy;
329   }
330 
331   /**
332    * Get the kinetic energy. This field will be zero if the dataTypes mask did not include the energy.
333    *
334    * @return The kinetic energy.
335    */
336   @Override
337   public double getKineticEnergy() {
338     return kineticEnergy;
339   }
340 
341   /**
342    * Get the total energy. This field will be zero if the dataTypes mask did not include the energy.
343    *
344    * @return The total energy.
345    */
346   public double getTotalEnergy() {
347     return totalEnergy;
348   }
349 
350   /**
351    * Get the mask of information contained in the state.
352    *
353    * @return The mask of information contained in the state.
354    */
355   @Override
356   public int getDataTypes() {
357     return dataTypes;
358   }
359 
360   /**
361    * Check to see if the state contains the requested information.
362    *
363    * @param dataType Information to check for.
364    * @return boolean indicating whether the state contains the requested information.
365    */
366   private boolean stateContains(int dataType) {
367     return (dataTypes & dataType) == dataType;
368   }
369 
370   /**
371    * Filter an array to only include elements where the corresponding Atom is active.
372    *
373    * @param source The source array to filter.
374    * @param target The target array to fill with filtered values (or null to create a new one).
375    * @param atoms  The array of Atoms, which should have a corresponding active mask.
376    */
377   private static double[] filterToActive(double[] source, @Nullable double[] target, Atom[] atoms) {
378     if (source == null || atoms == null) {
379       throw new IllegalArgumentException("The arrays must be non-null.");
380     }
381 
382     // Validate that the source array length is three times the number of atoms.
383     if (source.length != atoms.length * 3) {
384       throw new IllegalArgumentException("Source array length must be three times the number of atoms.");
385     }
386 
387     // Count the number of active atoms.
388     int count = (int) Arrays.stream(atoms).filter(Atom::isActive).count();
389 
390     // Ensure target is large enough to hold the filtered values.
391     if (target == null || target.length < count * 3) {
392       target = new double[count * 3];
393     }
394 
395     // Fill the target array with values from the source array for active atoms.
396     int sourceIndedx = 0;
397     int targetIndex = 0;
398     for (Atom atom : atoms) {
399       if (atom.isActive()) {
400         target[targetIndex] = source[sourceIndedx];
401         target[targetIndex + 1] = source[sourceIndedx + 1];
402         target[targetIndex + 2] = source[sourceIndedx + 2];
403         targetIndex += 3;
404       }
405       // Always increment the source index by 3 for each atom.
406       sourceIndedx += 3;
407     }
408     return target;
409   }
410 }