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.test;
39
40 import ffx.numerics.Potential;
41 import ffx.potential.ForceFieldEnergy;
42 import ffx.potential.MolecularAssembly;
43 import ffx.potential.bonded.LambdaInterface;
44 import ffx.potential.cli.AlchemicalOptions;
45 import ffx.potential.cli.GradientOptions;
46 import ffx.potential.cli.PotentialCommand;
47 import ffx.potential.cli.TopologyOptions;
48 import ffx.potential.nonbonded.ParticleMeshEwald;
49 import ffx.potential.nonbonded.pme.AlchemicalParameters;
50 import ffx.potential.openmm.OpenMMEnergy;
51 import ffx.utilities.FFXBinding;
52 import picocli.CommandLine.Command;
53 import picocli.CommandLine.Mixin;
54 import picocli.CommandLine.Option;
55 import picocli.CommandLine.Parameters;
56
57 import java.util.ArrayList;
58 import java.util.Arrays;
59 import java.util.Collections;
60 import java.util.List;
61
62 import static ffx.utilities.StringUtils.parseAtomRanges;
63 import static java.lang.String.format;
64 import static org.apache.commons.math3.util.FastMath.abs;
65 import static org.apache.commons.math3.util.FastMath.sqrt;
66
67
68
69
70
71
72
73
74
75 @Command(description = " Test potential energy derivatives with respect to Lambda.", name = "test.LambdaGradient")
76 public class LambdaGradient extends PotentialCommand {
77
78 @Mixin
79 AlchemicalOptions alchemicalOptions;
80
81 @Mixin
82 GradientOptions gradientOptions;
83
84 @Mixin
85 TopologyOptions topologyOptions;
86
87
88
89
90 @Option(names = {"--ls", "--lambdaScan"}, paramLabel = "false",
91 description = "Scan lambda values.")
92 boolean lambdaScan = false;
93
94
95
96
97 @Option(names = {"--lm", "--lambdaMoveSize"}, paramLabel = "0.01",
98 description = "Size of the lambda moves during the test.")
99 double lambdaMoveSize = 0.01;
100
101
102
103
104 @Option(names = {"--sk2", "--skip2"}, paramLabel = "false",
105 description = "Skip 2nd derivatives.")
106 boolean skipSecondDerivatives = false;
107
108
109
110
111
112
113 @Option(names = {"--sdX", "--skipdX"}, paramLabel = "false",
114 description = "Skip calculating per-atom dUdX values and only test lambda gradients.")
115 boolean skipAtomGradients = false;
116
117
118
119
120 @Parameters(arity = "1..*", paramLabel = "files", description = "The atomic coordinate files in PDB or XYZ format.")
121 List<String> filenames = null;
122
123 MolecularAssembly[] topologies;
124 private Potential potential;
125
126 public int ndEdLFailures = 0;
127 public int ndEdXFailures = 0;
128 public int nd2EdL2Failures = 0;
129 public int ndEdXdLFailures = 0;
130 public double e0 = 0.0;
131 public double e1 = 0.0;
132
133
134
135
136 public LambdaGradient() {
137 super();
138 }
139
140
141
142
143
144 public LambdaGradient(FFXBinding binding) {
145 super(binding);
146 }
147
148
149
150
151
152 public LambdaGradient(String[] args) {
153 super(args);
154 }
155
156
157
158
159 @Override
160 public LambdaGradient run() {
161
162 if (!init()) {
163 return this;
164 }
165
166
167 int numTopologies = topologyOptions.getNumberOfTopologies(filenames);
168 int threadsPerTopology = topologyOptions.getThreadsPerTopology(numTopologies);
169 topologies = new MolecularAssembly[numTopologies];
170
171
172 alchemicalOptions.setAlchemicalProperties();
173 topologyOptions.setAlchemicalProperties(numTopologies);
174
175
176 if (filenames == null || filenames.isEmpty()) {
177 activeAssembly = getActiveAssembly(null);
178 if (activeAssembly == null) {
179 logger.info(helpString());
180 return this;
181 }
182 filenames = new ArrayList<>();
183 filenames.add(activeAssembly.getFile().getName());
184 topologies[0] = alchemicalOptions.processFile(topologyOptions, activeAssembly, 0);
185 } else {
186 logger.info(format(" Initializing %d topologies...", numTopologies));
187 for (int i = 0; i < numTopologies; i++) {
188 topologies[i] = alchemicalOptions.openFile(potentialFunctions, topologyOptions,
189 threadsPerTopology, filenames.get(i), i);
190 }
191 }
192
193
194 StringBuilder sb = new StringBuilder("\n Testing lambda derivatives for ");
195 potential = topologyOptions.assemblePotential(topologies, sb);
196 logger.info(sb.toString());
197
198
199 AlchemicalParameters.AlchemicalMode mode = AlchemicalParameters.AlchemicalMode.OST;
200 for (MolecularAssembly assembly : topologies) {
201 ForceFieldEnergy energy = assembly.getPotentialEnergy();
202 ParticleMeshEwald pme = energy.getPmeNode();
203 if (pme == null) {
204 continue;
205 }
206 AlchemicalParameters alchemicalParameters = pme.getAlchemicalParameters();
207 if (alchemicalParameters.mode != AlchemicalParameters.AlchemicalMode.OST) {
208 mode = AlchemicalParameters.AlchemicalMode.SCALE;
209 }
210 }
211
212 boolean skipLambdaDerivatives = false;
213 if (mode == AlchemicalParameters.AlchemicalMode.SCALE) {
214 skipLambdaDerivatives = true;
215 }
216 if (potential instanceof OpenMMEnergy || mode == AlchemicalParameters.AlchemicalMode.SCALE) {
217 skipSecondDerivatives = true;
218 }
219
220 LambdaInterface lambdaInterface = (LambdaInterface) potential;
221
222
223 int n = potential.getNumberOfVariables();
224 double[] x = new double[n];
225 double[] gradient = new double[n];
226 double[] lambdaGrad = new double[n];
227 double[][] lambdaGradFD = new double[2][n];
228
229
230 assert (n % 3 == 0);
231 int nAtoms = n / 3;
232
233
234 double lambda = 0.0;
235 lambdaInterface.setLambda(lambda);
236 potential.getCoordinates(x);
237
238 e0 = potential.energyAndGradient(x, gradient);
239
240 if (!skipLambdaDerivatives) {
241 double dEdL = lambdaInterface.getdEdL();
242 logger.info(format(" L=%4.2f E=%12.6f dE/dL=%12.6f", lambda, e0, dEdL));
243 } else {
244 logger.info(format(" L=%4.2f E=%12.6f", lambda, e0));
245 }
246
247
248 if (lambdaScan) {
249 for (int i = 1; i <= 9; i++) {
250 lambda = i * 0.1;
251 lambdaInterface.setLambda(lambda);
252 double e = potential.energyAndGradient(x, gradient);
253 if (!skipLambdaDerivatives) {
254 double dEdL = lambdaInterface.getdEdL();
255 logger.info(format(" L=%4.2f E=%12.6f dE/dL=%12.6f", lambda, e, dEdL));
256 } else {
257 logger.info(format(" L=%4.2f E=%12.6f", lambda, e));
258 }
259
260 }
261 }
262
263
264 lambda = 1.0;
265 lambdaInterface.setLambda(lambda);
266 e1 = potential.energyAndGradient(x, gradient);
267 if (!skipLambdaDerivatives) {
268 double dEdL = lambdaInterface.getdEdL();
269 logger.info(format(" L=%4.2f E=%12.6f dE/dL=%12.6f", lambda, e1, dEdL));
270 } else {
271 logger.info(format(" L=%4.2f E=%12.6f", lambda, e1));
272 }
273
274 logger.info(format(" E(1)-E(0): %12.6f.\n", e1 - e0));
275
276
277 double step = gradientOptions.getDx();
278 double width = 2.0 * step;
279 logger.info(" Finite-difference step size:\t" + step);
280
281
282 boolean print = gradientOptions.getVerbose();
283 logger.info(" Verbose printing:\t\t" + print + "\n");
284
285
286 double errTol = gradientOptions.getTolerance();
287
288
289 double expGrad = 1000.0;
290
291
292 if (!skipLambdaDerivatives) {
293
294 for (int j = 0; j < 3; j++) {
295
296 int jd2EdXdLFailures = 0;
297
298 lambda = alchemicalOptions.getInitialLambda() - lambdaMoveSize + lambdaMoveSize * j;
299
300 if (lambda - gradientOptions.getDx() < 0.0 || lambda + gradientOptions.getDx() > 1.0) {
301 continue;
302 }
303
304 logger.info(format(" Current lambda value %6.4f", lambda));
305 lambdaInterface.setLambda(lambda);
306
307
308 double e = potential.energyAndGradient(x, gradient);
309
310
311 double dEdL = lambdaInterface.getdEdL();
312
313 double d2EdL2 = lambdaInterface.getd2EdL2();
314 Arrays.fill(lambdaGrad, 0.0);
315 lambdaInterface.getdEdXdL(lambdaGrad);
316
317
318 lambdaInterface.setLambda(lambda + gradientOptions.getDx());
319 double lp = potential.energyAndGradient(x, lambdaGradFD[0]);
320 double dedlp = lambdaInterface.getdEdL();
321 lambdaInterface.setLambda(lambda - gradientOptions.getDx());
322 double lm = potential.energyAndGradient(x, lambdaGradFD[1]);
323 double dedlm = lambdaInterface.getdEdL();
324
325 double dEdLFD = (lp - lm) / width;
326 double d2EdL2FD = (dedlp - dedlm) / width;
327
328 double err = abs(dEdLFD - dEdL);
329 if (err < errTol) {
330 logger.info(format(" dE/dL passed: %10.6f", err));
331 } else {
332 logger.info(format(" dE/dL failed: %10.6f", err));
333 ndEdLFailures++;
334 }
335 logger.info(format(" Numeric: %15.8f", dEdLFD));
336 logger.info(format(" Analytic: %15.8f", dEdL));
337
338 if (!skipSecondDerivatives) {
339 err = abs(d2EdL2FD - d2EdL2);
340 if (err < errTol) {
341 logger.info(format(" d2E/dL2 passed: %10.6f", err));
342 } else {
343 logger.info(format(" d2E/dL2 failed: %10.6f", err));
344 nd2EdL2Failures++;
345 }
346 logger.info(format(" Numeric: %15.8f", d2EdL2FD));
347 logger.info(format(" Analytic: %15.8f", d2EdL2));
348
349 double rmsError = 0;
350 for (int i = 0; i < nAtoms; i++) {
351 int ii = i * 3;
352 double dX = (lambdaGradFD[0][ii] - lambdaGradFD[1][ii]) / width;
353 double dXa = lambdaGrad[ii];
354 double eX = dX - dXa;
355 ii++;
356 double dY = (lambdaGradFD[0][ii] - lambdaGradFD[1][ii]) / width;
357 double dYa = lambdaGrad[ii];
358 double eY = dY - dYa;
359 ii++;
360 double dZ = (lambdaGradFD[0][ii] - lambdaGradFD[1][ii]) / width;
361 double dZa = lambdaGrad[ii];
362 double eZ = dZ - dZa;
363
364 double error = eX * eX + eY * eY + eZ * eZ;
365 rmsError += error;
366 error = sqrt(error);
367 if (error < errTol) {
368 logger.fine(format(" dE/dX/dL for degree of freedom %d passed: %10.6f", i + 1, error));
369 } else {
370 logger.info(format(" dE/dX/dL for degree of freedom %d failed: %10.6f", i + 1, error));
371 logger.info(format(" Analytic: (%15.8f, %15.8f, %15.8f)", dXa, dYa, dZa));
372 logger.info(format(" Numeric: (%15.8f, %15.8f, %15.8f)", dX, dY, dZ));
373 ndEdXdLFailures++;
374 jd2EdXdLFailures++;
375 }
376 }
377 rmsError = sqrt(rmsError / nAtoms);
378 if (ndEdXdLFailures == 0) {
379 logger.info(
380 format(" dE/dX/dL passed for all degrees of freedom: RMS error %15.8f", rmsError));
381 } else {
382 logger.info(
383 format(" dE/dX/dL failed for %d of %d atoms: RMS error %15.8f", jd2EdXdLFailures,
384 nAtoms, rmsError));
385 }
386 logger.info("");
387 }
388 }
389 }
390
391 lambdaInterface.setLambda(alchemicalOptions.getInitialLambda());
392 potential.getCoordinates(x);
393 potential.energyAndGradient(x, gradient, print);
394
395 if (!skipAtomGradients) {
396 double[] numeric = new double[3];
397 double avLen = 0.0;
398 double avGrad = 0.0;
399
400
401 List<Integer> degreesOfFreedomToTest;
402 if (gradientOptions.getGradientAtoms().equalsIgnoreCase("NONE")) {
403 logger.info(" The gradient of no atoms will be evaluated.");
404 return this;
405 } else if (gradientOptions.getGradientAtoms().equalsIgnoreCase("ALL")) {
406 logger.info(" Checking gradient for all active atoms.\n");
407 degreesOfFreedomToTest = new ArrayList<>();
408 for (int i = 0; i < nAtoms; i++) {
409 degreesOfFreedomToTest.add(i);
410 }
411 } else {
412 degreesOfFreedomToTest =
413 parseAtomRanges(" Gradient atoms", gradientOptions.getGradientAtoms(), nAtoms);
414 logger.info(
415 " Checking gradient for active atoms in the range: " + gradientOptions.getGradientAtoms()
416 +
417 "\n");
418 }
419
420 for (int i : degreesOfFreedomToTest) {
421 int i3 = i * 3;
422 int i0 = i3 + 0;
423 int i1 = i3 + 1;
424 int i2 = i3 + 2;
425
426
427 double orig = x[i0];
428 x[i0] = x[i0] + step;
429 double e = potential.energyAndGradient(x, lambdaGradFD[0], print);
430 x[i0] = orig - step;
431 e -= potential.energyAndGradient(x, lambdaGradFD[1], print);
432 x[i0] = orig;
433 numeric[0] = e / width;
434
435
436 orig = x[i1];
437 x[i1] = x[i1] + step;
438 e = potential.energyAndGradient(x, lambdaGradFD[0], print);
439 x[i1] = orig - step;
440 e -= potential.energyAndGradient(x, lambdaGradFD[1], print);
441 x[i1] = orig;
442 numeric[1] = e / width;
443
444
445 orig = x[i2];
446 x[i2] = x[i2] + step;
447 e = potential.energyAndGradient(x, lambdaGradFD[0], print);
448 x[i2] = orig - step;
449 e -= potential.energyAndGradient(x, lambdaGradFD[1], print);
450 x[i2] = orig;
451 numeric[2] = e / width;
452
453 double dx = gradient[i0] - numeric[0];
454 double dy = gradient[i1] - numeric[1];
455 double dz = gradient[i2] - numeric[2];
456 double len = dx * dx + dy * dy + dz * dz;
457 avLen += len;
458 len = Math.sqrt(len);
459
460 double grad2 =
461 gradient[i0] * gradient[i0] + gradient[i1] * gradient[i1] + gradient[i2] * gradient[i2];
462 avGrad += grad2;
463
464 if (len > errTol) {
465 logger.info(format(" Degree of freedom %d failed: %10.6f.", i + 1, len)
466 + format("\n Analytic: (%12.4f, %12.4f, %12.4f)\n", gradient[i0], gradient[i1],
467 gradient[i2])
468 + format(" Numeric: (%12.4f, %12.4f, %12.4f)\n", numeric[0], numeric[1], numeric[2]));
469 ++ndEdXFailures;
470 } else {
471 logger.info(format(" Degree of freedom %d passed: %10.6f.", i + 1, len)
472 + format("\n Analytic: (%12.4f, %12.4f, %12.4f)\n", gradient[i0], gradient[i1],
473 gradient[i2])
474 + format(" Numeric: (%12.4f, %12.4f, %12.4f)", numeric[0], numeric[1], numeric[2]));
475 }
476
477 if (grad2 > expGrad) {
478 logger.info(
479 format(" Degree of freedom %d has an unusually large gradient: %10.6f", i + 1, grad2));
480 }
481 logger.info("\n");
482 }
483
484 avLen = avLen / nAtoms;
485 avLen = sqrt(avLen);
486 if (avLen > errTol) {
487 logger.info(
488 format(" Test failure: RMSD from analytic solution is %10.6f > %10.6f", avLen, errTol));
489 } else {
490 logger.info(
491 format(" Test success: RMSD from analytic solution is %10.6f < %10.6f", avLen, errTol));
492 }
493 logger.info(format(" Number of atoms failing gradient test: %d", ndEdXFailures));
494
495 avGrad = avGrad / nAtoms;
496 avGrad = sqrt(avGrad);
497 if (avGrad > expGrad) {
498 logger.info(format(" Unusually large RMS gradient: %10.6f > %10.6f", avGrad, expGrad));
499 } else {
500 logger.info(format(" RMS gradient: %10.6f", avGrad));
501 }
502 } else {
503 logger.info(" Skipping atomic dU/dX gradients.");
504 }
505
506 return this;
507 }
508
509 @Override
510 public List<Potential> getPotentials() {
511 if (potential == null) {
512 return Collections.emptyList();
513 } else {
514 return Collections.singletonList(potential);
515 }
516 }
517 }