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.potential.nonbonded.octree;
39  
40  import java.util.ArrayList;
41  import java.util.logging.Logger;
42  import org.apache.commons.lang3.BooleanUtils;
43  
44  /**
45   * Octree method presented in the Fast Multipole Method (FMM) tutorial from the Barba Group:
46   * https://github.com/barbagroup/FMM_tutorial
47   */
48  public class Octree {
49  
50    private static final Logger logger = Logger.getLogger(Octree.class.getName());
51    /** List of all leaf cells */
52    private final ArrayList<OctreeCell> leaves = new ArrayList<>();
53    /**
54     * Critical (maximum allowed) number of points allowed in any one cell: If a cell already contains
55     * nCritical points, it needs to be split
56     */
57    private final int nCritical;
58    /** List of particles */
59    private final ArrayList<OctreeParticle> particles;
60    /** Tolerance parameter */
61    private final double theta;
62    /** List of cells */
63    private ArrayList<OctreeCell> cells = new ArrayList<>();
64  
65    /**
66     * Default constructor: only need to pass in a list of particles nCritical and theta set to
67     * defaults
68     *
69     * @param particles ArrayList of type OctreeParticles of all particles to be used in tree
70     */
71    public Octree(ArrayList<OctreeParticle> particles) {
72      this.particles = particles;
73      this.nCritical = 10;
74      this.theta = 0.5;
75    }
76  
77    /**
78     * Constructor allowing the specification of nCritical, default theta value
79     *
80     * @param nCritical Critical number of particles; cells must split when they reach nCritical
81     * @param particles ArrayList of type OctreeParticles of all particles to be used in tree
82     */
83    public Octree(int nCritical, ArrayList<OctreeParticle> particles) {
84      this.nCritical = nCritical;
85      this.particles = particles;
86      this.theta = 0.5;
87    }
88  
89    /**
90     * Constructor allowing the specification of nCritical and theta
91     *
92     * @param nCritical Critical number of particles; cells must split when they reach nCritical
93     * @param particles ArrayList of type OctreeParticles of all particles to be used in tree
94     * @param theta Specifies near field vs far field
95     */
96    public Octree(int nCritical, ArrayList<OctreeParticle> particles, double theta) {
97      this.nCritical = nCritical;
98      this.particles = particles;
99      this.theta = theta;
100   }
101 
102   /**
103    * Add a child.
104    *
105    * @param octant The octant.
106    * @param p Cell index p.
107    */
108   public void addChild(int octant, int p) {
109     OctreeCell tempCell = new OctreeCell(nCritical);
110 
111     // Create new cell
112     // TODO: should the cells array be passed in or the "this" value from the particular Octree?
113     cells.add(tempCell);
114 
115     // Last element of cells list is new child, c
116     int c = cells.size() - 1;
117 
118     // Geometric reference between parent and child
119     cells.get(c).setR((cells.get(p).getR()) * 0.5);
120     cells.get(c).setX((cells.get(p).getX()) * ((octant & 1) * 2 - 1));
121     cells.get(c).setY((cells.get(p).getY()) * ((octant & 2) - 1));
122     cells.get(c).setZ((cells.get(p).getZ()) * ((octant & 4) * 0.5 - 1));
123 
124     // Establish mutual reference in cells list
125     cells.get(c).setParentIndex(p);
126     cells.get(c).setChildren(octant, c);
127     cells.get(c).setnChild(cells.get(p).getnChild() | (1 << octant));
128   }
129 
130   /**
131    * Build the tree.
132    *
133    * @param root The Octree root cell.
134    */
135   public void buildTree(OctreeCell root) {
136     // (Re)Initialize cells to an empty array list
137     cells = new ArrayList<>();
138 
139     // Set root cell - add it to the cells array list
140     cells.add(root);
141 
142     // Build tree
143     int n = particles.size();
144 
145     for (int i = 0; i < n; i++) {
146       int current = 0;
147 
148       while (cells.get(current).getNumLeaves() >= nCritical) {
149         cells.get(current).setNumLeaves(cells.get(current).getNumLeaves() + 1);
150         int octX = 0;
151         int octY = 0;
152         int octZ = 0;
153 
154         if (particles.get(i).getX() > cells.get(current).getX()) {
155           octX = 1;
156         }
157         if (particles.get(i).getY() > cells.get(current).getY()) {
158           octY = 1;
159         }
160         if (particles.get(i).getZ() > cells.get(current).getZ()) {
161           octZ = 1;
162         }
163 
164         // Find particle's octant - should be an integer from 0 to 7
165         int octant = octX + (octY << 1) + (octZ << 2);
166 
167         // If there's not a child cell in the particle's octant, create one
168         boolean noChildInOctant =
169             BooleanUtils.toBoolean(cells.get(current).getnChild() & (1 << octant));
170         if (noChildInOctant) {
171           addChild(octant, current);
172         }
173 
174         current = cells.get(current).getChildAtIndex(octant);
175       }
176 
177       // Allocate the particle in the leaf cell
178       cells.get(current).setLeaf(cells.get(current).getNumLeaves(), i);
179       cells.get(current).setNumLeaves(cells.get(current).getNumLeaves() + 1);
180 
181       // Check whether to split cell
182       if (cells.get(current).getNumLeaves() >= nCritical) {
183         splitCell(current);
184       }
185     }
186 
187     // return cells;
188   }
189 
190   /** Direct summation. */
191   public void directSum() {
192     for (int i = 0; i < particles.size(); i++) {
193       for (int j = 0; j < particles.size(); j++) {
194         if (j != i) {
195           double r = particles.get(i).distance(particles.get(j));
196           particles.get(j).addToPhi(particles.get(j).getCharge() / r);
197         }
198       }
199     }
200     // Reset potential for all particles
201     for (OctreeParticle particle : particles) {
202       particle.addToPhi(0);
203     }
204   }
205 
206   /**
207    * Compute a distance between a position and the OctreePoint.
208    *
209    * @param array Position.
210    * @param point OctreePoint.
211    * @return Returns the distance.
212    */
213   public double distance(double[] array, OctreePoint point) {
214     return Math.sqrt(
215         Math.pow((array[0] - point.getX()), 2)
216             + Math.pow((array[1] - point.getY()), 2)
217             + Math.pow((array[2] - point.getZ()), 2));
218   }
219 
220   /** Evaluate potential at all target points */
221   public void evalPotnetial() {
222     for (int i = 0; i < particles.size(); i++) {
223       evalAtTarget(0, i);
224     }
225   }
226 
227   /**
228    * Compute the L2 error.
229    *
230    * @param phiDirect Potential from direct summation.
231    * @param phiTree Potential from the tree.
232    */
233   public void l2Error(double[] phiDirect, double[] phiTree) {
234     double errorSumNum = 0.0;
235     double errorSumDenom = 0.0;
236     for (int i = 0; i < phiDirect.length; i++) {
237       errorSumNum = errorSumNum + Math.pow((phiDirect[i] - phiTree[i]), 2);
238       errorSumDenom = errorSumDenom + Math.pow(phiDirect[i], 2);
239     }
240     double error = Math.sqrt(errorSumNum / errorSumDenom);
241     logger.info("L2 Norm Error: " + error);
242   }
243 
244   /** Update sweep. */
245   public void upwardSweep() {
246     for (int c = (cells.size() - 1); c > 0; c--) {
247       int p = cells.get(c).getParentIndex();
248       M2M(p, c);
249     }
250   }
251 
252   /**
253    * Spit a cell.
254    *
255    * @param p Cell index.
256    */
257   private void splitCell(int p) {
258     for (int i = 0; i < nCritical; i++) {
259       int octX = 0;
260       int octY = 0;
261       int octZ = 0;
262 
263       if (particles.get(i).getX() > cells.get(p).getX()) {
264         octX = 1;
265       }
266       if (particles.get(i).getY() > cells.get(p).getY()) {
267         octY = 1;
268       }
269       if (particles.get(i).getZ() > cells.get(p).getZ()) {
270         octZ = 1;
271       }
272 
273       // Find particle's octant - should be an integer from 0 to 7
274       int octant = octX + (octY << 1) + (octZ << 2);
275 
276       // If there's not a child cell in the particle's octant, create one
277       boolean noChildInOctant = BooleanUtils.toBoolean(cells.get(p).getnChild() & (1 << octant));
278       if (noChildInOctant) {
279         addChild(octant, p);
280       }
281 
282       // Reallocate the particle in the child cell
283       int c = cells.get(p).getChildAtIndex(octant);
284       cells.get(c).setLeaf(cells.get(c).getNumLeaves(), 1);
285       cells.get(c).setNumLeaves(cells.get(c).getNumLeaves() + 1);
286 
287       // Check if child cell reaches nCritical - split recursively if so
288       if (cells.get(c).getNumLeaves() >= nCritical) {
289         splitCell(c);
290       }
291     }
292   }
293 
294   /**
295    * Get a multipole
296    *
297    * @param p Cell index.
298    * @param leaves Leaves.
299    */
300   private void getMultipole(int p, ArrayList<Integer> leaves) {
301     // If the current cell is not a leaf cell, traverse down
302     if (cells.get(p).getNumLeaves() >= nCritical) {
303       for (int c = 0; c < 8; c++) {
304         if (BooleanUtils.toBoolean(cells.get(p).getnChild() & (1 << c))) {
305           getMultipole(cells.get(p).getChildAtIndex(c), leaves);
306         }
307       }
308     } else { // Otherwise, cell p is a leaf cell
309       // Loop in leaf particles, do P2M
310       for (int i = 0; i < cells.get(p).getNumLeaves(); i++) {
311         int l = cells.get(p).getLeavesValueAtIndex(i);
312         double dx = cells.get(p).getX() - particles.get(l).getX();
313         double dy = cells.get(p).getY() - particles.get(l).getY();
314         double dz = cells.get(p).getZ() - particles.get(l).getZ();
315 
316         // Calculate Multipole and fill array
317         double charge = particles.get(l).getCharge();
318         double[] calculatedMultipole = new double[10];
319         // Monopole (charge)
320         calculatedMultipole[0] = charge;
321         // Dipole
322         calculatedMultipole[1] = dx * charge;
323         calculatedMultipole[2] = dy * charge;
324         calculatedMultipole[3] = dz * charge;
325         // Quadropole
326         calculatedMultipole[4] = (Math.pow(dx, 2) * 0.5) * charge;
327         calculatedMultipole[5] = (Math.pow(dy, 2) * 0.5) * charge;
328         calculatedMultipole[6] = (Math.pow(dz, 2) * 0.5) * charge;
329         calculatedMultipole[7] = ((dx * dy) * 0.5) * charge;
330         calculatedMultipole[8] = ((dy * dz) * 0.5) * charge;
331         calculatedMultipole[9] = ((dz * dx) * 0.5) * charge;
332 
333         // Set Multipole
334         cells.get(p).addToMultipole(calculatedMultipole);
335 
336         // TODO: decide if leaves should be an array or ArrayList and adjust accordingly
337         leaves.add(p);
338       }
339     }
340   }
341 
342   /**
343    * M2M.
344    *
345    * @param p Cell index p.
346    * @param c Cell index c.
347    */
348   private void M2M(int p, int c) {
349     double dx = cells.get(p).getX() - cells.get(c).getX();
350     double dy = cells.get(p).getY() - cells.get(c).getY();
351     double dz = cells.get(p).getZ() - cells.get(c).getZ();
352 
353     double[] Dxyz = new double[] {dx, dy, dz};
354     double[] Dyzx = new double[] {dy, dz, dx};
355 
356     cells.get(p).addToMultipole(cells.get(c).getMultipole());
357 
358     double[] currentChildMultipole = cells.get(c).getMultipole();
359 
360     // Additional Multipole Terms
361     double[] additionalMultipoleTerms = new double[10];
362     // Added to charge
363     additionalMultipoleTerms[0] = 0;
364     // Added to Dipole
365     additionalMultipoleTerms[1] = currentChildMultipole[0] * Dxyz[0];
366     additionalMultipoleTerms[2] = currentChildMultipole[0] * Dxyz[1];
367     additionalMultipoleTerms[3] = currentChildMultipole[0] * Dxyz[2];
368     // Added to Quadropole
369     additionalMultipoleTerms[4] =
370         currentChildMultipole[1] * Dxyz[0] + 0.5 * currentChildMultipole[0] * Math.pow(Dxyz[0], 2);
371     additionalMultipoleTerms[5] =
372         currentChildMultipole[2] * Dxyz[1] + 0.5 * currentChildMultipole[0] * Math.pow(Dxyz[1], 2);
373     additionalMultipoleTerms[6] =
374         currentChildMultipole[3] * Dxyz[2] + 0.5 * currentChildMultipole[0] * Math.pow(Dxyz[2], 2);
375 
376     additionalMultipoleTerms[7] =
377         0.5 * currentChildMultipole[2] * Dyzx[0]
378             + 0.5 * currentChildMultipole[1] * Dxyz[0]
379             + 0.5 * currentChildMultipole[0] * Dxyz[0] * Dyzx[0];
380     additionalMultipoleTerms[8] =
381         0.5 * currentChildMultipole[3] * Dyzx[1]
382             + 0.5 * currentChildMultipole[2] * Dxyz[1]
383             + 0.5 * currentChildMultipole[0] * Dxyz[1] * Dyzx[1];
384     additionalMultipoleTerms[9] =
385         0.5 * currentChildMultipole[1] * Dyzx[2]
386             + 0.5 * currentChildMultipole[3] * Dxyz[2]
387             + 0.5 * currentChildMultipole[0] * Dxyz[2] * Dyzx[2];
388 
389     cells.get(p).addToMultipole(additionalMultipoleTerms);
390   }
391 
392   /**
393    * Evaluate potential at one target
394    *
395    * @param p Index of parent cell
396    * @param i Index of target particle
397    */
398   private void evalAtTarget(int p, int i) {
399 
400     // Non-leaf cell
401     if (cells.get(p).getNumLeaves() >= nCritical) {
402 
403       // Loop through p's child cells (8 octants)
404       for (int oct = 0; oct < 8; oct++) {
405         if (BooleanUtils.toBoolean(cells.get(p).getnChild() & (1 << oct))) {
406           int c = cells.get(p).getChildAtIndex(oct);
407           double r = particles.get(i).distance(cells.get(c));
408 
409           // Near field child cell
410           if (cells.get(c).getR() > theta * r) {
411             evalAtTarget(c, i);
412           } else { // Far field child cell
413             double dx = particles.get(i).getX() - cells.get(c).getX();
414             double dy = particles.get(i).getY() - cells.get(c).getY();
415             double dz = particles.get(i).getZ() - cells.get(c).getZ();
416             double r3 = Math.pow(r, 3);
417             double r5 = r3 * Math.pow(r, 2);
418 
419             // Calculate the weight from each multipole
420             double[] weight = new double[10];
421             weight[0] = 1 / r;
422             weight[1] = -dx / r3;
423             weight[2] = -dy / r3;
424             weight[3] = -dz / r3;
425             weight[4] = (3 * Math.pow(dx, 2)) / r5 - (1 / r3);
426             weight[5] = (3 * Math.pow(dy, 2)) / r5 - (1 / r3);
427             weight[6] = (3 * Math.pow(dz, 2)) / r5 - (1 / r3);
428             weight[7] = 3 * dx * dy / r5;
429             weight[8] = 3 * dy * dz / r5;
430             weight[9] = 3 * dz * dx / r5;
431 
432             // Calculate dot product of multipole array and weight array
433             double dotProduct = 0.0;
434             for (int d = 0; d < weight.length; d++) {
435               double[] multipoleArray = cells.get(c).getMultipole();
436               dotProduct = dotProduct + multipoleArray[d] * weight[d];
437             }
438 
439             particles.get(i).addToPhi(dotProduct);
440           }
441         } else { // Leaf Cell
442           // Loop in twig cell's particles
443           for (int j = 0; j < cells.get(p).getNumLeaves(); j++) {
444             OctreeParticle source = particles.get(cells.get(p).getLeavesValueAtIndex(j));
445             double r = particles.get(i).distance(source);
446             if (r != 0) {
447               particles.get(i).addToPhi(source.getCharge() / r);
448             }
449           }
450         }
451       }
452     }
453   }
454 }