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.cli;
39
40 import edu.rit.pj.ParallelTeam;
41 import ffx.numerics.Potential;
42 import ffx.numerics.switching.MultiplicativeSwitch;
43 import ffx.numerics.switching.PowerSwitch;
44 import ffx.numerics.switching.SquaredTrigSwitch;
45 import ffx.numerics.switching.UnivariateSwitchingFunction;
46 import ffx.potential.DualTopologyEnergy;
47 import ffx.potential.MolecularAssembly;
48 import ffx.potential.QuadTopologyEnergy;
49 import ffx.potential.bonded.Atom;
50 import picocli.CommandLine.ArgGroup;
51 import picocli.CommandLine.Option;
52
53 import javax.annotation.Nullable;
54 import java.util.Arrays;
55 import java.util.Collections;
56 import java.util.HashSet;
57 import java.util.List;
58 import java.util.Set;
59 import java.util.logging.Level;
60 import java.util.logging.Logger;
61 import java.util.regex.Matcher;
62 import java.util.stream.Collectors;
63
64 import static java.lang.Double.parseDouble;
65 import static java.lang.String.format;
66
67
68
69
70
71
72
73
74 public class TopologyOptions {
75
76
77
78
79 public static final Logger logger = Logger.getLogger(TopologyOptions.class.getName());
80
81
82
83
84 @ArgGroup(heading = "%n Alchemical Options for Dual and Quad Topologies%n", validate = false)
85 private final TopologyOptionGroup group = new TopologyOptionGroup();
86
87
88
89
90
91
92 public String getAlchemicalAtoms2() {
93 return group.alchemicalAtoms2;
94 }
95
96
97
98
99
100
101 public String getAlchemicalResidues2() {
102 return group.alchemicalResidues2;
103 }
104
105
106
107
108
109
110 public String getUnchargedAtoms2() {
111 return group.unchargedAtoms2;
112 }
113
114
115
116
117
118
119 public int getTopologiesInParallel() {
120 return group.nTopologiesInParallel;
121 }
122
123
124
125
126
127
128
129 public String getUnsharedA() {
130 return group.unsharedA;
131 }
132
133
134
135
136
137
138
139 public String getUnsharedB() {
140 return group.unsharedB;
141 }
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162 public String getLambdaFunction() {
163 return group.lambdaFunction;
164 }
165
166
167
168
169
170
171
172 public int getTopologiesInParallel(int nTopologies) {
173 int nThreads = ParallelTeam.getDefaultThreadCount();
174 int numParallel = getTopologiesInParallel();
175 if (nThreads % numParallel != 0) {
176 logger.warning(format(
177 " Number of threads available %d not evenly divisible by np %d; reverting to sequential.",
178 nThreads, numParallel));
179 numParallel = 1;
180 } else if (nTopologies % numParallel != 0) {
181 logger.warning(
182 format(" Number of topologies %d not evenly divisible by np %d; reverting to sequential.",
183 nTopologies, numParallel));
184 numParallel = 1;
185 }
186 return numParallel;
187 }
188
189
190
191
192
193
194
195 public int getThreadsPerTopology(int nTopologies) {
196 int nThreads = ParallelTeam.getDefaultThreadCount();
197 int numParallel = getTopologiesInParallel(nTopologies);
198 return nThreads / numParallel;
199 }
200
201
202
203
204
205
206
207 public int getNumberOfTopologies(@Nullable List<String> topologyFilenames) {
208 if (topologyFilenames == null || topologyFilenames.isEmpty()) {
209 logger.severe(" No filenames were provided.");
210 return 0;
211 } else if (topologyFilenames.size() != 1
212 && topologyFilenames.size() != 2
213 && topologyFilenames.size() != 4) {
214 logger.severe(" Either 1, 2, or 4 filenames must be provided.!");
215 return 0;
216 }
217 return topologyFilenames.size();
218 }
219
220
221
222
223
224
225
226
227
228
229
230 public Potential assemblePotential(MolecularAssembly[] assemblies, StringBuilder sb) {
231 int nargs = assemblies.length;
232 int numPar = getTopologiesInParallel(nargs);
233 UnivariateSwitchingFunction sf = nargs > 1 ? getSwitchingFunction() : null;
234 List<Integer> uniqueA;
235 List<Integer> uniqueB;
236 if (assemblies.length >= 4) {
237 uniqueA = getUniqueAtomsA(assemblies[0]);
238 uniqueB = getUniqueAtomsB(assemblies[2]);
239 } else {
240 uniqueA = Collections.emptyList();
241 uniqueB = Collections.emptyList();
242 }
243 return getTopology(assemblies, sf, uniqueA, uniqueB, numPar, sb);
244 }
245
246
247
248
249
250
251 public UnivariateSwitchingFunction getSwitchingFunction() {
252 UnivariateSwitchingFunction sf;
253 String lambdaFunction = getLambdaFunction();
254 if (!lambdaFunction.equalsIgnoreCase("1.0")) {
255 String lf = lambdaFunction.toUpperCase();
256 switch (lf) {
257 case "TRIG" -> sf = new SquaredTrigSwitch(false);
258 case "MULT" -> sf = new MultiplicativeSwitch(1.0, 0.0);
259 default -> {
260 try {
261 double beta = parseDouble(lf);
262 sf = new PowerSwitch(1.0, beta);
263 } catch (NumberFormatException ex) {
264 logger.warning(format(
265 "Argument to option -sf %s could not be properly parsed; using default linear switch",
266 lambdaFunction));
267 sf = new PowerSwitch(1.0, 1.0);
268 }
269 }
270 }
271 } else {
272 sf = new PowerSwitch(1.0, 1.0);
273 }
274 return sf;
275 }
276
277
278
279
280
281
282
283 public List<Integer> getUniqueAtomsA(MolecularAssembly topology) {
284 return getUniqueAtoms(topology, "A", getUnsharedA());
285 }
286
287
288
289
290
291
292
293
294
295
296
297
298 public Potential getTopology(MolecularAssembly[] topologies, UnivariateSwitchingFunction sf,
299 List<Integer> uniqueA, List<Integer> uniqueB, int numParallel, StringBuilder sb) {
300 Potential potential = null;
301 switch (topologies.length) {
302 case 1 -> {
303 sb.append("Single Topology ");
304 potential = topologies[0].getPotentialEnergy();
305 }
306 case 2 -> {
307 sb.append("Dual Topology ");
308 DualTopologyEnergy dte = DualTopologyEnergy.energyFactory(topologies[0], topologies[1], sf);
309 if (numParallel == 2) {
310 dte.setParallel(true);
311 }
312 potential = dte;
313 }
314 case 4 -> {
315 sb.append("Quad Topology ");
316 DualTopologyEnergy dta = new DualTopologyEnergy(topologies[0], topologies[1], sf);
317 DualTopologyEnergy dtb = new DualTopologyEnergy(topologies[3], topologies[2], sf);
318 QuadTopologyEnergy qte = new QuadTopologyEnergy(dta, dtb, uniqueA, uniqueB);
319 if (numParallel >= 2) {
320 qte.setParallel(true);
321 if (numParallel == 4) {
322 dta.setParallel(true);
323 dtb.setParallel(true);
324 }
325 }
326 potential = qte;
327 }
328 default -> logger.severe(" Must have 2, 4, or 8 topologies!");
329 }
330 sb.append(Arrays.stream(topologies).map(MolecularAssembly::toString)
331 .collect(Collectors.joining(", ", " [", "] ")));
332 return potential;
333 }
334
335
336
337
338
339
340
341
342
343 public static List<Integer> getUniqueAtoms(MolecularAssembly assembly, String label,
344 String unshared) {
345 if (!unshared.isEmpty()) {
346 logger.info(" Finding unique atoms for dual topology " + label);
347 Set<Integer> indices = new HashSet<>();
348 String[] toks = unshared.split("\\.");
349 Atom[] atoms1 = assembly.getAtomArray();
350 for (String range : toks) {
351 Matcher m = AlchemicalOptions.rangeRegEx.matcher(range);
352 if (m.find()) {
353 int rangeStart = Integer.parseInt(m.group(1));
354 int rangeEnd = (m.group(2) != null) ? Integer.parseInt(m.group(2)) : rangeStart;
355 if (rangeStart > rangeEnd) {
356 logger.severe(format(" Range %s was invalid start was greater than end", range));
357 }
358 logger.info(
359 format(" Range %s for %s, start %d end %d", range, label, rangeStart, rangeEnd));
360 if (logger.isLoggable(Level.FINE)) {
361 logger.fine(format(" First atom in range: %s", atoms1[rangeStart - 1]));
362 if (rangeEnd > rangeStart) {
363 logger.fine(format(" Last atom in range: %s", atoms1[rangeEnd - 1]));
364 }
365 }
366 for (int i = rangeStart; i <= rangeEnd; i++) {
367 indices.add(i - 1);
368 }
369 }
370 }
371 int counter = 0;
372 Set<Integer> adjustedIndices = new HashSet<>();
373 int atomLength = atoms1.length;
374 for (int i = 0; i < atomLength; i++) {
375 Atom ai = atoms1[i];
376 if (indices.contains(i)) {
377 if (ai.applyLambda()) {
378 logger.warning(format(
379 " Ranges defined in %s should not overlap with ligand atoms they are assumed to not be shared.",
380 label));
381 } else {
382 logger.fine(format(" Unshared %s: %d variables %d-%d", label, i, counter, counter + 2));
383 for (int j = 0; j < 3; j++) {
384 adjustedIndices.add(counter + j);
385 }
386 }
387 }
388 if (!ai.applyLambda()) {
389 counter += 3;
390 }
391 }
392 return adjustedIndices.stream().sorted().collect(Collectors.toList());
393 } else {
394 return Collections.emptyList();
395 }
396 }
397
398
399
400
401
402
403
404 public List<Integer> getUniqueAtomsB(MolecularAssembly topology) {
405 return getUniqueAtoms(topology, "B", getUnsharedB());
406 }
407
408
409
410
411
412
413 public boolean hasSoftcore() {
414 String alchemicalAtoms2 = getAlchemicalAtoms2();
415 return (alchemicalAtoms2 != null && !alchemicalAtoms2.equalsIgnoreCase("NONE")
416 && !alchemicalAtoms2.equalsIgnoreCase(""));
417 }
418
419
420
421
422
423
424 public void setSecondSystemAlchemistry(MolecularAssembly topology) {
425 String alchemicalAtoms2 = getAlchemicalAtoms2();
426 String alchemicalResidues2 = getAlchemicalResidues2();
427 AlchemicalOptions.setAlchemicalAtoms(topology, alchemicalAtoms2, alchemicalResidues2);
428 }
429
430
431
432
433
434
435 public void setSecondSystemUnchargedAtoms(MolecularAssembly topology) {
436 String unchargedAtoms2 = getUnchargedAtoms2();
437 AlchemicalOptions.setUnchargedAtoms(topology, unchargedAtoms2);
438 }
439
440
441
442
443
444
445
446 public void setAlchemicalProperties(int numberOfTopologies) {
447 if (numberOfTopologies >= 2) {
448
449
450 System.setProperty("ligand-vapor-elec", "false");
451
452 if (hasSoftcore()) {
453 System.setProperty("lambdaterm", "true");
454 }
455 }
456 }
457
458
459
460
461 private static class TopologyOptionGroup {
462
463
464
465
466 @Option(names = {"--ac2", "--alchemicalAtoms2"}, paramLabel = "<selection>", defaultValue = "",
467 description = "Specify alchemical atoms for the 2nd topology [ALL, NONE, Range(s): 1-3,6-N].")
468 String alchemicalAtoms2 = "";
469
470
471
472
473 @Option(
474 names = {"--acRes2", "--alchemicalResidues2"},
475 paramLabel = "<selection>",
476 defaultValue = "",
477 description = "Specify alchemical residues by chain and residue number for the 2nd topology [A4,B21]")
478 String alchemicalResidues2 = "";
479
480
481
482
483
484 @Option(names = {"--uc2", "--unchargedAtoms2"}, paramLabel = "<selection>", defaultValue = "",
485 description = "Specify atoms without electrostatics for the 2nd topology [ALL, NONE, Range(s): 1-3,6-N].")
486 String unchargedAtoms2 = "";
487
488
489
490
491
492 @Option(names = {"--np", "--nParallel"}, paramLabel = "1",
493 description = "Number of topologies to evaluate in parallel")
494 int nTopologiesInParallel = 1;
495
496
497
498
499
500 @Option(names = {"--uaA", "--unsharedA"}, paramLabel = "-1",
501 description = "Unshared atoms in the A dual topology (e.g. 1-24.32-65).")
502 String unsharedA = null;
503
504
505
506
507
508 @Option(names = {"--uaB", "--unsharedB"}, paramLabel = "-1",
509 description = "Unshared atoms in the B dual topology (e.g. 1-24.32-65).")
510 String unsharedB = null;
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537 @Option(names = {"--sf", "--switchingFunction"}, paramLabel = "1.0",
538 description = "Switching function to use for dual topology: options are TRIG, MULT, or a number (original behavior with specified lambda exponent)")
539 String lambdaFunction = "1.0";
540 }
541 }