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.commands;
39  
40  import ffx.crystal.Crystal;
41  import ffx.crystal.ReplicatesCrystal;
42  import ffx.numerics.Potential;
43  import ffx.potential.ForceFieldEnergy;
44  import ffx.potential.MolecularAssembly;
45  import ffx.potential.bonded.Atom;
46  import ffx.potential.bonded.MSNode;
47  import ffx.potential.bonded.Polymer;
48  import ffx.potential.cli.PotentialCommand;
49  import ffx.potential.extended.ExtendedSystem;
50  import ffx.potential.utils.ConvexHullOps;
51  import ffx.utilities.Constants;
52  import ffx.utilities.FFXBinding;
53  import org.apache.commons.configuration2.Configuration;
54  import org.apache.commons.io.FilenameUtils;
55  import picocli.CommandLine.Command;
56  import picocli.CommandLine.Option;
57  import picocli.CommandLine.Parameters;
58  
59  import java.io.BufferedReader;
60  import java.io.File;
61  import java.io.FileReader;
62  import java.io.IOException;
63  import java.util.ArrayList;
64  import java.util.Arrays;
65  import java.util.Collections;
66  import java.util.HashSet;
67  import java.util.List;
68  import java.util.Random;
69  import java.util.Set;
70  import java.util.logging.Logger;
71  import java.util.regex.Matcher;
72  import java.util.regex.Pattern;
73  import java.util.stream.Collectors;
74  
75  import static java.lang.String.format;
76  import static org.apache.commons.math3.util.FastMath.round;
77  
78  /**
79   * The Solvator script puts a box of solvent around a solute.
80   * <br>
81   * Usage:
82   * <br>
83   * ffxc Solvator [options] &lt;filename&gt;
84   */
85  @Command(description = " Creates a box of solvent around a solute.", name = "Solvator")
86  public class Solvator extends PotentialCommand {
87  
88    @Option(names = {"--sFi", "--solventFile"}, paramLabel = "water",
89        description = "A file containing the solvent box to be used. There is currently no default.")
90    private String solventFileName = null;
91  
92    @Option(names = {"--iFi", "--ionFile"}, paramLabel = "ions",
93        description = "Name of the file containing ions. Must also have a .ions file (e.g. nacl.pdb must also have nacl.ions). Default: no ions.")
94    private String ionFileName = null;
95  
96    @Option(names = {"-r", "--rectangular"}, paramLabel = "false", defaultValue = "false",
97        description = "Use a rectangular prism rather than a cube for solvation.")
98    private boolean rectangular = false;
99  
100   @Option(names = {"-p", "--padding"}, paramLabel = "9.0", defaultValue = "9.0",
101       description = "Sets the minimum amount of solvent padding around the solute.")
102   private double padding = 9.0;
103 
104   @Option(names = {"-b", "--boundary"}, paramLabel = "2.5", defaultValue = "2.5",
105       description = "Delete solvent molecules that infringe closer than this to the solute.")
106   private double boundary = 2.5;
107 
108   @Option(names = {"-x", "--translate"}, paramLabel = "true", defaultValue = "true",
109       description = "Move solute molecules to center of box. Turning off should only considered when specifying the unit cell box dimensions")
110   private boolean translate = true;
111 
112   @Option(names = {"--abc", "--boxLengths"}, paramLabel = "a,b,c",
113       description = "Specify a comma-separated set of unit cell box lengths, instead of calculating them (a,b,c)")
114   private String manualBox = null;
115 
116   @Option(names = {"-s", "--randomSeed"}, paramLabel = "auto",
117       description = "Specify a random seed for ion placement.")
118   private String seedString = null;
119 
120   @Option(names = {"--pH", "--constantPH"}, paramLabel = "7.4",
121       description = "pH value for the system. If set, titration states will be initialized based on this pH.")
122   private Double pH = null;
123 
124   @Parameters(arity = "1", paramLabel = "file",
125       description = "The atomic coordinate file in PDB or XYZ format.")
126   String filename = null;
127 
128   private MolecularAssembly solute;
129   private MolecularAssembly solvent;
130   private MolecularAssembly ions;
131   private File createdFile;
132 
133   private Configuration additionalProperties;
134 
135   public Solvator() {
136     super();
137   }
138 
139   public Solvator(FFXBinding binding) {
140     super(binding);
141   }
142 
143   public Solvator(String[] args) {
144     super(args);
145   }
146 
147   public void setProperties(Configuration additionalProps) {
148     this.additionalProperties = additionalProps;
149   }
150 
151   @Override
152   public Solvator run() {
153     // Init the context and bind variables.
154     if (!init()) {
155       return this;
156     }
157 
158     if (solventFileName == null) {
159       logger.info(helpString());
160       return this;
161     }
162 
163     // Reduce cutoff distances to avoid ill behavior caused by default aperiodic 900-1000A cutoffs.
164     double twoBoundary = 2.0 * boundary;
165     String nlistCuts = Double.toString(twoBoundary);
166     System.setProperty("vdw-cutoff", nlistCuts);
167     System.setProperty("ewald-cutoff", nlistCuts);
168 
169     // Load the MolecularAssembly.
170     activeAssembly = getActiveAssembly(filename);
171     if (activeAssembly == null) {
172       logger.info(helpString());
173       return this;
174     }
175 
176     // Calculate the monopole moment (total charge) of the system
177     ForceFieldEnergy forceFieldEnergy = activeAssembly.getPotentialEnergy();
178 
179     // If pH is specified, create and attach an ExtendedSystem
180     if (pH != null) {
181       logger.info("\n Initializing titration states for pH " + pH);
182 
183       ExtendedSystem esvSystem = new ExtendedSystem(activeAssembly, pH, null);
184       forceFieldEnergy.attachExtendedSystem(esvSystem);
185 
186       // Set titration states based on pH
187       esvSystem.reGuessLambdas();
188       logger.info(esvSystem.getLambdaList());
189     }
190 
191     // Coordinates and energy call to ensure PME and other potentials are fully initialized
192     int nVars = forceFieldEnergy.getNumberOfVariables();
193     double[] coordinates = new double[nVars];
194     forceFieldEnergy.getCoordinates(coordinates);
195     forceFieldEnergy.energy(coordinates, true);
196 
197     // Now safely compute the monopole (total charge)
198     if (forceFieldEnergy.getPmeNode() == null) {
199       logger.severe("PME (electrostatics) is not initialized. Check force field settings.");
200       return this;
201     }
202 
203     Atom[] activeAtoms = activeAssembly.getActiveAtomArray();
204     if (activeAtoms == null || activeAtoms.length == 0) {
205       logger.severe("No active atoms found in the assembly.");
206       return this;
207     }
208 
209     double[] moments;
210     try {
211       moments = forceFieldEnergy.getPmeNode().computeMoments(activeAtoms, false);
212       if (moments == null || moments.length == 0) {
213         logger.severe("Failed to compute moments (null or empty array returned).");
214         return this;
215       }
216       logger.info(format("\n System has a total charge of %8.4g before solvation", moments[0]));
217     } catch (Exception e) {
218       logger.severe("Error computing moments: " + e.getMessage());
219       return this;
220     }
221 
222     solute = activeAssembly;
223     ForceFieldEnergy soluteEnergy = activeAssembly.getPotentialEnergy();
224 
225     solvent = potentialFunctions.open(solventFileName);
226     Atom[] baseSolventAtoms = solvent.getActiveAtomArray();
227 
228     Atom[] ionAtoms = null;
229     File ionsFile = null;
230     if (ionFileName != null) {
231       ions = potentialFunctions.open(ionFileName);
232       ionAtoms = ions.getActiveAtomArray();
233       String ionsName = FilenameUtils.removeExtension(ionFileName);
234       ionsName = ionsName + ".ions";
235       ionsFile = new File(ionsName);
236       assert ionsFile != null && ionsFile.exists();
237     }
238 
239     Atom[] soluteAtoms = activeAssembly.getAtomArray();
240     int nSolute = soluteAtoms.length;
241     double[][] soluteCoordinates = new double[nSolute][3];
242 
243     Crystal soluteCrystal = activeAssembly.getCrystal();
244     Crystal solventCrystal = solvent.getCrystal();
245     if (solventCrystal instanceof ReplicatesCrystal) {
246       solventCrystal = solventCrystal.getUnitCell();
247     }
248 
249     if (!soluteCrystal.aperiodic()) {
250       if (!soluteCrystal.spaceGroup.shortName.equalsIgnoreCase("P1")) {
251         logger.severe(" Solute must be aperiodic or strictly P1 periodic");
252       }
253     }
254 
255     if (solventCrystal.aperiodic() || !solventCrystal.spaceGroup.shortName.equalsIgnoreCase("P1")) {
256       logger.severe(" Solvent box must be periodic (and P1)!");
257     }
258 
259     for (int i = 0; i < nSolute; i++) {
260       Atom ati = soluteAtoms[i];
261       double[] xyzi = new double[3];
262       xyzi = ati.getXYZ(xyzi);
263       System.arraycopy(xyzi, 0, soluteCoordinates[i], 0, 3);
264     }
265 
266     if (!soluteCrystal.aperiodic()) {
267       for (Atom ati : soluteAtoms) {
268         double[] xyzi = new double[3];
269         soluteCrystal.image(ati.getXYZ(xyzi));
270         ati.setXYZ(xyzi);
271       }
272     }
273 
274     double[] minSoluteXYZ = new double[3];
275     double[] maxSoluteXYZ = new double[3];
276     double[] soluteBoundingBox = new double[3];
277     for (int i = 0; i < 3; i++) {
278       final int index = i;
279       minSoluteXYZ[i] = Arrays.stream(soluteCoordinates).mapToDouble(xyz -> xyz[index]).min().getAsDouble();
280       maxSoluteXYZ[i] = Arrays.stream(soluteCoordinates).mapToDouble(xyz -> xyz[index]).max().getAsDouble();
281       soluteBoundingBox[i] = maxSoluteXYZ[i] - minSoluteXYZ[i];
282     }
283 
284     double soluteMass = Arrays.stream(soluteAtoms).mapToDouble(Atom::getMass).sum();
285     double invSolMass = 1.0 / soluteMass;
286     double[] origCoM = new double[3];
287     for (Atom ati : soluteAtoms) {
288       double massi = ati.getMass();
289       massi *= invSolMass;
290       double[] xyzi = new double[3];
291       xyzi = ati.getXYZ(xyzi);
292       for (int j = 0; j < 3; j++) {
293         origCoM[j] += (xyzi[j] * massi);
294       }
295     }
296 
297     logger.fine(format(" Original CoM: %s", Arrays.toString(origCoM)));
298 
299     double[] newBox = new double[3];
300     if (manualBox != null) {
301       String[] toks = manualBox.split(",");
302       if (toks.length == 1) {
303         double len = Double.parseDouble(manualBox);
304         Arrays.fill(newBox, len);
305       } else if (toks.length == 3) {
306         for (int i = 0; i < 3; i++) {
307           newBox[i] = Double.parseDouble(toks[i]);
308           if (newBox[i] <= 0) {
309             logger.severe(" Specified a box length of less than zero!");
310           }
311         }
312       } else {
313         logger.severe(" The manualBox option requires either 1 box length, or 3 comma-separated values.");
314       }
315     } else {
316       if (rectangular) {
317         newBox = Arrays.stream(soluteBoundingBox).map(x -> x + (2.0 * padding)).toArray();
318       } else {
319         double soluteLinearSize = ConvexHullOps.maxDist(soluteAtoms);
320         soluteLinearSize += (2.0 * padding);
321         Arrays.fill(newBox, soluteLinearSize);
322       }
323     }
324 
325     logger.info(format(" Molecule will be solvated in a periodic box of size %10.4g, %10.4g, %10.4g",
326         newBox[0], newBox[1], newBox[2]));
327 
328     if (translate) {
329       double[] soluteTranslate = new double[3];
330       for (int i = 0; i < 3; i++) {
331         soluteTranslate[i] = 0.5 * newBox[i];
332         soluteTranslate[i] -= origCoM[i];
333       }
334       for (Atom atom : soluteAtoms) {
335         atom.move(soluteTranslate);
336       }
337       logger.fine(format(" Solute translated by %s", Arrays.toString(soluteTranslate)));
338     }
339 
340     solvent.moveAllIntoUnitCell();
341 
342     int nSolvent = baseSolventAtoms.length;
343     double[][] baseSolventCoordinates = new double[nSolvent][3];
344     for (int i = 0; i < nSolvent; i++) {
345       Atom ati = baseSolventAtoms[i];
346       double[] xyzi = new double[3];
347       xyzi = ati.getXYZ(xyzi);
348       System.arraycopy(xyzi, 0, baseSolventCoordinates[i], 0, 3);
349     }
350 
351     logger.info(format(" Solute size: %12.4g, %12.4g, %12.4g",
352         soluteBoundingBox[0], soluteBoundingBox[1], soluteBoundingBox[2]));
353 
354     int[] solventReplicas = new int[3];
355     double[] solventBoxVectors = new double[3];
356     solventBoxVectors[0] = solventCrystal.a;
357     solventBoxVectors[1] = solventCrystal.b;
358     solventBoxVectors[2] = solventCrystal.c;
359 
360     for (int i = 0; i < 3; i++) {
361       solventReplicas[i] = (int) Math.ceil((newBox[i] / solventBoxVectors[i]));
362     }
363 
364     Crystal newCrystal = new Crystal(newBox[0], newBox[1], newBox[2], 90, 90, 90, "P1");
365     forceFieldEnergy.setCrystal(newCrystal, true);
366 
367     List<MSNode> bondedNodes = solvent.getAllBondedEntities();
368     MSNode[] solventEntities = bondedNodes.toArray(new MSNode[0]);
369 
370     int nSolventMols = solventEntities.length;
371     double[][] solventCOMs = new double[nSolventMols][];
372     for (int i = 0; i < nSolventMols; i++) {
373       List<Atom> moleculeAtoms = solventEntities[i].getAtomList();
374 
375       double totMass = 0.0;
376       for (Atom atom : moleculeAtoms) {
377         totMass += atom.getMass();
378       }
379 
380       double invMass = 1.0 / totMass;
381       double[] com = new double[3];
382       Arrays.fill(com, 0);
383 
384       for (Atom atom : moleculeAtoms) {
385         double[] xyz = new double[3];
386         xyz = atom.getXYZ(xyz);
387         double mass = atom.getAtomType().atomicWeight;
388         for (int j = 0; j < 3; j++) {
389           com[j] += (mass * xyz[j] * invMass);
390         }
391       }
392       solventCrystal.toPrimaryCell(com, com);
393       solventCOMs[i] = com;
394     }
395 
396     double[] xyzOffset = new double[3];
397     int currXYZIndex = Arrays.stream(soluteAtoms).mapToInt(Atom::getXyzIndex).max().getAsInt();
398     int currResSeq = 1;
399 
400     char[] possibleChains = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
401     char solventChain = ' ';
402     Set<Character> soluteChains = Arrays.stream(soluteAtoms).map(Atom::getChainID).collect(Collectors.toSet());
403     for (char solvChainOpt : possibleChains) {
404       if (!soluteChains.contains(solvChainOpt)) {
405         solventChain = solvChainOpt;
406         break;
407       }
408     }
409     char ionChain = ' ';
410     for (char solvChainOpt : possibleChains) {
411       if (solvChainOpt != solventChain && !soluteChains.contains(solvChainOpt)) {
412         ionChain = solvChainOpt;
413         break;
414       }
415     }
416 
417     // Decompose the solute into domains so solute-solvent clashes can be quickly checked.
418     DomainDecomposition ddc = new DomainDecomposition(boundary, soluteAtoms, newCrystal);
419 
420     // Unfortunately, Groovy treats ' ' as a String, not as a char.
421     if (solventChain == ' ') {
422       logger.severe(" Could not find an unused character A-Z for the new solvent!");
423     }
424     logger.info(format(" New solvent molecules will be placed in chain %c", solventChain));
425     if (ionChain == ' ') {
426       logger.severe(" Could not find an unused character A-Z for the new solvent!");
427     }
428     logger.info(format(" New ions will be placed in chain %c", ionChain));
429 
430     // Accumulator for new solvent molecules.
431     // Currently implemented as an ArrayList with the notion of removing from the end of the List.
432     List<Atom[]> newMolecules = new ArrayList<>();
433 
434     for (int ai = 0; ai < solventReplicas[0]; ai++) {
435       xyzOffset[0] = ai * solventBoxVectors[0];
436       for (int bi = 0; bi < solventReplicas[1]; bi++) {
437         xyzOffset[1] = bi * solventBoxVectors[1];
438         for (int ci = 0; ci < solventReplicas[2]; ci++) {
439           xyzOffset[2] = ci * solventBoxVectors[2];
440 
441           logger.info(format(" Tiling solvent replica %d,%d,%d", ai + 1, bi + 1, ci + 1));
442 
443           MoleculePlace:
444           for (int i = 0; i < nSolventMols; i++) {
445             MSNode moli = solventEntities[i];
446             double[] comi = new double[3];
447             for (int j = 0; j < 3; j++) {
448               comi[j] = xyzOffset[j] + solventCOMs[i][j];
449               if (comi[j] < 0) {
450                 logger.warning(format(
451                     " Skipping a copy of solvent molecule %d for violating minimum boundary 0,0,0. This should not occur!",
452                     i));
453                 continue MoleculePlace;
454               } else if (comi[j] > newBox[j]) {
455                 logger.fine(format(
456                     " Skipping a copy of solvent molecule %d for violating maximum boundary %12.4g,%12.4g,%12.4g",
457                     i, newBox[0], newBox[1], newBox[2]));
458                 continue MoleculePlace;
459               }
460             }
461 
462             logger.fine(format(" Placing a molecule at %f,%f,%f", comi[0], comi[1], comi[2]));
463 
464             List<Atom> parentAtoms = moli.getAtomList();
465             int nMolAtoms = parentAtoms.size();
466             Atom[] newAtomArray = new Atom[nMolAtoms];
467             for (int atI = 0; atI < nMolAtoms; atI++) {
468               Atom parentAtom = parentAtoms.get(atI);
469               double[] newXYZ = new double[3];
470               newXYZ = parentAtom.getXYZ(newXYZ);
471               for (int j = 0; j < 3; j++) {
472                 newXYZ[j] += xyzOffset[j];
473               }
474               Atom newAtom = new Atom(++currXYZIndex, parentAtom, newXYZ, currResSeq, solventChain,
475                   Character.toString(solventChain));
476               logger.fine(
477                   format(" New atom %s at chain %c on residue %s-%d", newAtom, newAtom.getChainID(),
478                       newAtom.getResidueName(), newAtom.getResidueNumber()));
479               newAtom.setHetero(!(moli instanceof Polymer));
480               newAtom.setResName(moli.getName());
481               if (newAtom.getAltLoc() == null) {
482                 newAtom.setAltLoc(' ');
483               }
484               newAtomArray[atI] = newAtom;
485             }
486 
487             boolean overlapFound = false;
488             for (Atom atom : newAtomArray) {
489               if (ddc.checkClashes(atom, boundary)) {
490                 overlapFound = true;
491                 break;
492               }
493             }
494 
495             if (overlapFound) {
496               logger.fine(
497                   format(" Skipping a copy of molecule %d for overlapping with the solute.", i));
498               continue;
499             }
500 
501             newMolecules.add(newAtomArray);
502             ++currResSeq;
503           }
504         }
505       }
506     }
507 
508     Random random;
509     if (seedString == null) {
510       random = new Random();
511     } else {
512       random = new Random(Long.parseLong(seedString));
513     }
514     Collections.shuffle(newMolecules, random);
515 
516     if (ionFileName != null) {
517       logger.info(format(" Ions will be placed into chain %c", ionChain));
518       double volume = newBox[0] * newBox[1] * newBox[2];
519       // newCrystal.volume may also work.
520       double ionsPermM = volume * Constants.LITERS_PER_CUBIC_ANGSTROM * Constants.AVOGADRO * 1E-3;
521 
522       List<IonAddition> byConc = new ArrayList<>();
523       IonAddition neutAnion = null;
524       IonAddition neutCation = null;
525 
526       Pattern ionicPattern =
527           Pattern.compile("^\\s*([0-9]+) +([0-9]+) +([0-9]+(?:\\.[0-9]*)?|NEUT\\S*)");
528       Pattern concPatt = Pattern.compile("^[0-9]+(?:\\.[0-9]*)?");
529       // Parse .ions file to figure out which ions need to be added.
530       try (BufferedReader br = new BufferedReader(new FileReader(ionsFile))) {
531         String line = br.readLine();
532         while (line != null) {
533           Matcher m = ionicPattern.matcher(line);
534           if (m.matches()) {
535             int start = Integer.parseInt(m.group(1));
536             int end = Integer.parseInt(m.group(2));
537             if (end < start) {
538               throw new IllegalArgumentException(" end < start");
539             }
540             int nAts = (end - start) + 1;
541 
542             Atom[] atoms = new Atom[nAts];
543             for (int i = 0; i < nAts; i++) {
544               int atI = start + i - 1;
545               atoms[i] = ionAtoms[atI];
546             }
547 
548             double conc = 0;
549             boolean toNeutralize;
550             if (concPatt.matcher(m.group(3)).matches()) {
551               conc = Double.parseDouble(m.group(3));
552               toNeutralize = false;
553             } else {
554               toNeutralize = true;
555             }
556             IonAddition ia = new IonAddition(atoms, conc, toNeutralize);
557             if (toNeutralize) {
558               if (ia.charge > 0) {
559                 neutCation = ia;
560               } else if (ia.charge < 0) {
561                 neutAnion = ia;
562               } else {
563                 logger.severe(format(" Specified a neutralizing ion %s with no net charge!",
564                     ia));
565               }
566             } else {
567               byConc.add(ia);
568             }
569           }
570           line = br.readLine();
571         }
572       } catch (IOException e) {
573         logger.severe("Error reading ions file: " + e.getMessage());
574         throw new RuntimeException(e);
575       }
576 
577       List<Atom[]> addedIons = new ArrayList<>();
578       int ionResSeq = 0;
579 
580       // Begin swapping waters for ions.
581       // Use the monopole moment (total charge) we calculated earlier
582       double initialCharge = moments[0];
583       logger.info(format(" Charge before addition of ions is %8.4g", initialCharge));
584 
585       for (IonAddition ia : byConc) {
586         logger.info(ia.toString());
587         int nIons = (int) Math.ceil(ionsPermM * ia.conc);
588         if (nIons > newMolecules.size()) {
589           logger.severe(
590               format(" Insufficient solvent molecules remain (%d) to add %d ions!", newMolecules.size(), nIons));
591         }
592         logger.info(format(" Number of ions to place: %d\n", nIons));
593         for (int i = 0; i < nIons; i++) {
594           Atom[] newIon = swapIon(newMolecules, ia, currXYZIndex, ionChain, ionResSeq++);
595           currXYZIndex += newIon.length;
596           addedIons.add(newIon);
597         }
598         initialCharge += nIons * ia.charge;
599       }
600 
601       logger.info(format(" Charge before neutralization is %8.4g", initialCharge));
602       if (initialCharge > 0) {
603         if (neutAnion == null) {
604           logger.info(
605               format(" No counter-anion specified; system will be cationic at charge %8.4g", initialCharge));
606         } else {
607           logger.info(format(" Neutralizing system with %s", neutAnion));
608           double charge = neutAnion.charge;
609           int nAnions = (int) round(-1.0 * (initialCharge / charge));
610           double netCharge = initialCharge + (nAnions * charge);
611           if (nAnions > newMolecules.size()) {
612             logger.severe(
613                 format(" Insufficient solvent molecules remain (%d) to add %d counter-anions!", newMolecules.size(), nAnions));
614           }
615           for (int i = 0; i < nAnions; i++) {
616             Atom[] newIon = swapIon(newMolecules, neutAnion, currXYZIndex, ionChain, ionResSeq++);
617             currXYZIndex += newIon.length;
618             addedIons.add(newIon);
619           }
620           logger.info(
621               format(" System neutralized to %8.4g charge with %d counter-anions", netCharge,
622                   nAnions));
623         }
624       } else if (initialCharge < 0) {
625         if (neutCation == null) {
626           logger.info(
627               format(" No counter-cation specified; system will be anionic at charge %8.4g", initialCharge));
628         } else {
629           logger.info(format(" Neutralizing system with %s", neutCation));
630           double charge = neutCation.charge;
631           int nCations = (int) round(-1.0 * (initialCharge / charge));
632           double netCharge = initialCharge + (nCations * charge);
633           if (nCations > newMolecules.size()) {
634             logger.severe(
635                 format(" Insufficient solvent molecules remain (%d) to add %d counter-cations!", newMolecules.size(), nCations));
636           }
637           for (int i = 0; i < nCations; i++) {
638             Atom[] newIon = swapIon(newMolecules, neutCation, currXYZIndex, ionChain, ionResSeq++);
639             currXYZIndex += newIon.length;
640             addedIons.add(newIon);
641           }
642           logger.info(
643               format(" System neutralized to %8.4g charge with %d counter-cations", netCharge,
644                   nCations));
645         }
646       } else {
647         logger.info(" System is neutral; no neutralizing ions needed.");
648       }
649       newMolecules.addAll(addedIons);
650     }
651 
652     logger.info(" Adding solvent and ion atoms to system...");
653     long time = -System.nanoTime();
654     for (Atom[] atoms : newMolecules) {
655       for (Atom atom : atoms) {
656         atom.setHetero(true);
657         activeAssembly.addMSNode(atom);
658       }
659     }
660     time += System.nanoTime();
661     logger.info(format(" Solvent and ions added in %12.4g sec", time * Constants.NS2SEC));
662     time = -System.nanoTime();
663 
664     String solvatedName = activeAssembly.getFile().getPath().replaceFirst("\\.[^.]+$", ".pdb");
665     createdFile = new File(solvatedName);
666     if (!soluteCrystal.aperiodic()) {
667       for (int i = 0; i < nSolute; i++) {
668         Atom ati = soluteAtoms[i];
669         ati.setXYZ(soluteCoordinates[i]);
670       }
671     }
672     potentialFunctions.saveAsPDB(activeAssembly, createdFile);
673     time += System.nanoTime();
674     logger.info(format(" Structure written to disc in %12.4g sec", time * Constants.NS2SEC));
675 
676     return this;
677   }
678 
679   public File getWrittenFile() {
680     return createdFile;
681   }
682 
683   @Override
684   public List<Potential> getPotentials() {
685     List<Potential> potentials = new ArrayList<>(3);
686     if (ions != null) {
687       potentials.add(ions.getPotentialEnergy());
688     }
689     if (solvent != null) {
690       potentials.add(solvent.getPotentialEnergy());
691     }
692     if (solute != null) {
693       potentials.add(solute.getPotentialEnergy());
694     }
695     return potentials;
696   }
697 
698   /**
699    * A domain decomposition scheme for solute atoms intended for rapidly assessing solute-solvent clashes.
700    * Instead of a naive double loop over solute & solvent atoms, pre-place solute into this domain decomposition.
701    * Then, for each new solvent atom, check which domain it belongs to, and check all atoms in that domain and
702    * neighboring domains for possible clashes.
703    */
704   private static class DomainDecomposition {
705 
706     /**
707      * The logger for this class.
708      */
709     private static final Logger logger = Logger.getLogger(DomainDecomposition.class.getName());
710 
711     // Solute atoms belonging in each domain.
712     private final Atom[][][][] domains;
713     // Solute atoms in that domain and all surrounding domains.
714     private final Atom[][][][] withAdjacent;
715     private final int nX, nY, nZ;
716     private final double domainLength;
717     private final Crystal crystal;
718 
719     /**
720      * Return a set of integers corresponding to domains that may neighbor a current domain along an axis.
721      * <p>
722      * Checks for wraparound, out-of-bounds indices, and the final domain being subsized (and thus always
723      * needing to include the penultimate domain).
724      *
725      * @param i  Index to get neighbors for.
726      * @param nI Number of domains along this axis.
727      * @return All neighboring domain indices in this axis accounting for wraparound.
728      */
729     private static Set<Integer> neighborsWithWraparound(int i, int nI) {
730       List<Integer> boxesI = new ArrayList<>(3);
731       boxesI.add(i);
732       // Account for wraparound, and for box[nX-1] possibly being
733       // undersized (and thus needing to include box[nX-2])
734       if (i == 0) {
735         boxesI.add(nI - 1);
736         boxesI.add(nI - 2);
737         boxesI.add(1);
738       } else if (i == (nI - 2)) {
739         boxesI.add(nI - 3);
740         boxesI.add(nI - 1);
741         boxesI.add(0);
742       } else if (i == (nI - 1)) {
743         boxesI.add(nI - 2);
744         boxesI.add(0);
745       } else {
746         boxesI.add(i - 1);
747         boxesI.add(i + 1);
748       }
749 
750       // Eliminate out-of-bounds values and duplicate values.
751       Set<Integer> set = new HashSet<>();
752       for (Integer n : boxesI) {
753         if (n >= 0 && n < nI) {
754           set.add(n);
755         }
756       }
757       return set;
758     }
759 
760     DomainDecomposition(double boundary, Atom[] soluteAtoms, Crystal crystal) {
761       long time = -System.nanoTime();
762       domainLength = 2.1 * boundary;
763       nX = (int) (crystal.a / domainLength) + 1;
764       nY = (int) (crystal.b / domainLength) + 1;
765       nZ = (int) (crystal.c / domainLength) + 1;
766       this.crystal = crystal;
767 
768       // First, determine how many Atoms per domain.
769       // nAts will later be reused as a counter of "how many atoms placed in this domain".
770       int[][][] nAts = new int[nX][nY][nZ];
771       for (Atom at : soluteAtoms) {
772         double[] xyz = new double[3];
773         xyz = at.getXYZ(xyz);
774         int i = (int) (xyz[0] / domainLength);
775         int j = (int) (xyz[1] / domainLength);
776         int k = (int) (xyz[2] / domainLength);
777         nAts[i][j][k]++;
778       }
779 
780       // Build the domains.
781       domains = new Atom[nX][nY][nZ][];
782       for (int i = 0; i < nX; i++) {
783         for (int j = 0; j < nY; j++) {
784           for (int k = 0; k < nZ; k++) {
785             domains[i][j][k] = new Atom[nAts[i][j][k]];
786           }
787           // Reset the nAts array, which will be reused for placing atoms.
788           Arrays.fill(nAts[i][j], 0);
789         }
790       }
791 
792       // Add Atoms to domains.
793       for (Atom at : soluteAtoms) {
794         double[] xyz = new double[3];
795         xyz = at.getXYZ(xyz);
796         int i = (int) (xyz[0] / domainLength);
797         int j = (int) (xyz[1] / domainLength);
798         int k = (int) (xyz[2] / domainLength);
799         domains[i][j][k][nAts[i][j][k]++] = at;
800       }
801 
802       // Now construct a faster lookup array that contains all atoms from the domain and its neighbors.
803       // I know it's a 6-deep loop, but it's still overall O(N) AFAICT; it does run < 300 ms for a 14000-atom system.
804       withAdjacent = new Atom[nX][nY][nZ][];
805       for (int i = 0; i < nX; i++) {
806         Set<Integer> neighborsI = neighborsWithWraparound(i, nX);
807         for (int j = 0; j < nY; j++) {
808           Set<Integer> neighborsJ = neighborsWithWraparound(j, nY);
809           for (int k = 0; k < nZ; k++) {
810             Set<Integer> neighborsK = neighborsWithWraparound(k, nZ);
811             List<Atom> neighborAtoms = new ArrayList<>();
812             for (int l : neighborsI) {
813               for (int m : neighborsJ) {
814                 for (int n : neighborsK) {
815                   neighborAtoms.addAll(Arrays.asList(domains[l][m][n]));
816                 }
817               }
818             }
819             logger.fine(format(" Constructing domain %3d-%3d-%3d with %d atoms.", i, j, k,
820                 neighborAtoms.size()));
821             logger.fine(format(" Neighbors along X: %s", neighborsI));
822             logger.fine(format(" Neighbors along Y: %s", neighborsJ));
823             logger.fine(format(" Neighbors along Z: %s\n", neighborsK));
824             withAdjacent[i][j][k] = new Atom[neighborAtoms.size()];
825             withAdjacent[i][j][k] = neighborAtoms.toArray(withAdjacent[i][j][k]);
826           }
827         }
828       }
829 
830       int nBoxes = nX * nY * nZ;
831       double avgAtoms = ((double) soluteAtoms.length) / nBoxes;
832       time += System.nanoTime();
833       logger.info(format(
834           " Decomposed the solute into %d domains of side length %11.4g, averaging %10.3g atoms apiece in %8.3g sec.",
835           nBoxes, domainLength, avgAtoms, (time * 1E-9)));
836     }
837 
838     /**
839      * Check if an Atom may clash with something registered with this DomainDecomposition
840      *
841      * @param a         A new solvent atom.
842      * @param threshold Solute-solvent boundary.
843      * @return If a clash is detected over all solute atoms and symops.
844      */
845     boolean checkClashes(Atom a, double threshold) {
846       double[] xyz = new double[3];
847       xyz = a.getXYZ(xyz);
848       int i = (int) (xyz[0] / domainLength);
849       int j = (int) (xyz[1] / domainLength);
850       int k = (int) (xyz[2] / domainLength);
851       if (i >= nX) {
852         logger.fine(
853             format(" Atom %s violates the X boundary at %12.7g Angstroms", a, xyz[0]));
854         i = 0;
855       }
856       if (j >= nY) {
857         logger.fine(
858             format(" Atom %s violates the Y boundary at %12.7g Angstroms", a, xyz[1]));
859         j = 0;
860       }
861       if (k >= nZ) {
862         logger.fine(
863             format(" Atom %s violates the Y boundary at %12.7g Angstroms", a, xyz[2]));
864         k = 0;
865       }
866 
867       final double finalThreshold = threshold;
868       final double[] finalXyz = xyz;
869       return Arrays.stream(withAdjacent[i][j][k]).anyMatch(s -> {
870         double[] xyzS = new double[3];
871         xyzS = s.getXYZ(xyzS);
872         double dist = crystal.minDistOverSymOps(finalXyz, xyzS);
873         return dist < finalThreshold;
874       });
875     }
876   }
877 
878   private static class IonAddition {
879 
880     /**
881      * The logger for this class.
882      */
883     private static final Logger logger = Logger.getLogger(IonAddition.class.getName());
884 
885     // Once again, modestly bad practice by exposing these fields.
886     final double conc;
887     final boolean toNeutralize;
888     final Atom[] atoms;
889     private final int nAts;
890     final double[][] atomOffsets;
891     final double charge;
892 
893     IonAddition(Atom[] atoms, double conc, boolean toNeutralize) {
894       this.atoms = Arrays.copyOf(atoms, atoms.length);
895       this.conc = conc;
896       this.toNeutralize = toNeutralize;
897       charge = Arrays.stream(atoms).mapToDouble(atom -> atom.getMultipoleType().getCharge()).sum();
898       nAts = atoms.length;
899 
900       if (nAts > 1) {
901         double[] com = new double[3];
902         atomOffsets = new double[nAts][3];
903         Arrays.fill(com, 0);
904         double sumMass = 0;
905 
906         // Calculate ionic center of mass.
907         for (Atom atom : atoms) {
908           double mass = atom.getMass();
909           sumMass += mass;
910           double[] xyz = new double[3];
911           xyz = atom.getXYZ(xyz);
912 
913           for (int i = 0; i < 3; i++) {
914             xyz[i] *= mass;
915             com[i] += xyz[i];
916           }
917         }
918 
919         for (int i = 0; i < 3; i++) {
920           com[i] /= sumMass;
921         }
922 
923         // Calculate positions as an offset from CoM.
924         for (int i = 0; i < nAts; i++) {
925           Atom ai = atoms[i];
926           double[] xyz = new double[3];
927           xyz = ai.getXYZ(xyz);
928 
929           for (int j = 0; j < 3; j++) {
930             atomOffsets[i][j] = xyz[j] - com[j];
931           }
932         }
933       } else {
934         atomOffsets = new double[1][3];
935         Arrays.fill(atomOffsets[0], 0.0);
936       }
937     }
938 
939     /**
940      * Creates a new ion.
941      *
942      * @param com          Center of mass to place at.
943      * @param currXYZIndex XYZ index of the atom directly preceding the ones to add.
944      * @param chain        Chain to add ion to.
945      * @param resSeq       Residue number to assign to the ion.
946      * @return Newly created ion atoms.
947      */
948     Atom[] createIon(double[] com, int currXYZIndex, char chain, int resSeq) {
949       Atom[] newIonAts = new Atom[nAts];
950       for (int i = 0; i < nAts; i++) {
951         Atom fromAtom = atoms[i];
952         double[] newXYZ = new double[3];
953         System.arraycopy(com, 0, newXYZ, 0, 3);
954         for (int j = 0; j < 3; j++) {
955           newXYZ[j] += atomOffsets[i][j];
956         }
957         Atom newAtom = new Atom(++currXYZIndex, fromAtom, newXYZ, resSeq, chain,
958             Character.toString(chain));
959         logger.fine(format(" New ion atom %s at chain %c on ion molecule %s-%d", newAtom,
960             newAtom.getChainID(), newAtom.getResidueName(), newAtom.getResidueNumber()));
961         newAtom.setHetero(true);
962         newAtom.setResName(fromAtom.getResidueName());
963         if (newAtom.getAltLoc() == null) {
964           newAtom.setAltLoc(' ');
965         }
966         newIonAts[i] = newAtom;
967       }
968       return newIonAts;
969     }
970 
971     @Override
972     public String toString() {
973       StringBuilder sb = new StringBuilder(
974           format(" Ion addition with %d atoms per ion, concentration %10.3g mM, " +
975               "charge %6.2f, used to neutralize %b", atoms.length, conc, charge, toNeutralize));
976       for (Atom atom : atoms) {
977         sb.append("\n").append(" Includes atom ").append(atom.toString());
978       }
979       return sb.toString();
980     }
981   }
982 
983   /**
984    * Calculates the center of mass for an array of Atoms.
985    *
986    * @param atoms The atoms to calculate the center of mass of.
987    * @return Center of mass
988    */
989   private static double[] getCOM(Atom[] atoms) {
990     double totMass = 0;
991     double[] com = new double[3];
992     double[] xyz = new double[3];
993     for (Atom atom : atoms) {
994       xyz = atom.getXYZ(xyz);
995       double mass = atom.getMass();
996       totMass += mass;
997       double invTotMass = 1.0 / totMass;
998       for (int i = 0; i < 3; i++) {
999         xyz[i] -= com[i];
1000         xyz[i] *= (mass * invTotMass);
1001         com[i] += xyz[i];
1002       }
1003     }
1004     return com;
1005   }
1006 
1007   /**
1008    * Pops a solvent molecule off the end of the list, and returns an ion at its center of mass.
1009    *
1010    * @param solvent      Source of solvent molecules (last member will be removed).
1011    * @param ia           Ion to replace it with.
1012    * @param currXYZIndex Index of the atom directly preceding the ion to be placed.
1013    * @param chain        Chain to place the ion into.
1014    * @param resSeq       Residue number to assign.
1015    * @return Newly created set of ion atoms.
1016    */
1017   private static Atom[] swapIon(List<Atom[]> solvent, IonAddition ia, int currXYZIndex, char chain,
1018                                 int resSeq) {
1019     int nSolv = solvent.size() - 1;
1020     Atom[] lastSolvent = solvent.get(nSolv);
1021     solvent.remove(nSolv);
1022     double[] com = getCOM(lastSolvent);
1023     return ia.createIon(com, currXYZIndex, chain, resSeq);
1024   }
1025 }