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