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