1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
80
81
82
83
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
154 if (!init()) {
155 return this;
156 }
157
158 if (solventFileName == null) {
159 logger.info(helpString());
160 return this;
161 }
162
163
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
170 activeAssembly = getActiveAssembly(filename);
171 if (activeAssembly == null) {
172 logger.info(helpString());
173 return this;
174 }
175
176
177 ForceFieldEnergy forceFieldEnergy = activeAssembly.getPotentialEnergy();
178
179
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
187 esvSystem.reGuessLambdas();
188 logger.info(esvSystem.getLambdaList());
189 }
190
191
192 int nVars = forceFieldEnergy.getNumberOfVariables();
193 double[] coordinates = new double[nVars];
194 forceFieldEnergy.getCoordinates(coordinates);
195 forceFieldEnergy.energy(coordinates, true);
196
197
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
418 DomainDecomposition ddc = new DomainDecomposition(boundary, soluteAtoms, newCrystal);
419
420
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
431
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
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
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
581
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
700
701
702
703
704 private static class DomainDecomposition {
705
706
707
708
709 private static final Logger logger = Logger.getLogger(DomainDecomposition.class.getName());
710
711
712 private final Atom[][][][] domains;
713
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
721
722
723
724
725
726
727
728
729 private static Set<Integer> neighborsWithWraparound(int i, int nI) {
730 List<Integer> boxesI = new ArrayList<>(3);
731 boxesI.add(i);
732
733
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
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
769
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
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
788 Arrays.fill(nAts[i][j], 0);
789 }
790 }
791
792
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
803
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
840
841
842
843
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
882
883 private static final Logger logger = Logger.getLogger(IonAddition.class.getName());
884
885
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
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
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
941
942
943
944
945
946
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
985
986
987
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
1009
1010
1011
1012
1013
1014
1015
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 }