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.numerics.multipole;
39  
40  import static ffx.numerics.multipole.MultipoleTensorTest.Qi;
41  import static ffx.numerics.multipole.MultipoleTensorTest.Qk;
42  import static ffx.numerics.multipole.MultipoleTensorTest.Ui;
43  import static ffx.numerics.multipole.MultipoleTensorTest.UiEwald;
44  import static ffx.numerics.multipole.MultipoleTensorTest.Uk;
45  import static ffx.numerics.multipole.MultipoleTensorTest.UkEwald;
46  import static ffx.numerics.multipole.MultipoleTensorTest.permTorqueI;
47  import static ffx.numerics.multipole.MultipoleTensorTest.permTorqueIEwald;
48  import static ffx.numerics.multipole.MultipoleTensorTest.permTorqueK;
49  import static ffx.numerics.multipole.MultipoleTensorTest.permTorqueKEwald;
50  import static ffx.numerics.multipole.MultipoleTensorTest.permanentEnergy;
51  import static ffx.numerics.multipole.MultipoleTensorTest.permanentEnergyEwald;
52  import static ffx.numerics.multipole.MultipoleTensorTest.polarGradICoulomb;
53  import static ffx.numerics.multipole.MultipoleTensorTest.polarGradIEwald;
54  import static ffx.numerics.multipole.MultipoleTensorTest.polarGradIThole;
55  import static ffx.numerics.multipole.MultipoleTensorTest.polarTorqueICoulomb;
56  import static ffx.numerics.multipole.MultipoleTensorTest.polarTorqueIEwald;
57  import static ffx.numerics.multipole.MultipoleTensorTest.polarTorqueIThole;
58  import static ffx.numerics.multipole.MultipoleTensorTest.polarTorqueKCoulomb;
59  import static ffx.numerics.multipole.MultipoleTensorTest.polarTorqueKEwald;
60  import static ffx.numerics.multipole.MultipoleTensorTest.polarTorqueKThole;
61  import static ffx.numerics.multipole.MultipoleTensorTest.polarizationEnergyCoulomb;
62  import static ffx.numerics.multipole.MultipoleTensorTest.polarizationEnergyEwald;
63  import static ffx.numerics.multipole.MultipoleTensorTest.polarizationEnergyThole;
64  import static ffx.numerics.multipole.MultipoleTensorTest.scaleMutual;
65  import static ffx.numerics.multipole.MultipoleTensorTest.xyz;
66  import static java.lang.String.format;
67  import static org.junit.Assert.assertEquals;
68  
69  import ffx.numerics.multipole.MultipoleTensor.OPERATOR;
70  import java.util.Arrays;
71  import java.util.Collection;
72  import java.util.logging.Logger;
73  
74  import ffx.utilities.FFXTest;
75  import org.junit.Test;
76  import org.junit.runner.RunWith;
77  import org.junit.runners.Parameterized;
78  import org.junit.runners.Parameterized.Parameters;
79  
80  /**
81   * Parameterized Test of the MultipoleTensor class.
82   *
83   * @author Michael J. Schnieders
84   * @since 1.0
85   */
86  @RunWith(Parameterized.class)
87  public class GlobalMultipoleTensorTest extends FFXTest {
88  
89    /** Logger for the MultipoleTensor class. */
90    private static final Logger logger = Logger.getLogger(GlobalMultipoleTensorTest.class.getName());
91  
92    private final double tolerance = 1.0e-13;
93    private final double fdTolerance = 1.0e-6;
94    private final double[] r = new double[3];
95    private final double[] tensor;
96    private final double[] noStorageTensor;
97    private final double[] fastTensor;
98    private final int order;
99    private final int tensorCount;
100   private final String info;
101   private final OPERATOR operator;
102   private final double beta = 0.545;
103   private final double thole = 0.39;
104   private final double AiAk = 1.061104559485911;
105 
106   public GlobalMultipoleTensorTest(
107       String info,
108       int order,
109       OPERATOR operator) {
110     this.info = info;
111     this.order = order;
112     r[0] = xyz[0];
113     r[1] = xyz[1];
114     r[2] = xyz[2];
115     this.tensorCount = MultipoleTensor.tensorCount(order);
116     tensor = new double[tensorCount];
117     noStorageTensor = new double[tensorCount];
118     fastTensor = new double[tensorCount];
119     this.operator = operator;
120   }
121 
122   @Parameters
123   public static Collection<Object[]> data() {
124     return Arrays.asList(
125         new Object[][] {
126             {
127                 "Order 5 Coulomb",
128                 5,      // Order
129                 OPERATOR.COULOMB
130             },
131             {
132                 "Order 5 Screened Coulomb",
133                 5,
134                 OPERATOR.SCREENED_COULOMB
135             },
136             {
137                 "Order 4 Thole Field",
138                 4,
139                 OPERATOR.THOLE_FIELD
140             }
141         });
142   }
143 
144   @Test
145   public void codingGenerationTest() {
146     // Only run code generation for the Coulomb operator.
147     if (operator != OPERATOR.COULOMB) {
148       return;
149     }
150 
151     // For code generation, make sure no mutipole terms are zero.
152     double[] Qi = {0.11, 0.22, 0.33, 0.44, 0.11, 0.22, -0.33, 0.12, 0.13, 0.14};
153     double[] r = {2.11, 2.12, 2.13};
154     double[] tensor = new double[tensorCount];
155     MultipoleTensor multipoleTensor = new CoulombTensorGlobal(order);
156     logger.info(format(" Writing Cartesian Order %d tensor recursion code:", order));
157     String code = multipoleTensor.codeTensorRecursion(r, tensor);
158     logger.info(format("\n%s", code));
159 
160     PolarizableMultipole polarizableMultipole = new PolarizableMultipole(Qi, Ui, Ui);
161     StringBuilder sb = new StringBuilder();
162     logger.info(" Writing potential code due to multipole I:");
163     multipoleTensor.codePotentialMultipoleI(polarizableMultipole, tensor, 0, 0, 0, sb);
164     logger.info("\n" + sb);
165 
166     sb = new StringBuilder();
167     logger.info(" Writing potential code due to multipole K:");
168     multipoleTensor.codePotentialMultipoleK(polarizableMultipole, tensor, 0, 0, 0, sb);
169     logger.info("\n" + sb);
170   }
171 
172   @Test
173   public void permanentMultipoleEnergyAndGradTest() {
174     // Thole damping is not used for permanent AMOEBA electrostatics.
175     if (operator == OPERATOR.THOLE_FIELD) {
176       return;
177     }
178 
179     MultipoleTensor multipoleTensor = new CoulombTensorGlobal(order);
180     if (operator == OPERATOR.SCREENED_COULOMB) {
181       multipoleTensor = new EwaldTensorGlobal(order, beta);
182     }
183 
184     double delta = 1.0e-5;
185     double delta2 = 2.0 * 1.0e-5;
186     double[] Fi = new double[3];
187     double[] Fk = new double[3];
188     double[] Ti = new double[3];
189     double[] Tk = new double[3];
190 
191     PolarizableMultipole mI = new PolarizableMultipole(Qi, Ui, Ui);
192     PolarizableMultipole mK = new PolarizableMultipole(Qk, Uk, Uk);
193     multipoleTensor.generateTensor(r);
194     double e = multipoleTensor.multipoleEnergyAndGradient(mI, mK, Fi, Fk, Ti, Tk);
195 
196     if (operator == OPERATOR.COULOMB) {
197       assertEquals(info + " Cartesian Multipole Energy", permanentEnergy, e, tolerance);
198       assertEquals(info + " Cartesian Multipole Torque I", permTorqueI[1], Ti[1], tolerance);
199       assertEquals(info + " Cartesian Multipole Torque K", permTorqueK[1], Tk[1], tolerance);
200     } else if (operator == OPERATOR.SCREENED_COULOMB) {
201       assertEquals(info + " Cartesian Ewald Permanent Energy", permanentEnergyEwald, e, tolerance);
202       assertEquals(info + " Cartesian Ewald Multipole Torque I", permTorqueIEwald[1], Ti[1],
203           tolerance);
204       assertEquals(info + " Cartesian Ewald Multipole Torque K", permTorqueKEwald[1], Tk[1],
205           tolerance);
206     }
207 
208     // Analytic gradient.
209     double aX = Fk[0];
210     double aY = Fk[1];
211     double aZ = Fk[2];
212 
213     r[0] += delta;
214     multipoleTensor.generateTensor(r);
215     double posX = multipoleTensor.multipoleEnergy(mI, mK);
216     r[0] -= delta2;
217     multipoleTensor.generateTensor(r);
218     double negX = multipoleTensor.multipoleEnergy(mI, mK);
219     r[0] += delta;
220     r[1] += delta;
221     multipoleTensor.generateTensor(r);
222     double posY = multipoleTensor.multipoleEnergy(mI, mK);
223     r[1] -= delta2;
224     multipoleTensor.generateTensor(r);
225     double negY = multipoleTensor.multipoleEnergy(mI, mK);
226     r[1] += delta;
227     r[2] += delta;
228     multipoleTensor.generateTensor(r);
229     double posZ = multipoleTensor.multipoleEnergy(mI, mK);
230     r[2] -= delta2;
231     multipoleTensor.generateTensor(r);
232     double negZ = multipoleTensor.multipoleEnergy(mI, mK);
233     r[2] += delta;
234 
235     double expect = aX;
236     double actual = (posX - negX) / delta2;
237     assertEquals(info + " Force X", expect, actual, fdTolerance);
238     expect = aY;
239     actual = (posY - negY) / delta2;
240     assertEquals(info + " Force Y", expect, actual, fdTolerance);
241     expect = aZ;
242     actual = (posZ - negZ) / delta2;
243     assertEquals(info + " Force Z", expect, actual, fdTolerance);
244   }
245 
246   @Test
247   public void polarizationEnergyAndGradTest() {
248 
249     MultipoleTensor multipoleTensor = new CoulombTensorGlobal(order);
250 
251     if (operator == OPERATOR.THOLE_FIELD) {
252       multipoleTensor = new TholeTensorGlobal(order, thole, AiAk);
253     } else if (operator == OPERATOR.SCREENED_COULOMB) {
254       multipoleTensor = new EwaldTensorGlobal(order, beta);
255     }
256 
257     double delta = 1.0e-5;
258     double delta2 = 2.0 * 1.0e-5;
259     double[] Gi = new double[3];
260     double[] Ti = new double[3];
261     double[] Tk = new double[3];
262 
263     PolarizableMultipole mI = new PolarizableMultipole(Qi, Ui, Ui);
264     PolarizableMultipole mK = new PolarizableMultipole(Qk, Uk, Uk);
265     if (operator == OPERATOR.SCREENED_COULOMB) {
266       mI.setInducedDipole(UiEwald, UiEwald);
267       mK.setInducedDipole(UkEwald, UkEwald);
268     }
269 
270     multipoleTensor.generateTensor(r);
271     double e = multipoleTensor.polarizationEnergyAndGradient(mI, mK, 1.0, 1.0, scaleMutual, Gi, Ti,
272         Tk);
273 
274     // Analytic gradient.
275     double aX = Gi[0];
276     double aY = Gi[1];
277     double aZ = Gi[2];
278 
279     double energy = polarizationEnergyCoulomb;
280     double[] gradI = polarGradICoulomb;
281     double[] torqueI = polarTorqueICoulomb;
282     double[] torqueK = polarTorqueKCoulomb;
283     if (operator == OPERATOR.THOLE_FIELD) {
284       energy = polarizationEnergyThole;
285       gradI = polarGradIThole;
286       torqueI = polarTorqueIThole;
287       torqueK = polarTorqueKThole;
288     } else if (operator == OPERATOR.SCREENED_COULOMB) {
289       energy = polarizationEnergyEwald;
290       gradI = polarGradIEwald;
291       torqueI = polarTorqueIEwald;
292       torqueK = polarTorqueKEwald;
293     }
294     assertEquals(info + " Polarization Energy", energy, e, tolerance);
295     assertEquals(info + " Polarization GradX", gradI[0], aX, tolerance);
296     assertEquals(info + " Polarization GradY", gradI[1], aY, tolerance);
297     assertEquals(info + " Polarization GradZ", gradI[2], aZ, tolerance);
298     assertEquals(info + " Polarization TorqueX I", torqueI[0], Ti[0], tolerance);
299     assertEquals(info + " Polarization TorqueY I", torqueI[1], Ti[1], tolerance);
300     assertEquals(info + " Polarization TorqueZ I", torqueI[2], Ti[2], tolerance);
301     assertEquals(info + " Polarization TorqueX K", torqueK[0], Tk[0], tolerance);
302     assertEquals(info + " Polarization TorqueY K", torqueK[1], Tk[1], tolerance);
303     assertEquals(info + " Polarization TorqueZ K", torqueK[2], Tk[2], tolerance);
304 
305     // Finite-Difference test only works for direct polarization.
306     if (scaleMutual == 0.0) {
307       // Compute the gradient for atom K.
308       r[0] += delta;
309       multipoleTensor.generateTensor(r);
310       double posX = multipoleTensor.polarizationEnergy(mI, mK, 1.0);
311       r[0] -= delta2;
312       multipoleTensor.generateTensor(r);
313       double negX = multipoleTensor.polarizationEnergy(mI, mK, 1.0);
314       r[0] += delta;
315 
316       r[1] += delta;
317       multipoleTensor.generateTensor(r);
318       double posY = multipoleTensor.polarizationEnergy(mI, mK, 1.0);
319       r[1] -= delta2;
320       multipoleTensor.generateTensor(r);
321       double negY = multipoleTensor.polarizationEnergy(mI, mK, 1.0);
322       r[1] += delta;
323       r[2] += delta;
324       multipoleTensor.generateTensor(r);
325       double posZ = multipoleTensor.polarizationEnergy(mI, mK, 1.0);
326       r[2] -= delta2;
327       multipoleTensor.generateTensor(r);
328       double negZ = multipoleTensor.polarizationEnergy(mI, mK, 1.0);
329       r[2] += delta;
330 
331       // Note that the factor of 2 below accounts for the induced dipoles not being updated (i.e. the analytic gradient is for
332       // induced dipoles, but the finite-difference is effectively for permanent dipoles due to lack of re-induction).
333       double expect = -aX;
334       double actual = 2.0 * (posX - negX) / delta2;
335       assertEquals(info + " Grad X", expect, actual, fdTolerance);
336       expect = -aY;
337       actual = 2.0 * (posY - negY) / delta2;
338       assertEquals(info + " Grad Y", expect, actual, fdTolerance);
339       expect = -aZ;
340       actual = 2.0 * (posZ - negZ) / delta2;
341       assertEquals(info + " Grad Z", expect, actual, fdTolerance);
342     }
343   }
344 
345   @Test
346   public void finiteDifferenceTest() {
347 
348     MultipoleTensor multipoleTensor = new CoulombTensorGlobal(order);
349     if (operator == OPERATOR.THOLE_FIELD) {
350       multipoleTensor = new TholeTensorGlobal(order, thole, AiAk);
351     } else if (operator == OPERATOR.SCREENED_COULOMB) {
352       multipoleTensor = new EwaldTensorGlobal(order, beta);
353     }
354 
355     multipoleTensor.recursion(r, tensor);
356 
357     double[] tensorsPx = new double[tensorCount];
358     double[] tensorsNx = new double[tensorCount];
359     double[] tensorsPy = new double[tensorCount];
360     double[] tensorsNy = new double[tensorCount];
361     double[] tensorsPz = new double[tensorCount];
362     double[] tensorsNz = new double[tensorCount];
363     double delta = 1.0e-5;
364     double delta2 = delta * 2;
365     r[0] += delta;
366     multipoleTensor.noStorageRecursion(r, tensorsPx);
367     r[0] -= delta2;
368     multipoleTensor.noStorageRecursion(r, tensorsNx);
369     r[0] += delta;
370 
371     r[1] += delta;
372     multipoleTensor.noStorageRecursion(r, tensorsPy);
373     r[1] -= delta2;
374     multipoleTensor.noStorageRecursion(r, tensorsNy);
375     r[1] += delta;
376 
377     r[2] += delta;
378     multipoleTensor.noStorageRecursion(r, tensorsPz);
379     r[2] -= delta2;
380     multipoleTensor.noStorageRecursion(r, tensorsNz);
381     r[2] += delta;
382 
383     tensorFiniteDifference(
384         multipoleTensor, delta2, tensorsPx, tensorsNx, tensorsPy, tensorsNy, tensorsPz, tensorsNz);
385 
386     multipoleTensor.recursion(r, tensor);
387     tensorFiniteDifference(
388         multipoleTensor, delta2, tensorsPx, tensorsNx, tensorsPy, tensorsNy, tensorsPz, tensorsNz);
389 
390     multipoleTensor.generateTensor();
391     multipoleTensor.getTensor(tensor);
392     tensorFiniteDifference(
393         multipoleTensor, delta2, tensorsPx, tensorsNx, tensorsPy, tensorsNy, tensorsPz, tensorsNz);
394   }
395 
396   /**
397    * Test of recursion and noStorageRecursion methods, of class MultipoleTensor.
398    *
399    * @since 1.0
400    */
401   @Test
402   public void multipoleTensorTest() {
403     MultipoleTensor multipoleTensor = new CoulombTensorGlobal(order);
404     if (operator == OPERATOR.THOLE_FIELD) {
405       multipoleTensor = new TholeTensorGlobal(order, thole, AiAk);
406     } else if (operator == OPERATOR.SCREENED_COULOMB) {
407       multipoleTensor = new EwaldTensorGlobal(order, beta);
408     }
409 
410     // Check Cartesian Tensors in the Global frame.
411     multipoleTensor.noStorageRecursion(r, noStorageTensor);
412     multipoleTensor.recursion(r, tensor);
413     multipoleTensor.generateTensor(r);
414     multipoleTensor.getTensor(fastTensor);
415 
416     for (int i = 0; i < tensorCount; i++) {
417       double expect = noStorageTensor[i];
418       double actual = tensor[i];
419       assertEquals(info + " @ " + i, expect, actual, tolerance);
420       if (order >= 2 || order <= 6) {
421         expect = noStorageTensor[i];
422         actual = fastTensor[i];
423         assertEquals(info + " @ " + i, expect, actual, tolerance);
424       }
425     }
426   }
427 
428   private void tensorFiniteDifference(MultipoleTensor multipoleTensor,
429       double delta2,
430       double[] tensorsPx, double[] tensorsNx,
431       double[] tensorsPy, double[] tensorsNy,
432       double[] tensorsPz, double[] tensorsNz) {
433 
434     int start = 0;
435 
436     // We do not calculate the zeroth term for Thole damping.
437     if (operator == OPERATOR.THOLE_FIELD) {
438       start = 1;
439     }
440 
441     // Test the partial derivatives for all tensor components.
442     for (int l = start; l < order; l++) {
443       // Test X derivative
444       double expect = tensor[multipoleTensor.ti(l + 1, 0, 0)];
445       double actual =
446           (tensorsPx[multipoleTensor.ti(l, 0, 0)] - tensorsNx[multipoleTensor.ti(l, 0, 0)]) / delta2;
447       assertEquals(info + " @ " + l, expect, actual, fdTolerance);
448       // Test Y derivative
449       expect = tensor[multipoleTensor.ti(l, 1, 0)];
450       actual =
451           (tensorsPy[multipoleTensor.ti(l, 0, 0)] - tensorsNy[multipoleTensor.ti(l, 0, 0)]) / delta2;
452       assertEquals(info + " @ " + l, expect, actual, fdTolerance);
453       // Test Z derivative
454       expect = tensor[multipoleTensor.ti(l, 0, 1)];
455       actual =
456           (tensorsPz[multipoleTensor.ti(l, 0, 0)] - tensorsNz[multipoleTensor.ti(l, 0, 0)]) / delta2;
457       assertEquals(info + " @ " + l, expect, actual, fdTolerance);
458     }
459     for (int l = 0; l < order; l++) {
460       for (int m = 1; m < order - l; m++) {
461         // Test X derivative
462         double expect = tensor[multipoleTensor.ti(l + 1, m, 0)];
463         double actual =
464             (tensorsPx[multipoleTensor.ti(l, m, 0)] - tensorsNx[multipoleTensor.ti(l, m, 0)])
465                 / delta2;
466         assertEquals(info + " " + l + " " + m, expect, actual, fdTolerance);
467         // Test Y derivative
468         expect = tensor[multipoleTensor.ti(l, m + 1, 0)];
469         actual =
470             (tensorsPy[multipoleTensor.ti(l, m, 0)] - tensorsNy[multipoleTensor.ti(l, m, 0)])
471                 / delta2;
472         assertEquals(info + " " + l + " " + m, expect, actual, fdTolerance);
473         // Test Z derivative
474         expect = tensor[multipoleTensor.ti(l, m, 1)];
475         actual =
476             (tensorsPz[multipoleTensor.ti(l, m, 0)] - tensorsNz[multipoleTensor.ti(l, m, 0)])
477                 / delta2;
478         assertEquals(info + " " + l + " " + m, expect, actual, fdTolerance);
479       }
480     }
481     // Find (d/dx)^l * (d/dy)^m * (d/dz)^n for l + m + n = 1..o (n >= 1)
482     for (int l = 0; l < order; l++) {
483       for (int m = 0; m < order - l; m++) {
484         for (int n = 1; n < order - l - m; n++) {
485           // Test X derivative
486           double expect = tensor[multipoleTensor.ti(l + 1, m, n)];
487           double actual =
488               (tensorsPx[multipoleTensor.ti(l, m, n)] - tensorsNx[multipoleTensor.ti(l, m, n)])
489                   / delta2;
490           assertEquals(info + " " + l + " " + m, expect, actual, fdTolerance);
491           // Test Y derivative
492           expect = tensor[multipoleTensor.ti(l, m + 1, n)];
493           actual =
494               (tensorsPy[multipoleTensor.ti(l, m, n)] - tensorsNy[multipoleTensor.ti(l, m, n)])
495                   / delta2;
496           assertEquals(info + " " + l + " " + m, expect, actual, fdTolerance);
497           // Test Z derivative
498           expect = tensor[multipoleTensor.ti(l, m, n + 1)];
499           actual =
500               (tensorsPz[multipoleTensor.ti(l, m, n)] - tensorsNz[multipoleTensor.ti(l, m, n)])
501                   / delta2;
502           assertEquals(info + " " + l + " " + m, expect, actual, fdTolerance);
503         }
504       }
505     }
506   }
507 
508 }