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.algorithms.commands.test;
39
40 import ffx.algorithms.cli.AlgorithmsCommand;
41 import ffx.numerics.Potential;
42 import ffx.potential.bonded.Atom;
43 import ffx.potential.bonded.Molecule;
44 import ffx.utilities.FFXBinding;
45 import picocli.CommandLine.Command;
46 import picocli.CommandLine.Option;
47 import picocli.CommandLine.Parameters;
48
49 import java.util.ArrayList;
50 import java.util.Arrays;
51 import java.util.List;
52
53 import java.util.*;
54 import static java.lang.Math.*;
55 import org.apache.commons.math3.linear.Array2DRowRealMatrix;
56 import org.apache.commons.math3.linear.EigenDecomposition;
57 import static java.lang.String.format;
58
59
60
61
62
63
64
65
66
67 @Command(description = "Find guest atoms for COM or Boresch restraints.",
68 name = "test.FindRestraints")
69 public class FindRestraints extends AlgorithmsCommand {
70
71
72
73
74 @Option(names = {"--hostName"}, paramLabel = "BCD", defaultValue = "BCD",
75 description = "Host molecule name in the file.")
76 private String hostName;
77
78
79
80
81 @Option(names = {"--guestName"}, paramLabel = "LIG", defaultValue = "LIG",
82 description = "Ligand molecule name in the file.")
83 private String guestName;
84
85
86
87
88 @Option(names = {"--distanceCutoff"}, paramLabel = "5", defaultValue = "5",
89 description = "Cutoff to use when selecting guest atoms near host COM")
90 private double distanceCutoff;
91
92
93
94
95
96 @Option(names = {"--boresch"}, defaultValue = "false",
97 description = "Use Boresch anchor selection instead of COM-based selection.")
98 private boolean boresch;
99
100
101 @Option(names = {"--H1"}, paramLabel = "int",
102 description = "Host anchor H1 atom index.")
103 private Integer H1Index;
104
105 @Option(names = {"--H2"}, paramLabel = "int",
106 description = "Host anchor H2 atom index.")
107 private Integer H2Index;
108
109 @Option(names = {"--H3"}, paramLabel = "int",
110 description = "Host anchor H3 atom index.")
111 private Integer H3Index;
112
113
114 @Option(names = {"--l1Range"}, defaultValue = "3.5",
115 description = "Cylinder diameter/height for G1 search.")
116 private double l1Range;
117
118 @Option(names = {"--minAdis"}, defaultValue = "1.5",
119 description = "Minimum distance between guest anchors.")
120 private double minAdis;
121
122 @Option(names = {"--maxAdis"}, defaultValue = "5.0",
123 description = "Maximum distance between guest anchors.")
124 private double maxAdis;
125
126
127
128
129 @Parameters(arity = "1..*", paramLabel = "files",
130 description = "XYZ or PDB input files.")
131 private List<String> filenames;
132
133
134
135
136
137 public Potential potential;
138
139
140
141
142
143 public Potential getPotentialObject() {
144 return potential;
145 }
146
147
148
149
150 public FindRestraints() {
151 super();
152 }
153
154
155
156
157
158 public FindRestraints(FFXBinding binding) {
159 super(binding);
160 }
161
162
163
164
165
166 public FindRestraints(String[] args) {
167 super(args);
168 }
169
170
171
172
173 @Override
174 public FindRestraints run() {
175 if (!init()) {
176 return this;
177 }
178
179 activeAssembly = getActiveAssembly(filenames.get(0));
180 if (activeAssembly == null) {
181 logger.info(helpString());
182 return this;
183 }
184 Molecule host = null;
185 Molecule guest = null;
186
187 for (Molecule mol : activeAssembly.getMoleculeArray()) {
188 if (mol.getName().contains(hostName)) host = mol;
189 if (mol.getName().contains(guestName)) guest = mol;
190 }
191
192 if (host == null || guest == null) {
193 logger.severe("Host or Guest molecule not found.");
194 return this;
195 }
196
197 if (boresch) {
198 runBoreschMode(host, guest);
199 } else {
200 runCOMMode();
201 }
202
203 return this;
204 }
205
206
207
208
209 private void runCOMMode() {
210 Molecule[] molArr = activeAssembly.getMoleculeArray();
211
212 List<Atom> restrainHostList = new ArrayList<>();
213 List<Atom> restrainList = new ArrayList<>();
214 double[] COM = new double[3];
215 double[] subCOM = new double[3];
216
217 int[] restrainHostIndices = new int[]{11, 16, 17, 20, 23, 26, 31, 32, 39, 40, 51, 63, 64, 70, 71, 82, 94, 95, 101, 102, 113, 125, 126, 132, 133, 144, 156, 157, 163, 164, 175, 187, 188, 191, 198};
218 for (Molecule molecule : molArr) {
219 logger.info(format(" Molecule name: " + molecule.getName()));
220 if (molecule.getName().contains(hostName)) {
221 Atom[] host_atoms = molecule.getAtomList().toArray(new Atom[0]);
222 COM = getCOM(host_atoms);
223 logger.info(format(" Center of mass of host molecule: " + Arrays.toString(COM)));
224 for (Atom atom : host_atoms) {
225 if (contains(restrainHostIndices, atom.getIndex())) {
226 restrainHostList.add(atom);
227 logger.info(format(" Atom: " + atom));
228 }
229 }
230 Atom[] subAtoms = restrainHostList.toArray(new Atom[0]);
231 subCOM = getCOM(subAtoms);
232 logger.info(format(" Center of mass of subsection host atoms: " + Arrays.toString(subCOM)));
233 double comdist = Math.sqrt(Math.pow(subCOM[0] - COM[0], 2) +
234 Math.pow(subCOM[1] - COM[1], 2) +
235 Math.pow(subCOM[2] - COM[2], 2));
236 logger.info(format(" Distance between COMs: " + comdist));
237 } else if (molecule.getName().contains(guestName)) {
238 Atom[] guest_atoms = molecule.getAtomList().toArray(new Atom[0]);
239 for (Atom atom : guest_atoms) {
240 double dist = Math.sqrt(Math.pow(atom.getXYZ().get()[0] - subCOM[0], 2) +
241 Math.pow(atom.getXYZ().get()[1] - subCOM[1], 2) +
242 Math.pow(atom.getXYZ().get()[2] - subCOM[2], 2));
243
244
245
246
247 if (dist < distanceCutoff && atom.isHeavy()) {
248 restrainList.add(atom);
249 }
250 }
251 }
252 }
253 logger.info(format(" Number of atoms to restrain: " + restrainList.size()));
254 int[] restrainIndices = restrainList.stream()
255 .mapToInt(Atom::getIndex)
256 .toArray();
257 logger.info(format(" Restrain list indices: " + Arrays.toString(restrainIndices)));
258
259 }
260
261
262
263
264
265
266
267
268
269
270 private double[][] getAlignmentRotationMatrix(Atom[] hostAtoms, double[] com) {
271
272 double[][] I = new double[3][3];
273
274 for (Atom atom : hostAtoms) {
275 double m = atom.getMass();
276 double[] r = atom.getXYZ().get();
277
278 double rx = r[0] - com[0];
279 double ry = r[1] - com[1];
280 double rz = r[2] - com[2];
281
282 I[0][0] += m * (ry * ry + rz * rz);
283 I[1][1] += m * (rx * rx + rz * rz);
284 I[2][2] += m * (rx * rx + ry * ry);
285
286 I[0][1] -= m * rx * ry;
287 I[0][2] -= m * rx * rz;
288 I[1][2] -= m * ry * rz;
289 }
290
291 I[1][0] = I[0][1];
292 I[2][0] = I[0][2];
293 I[2][1] = I[1][2];
294
295
296 EigenDecomposition ed = new EigenDecomposition(new Array2DRowRealMatrix(I));
297 double[] evals = ed.getRealEigenvalues();
298 double[][] evecs = ed.getV().getData();
299
300 int zIndex = 0;
301 int xIndex = 0;
302 for (int i = 1; i < 3; i++) {
303 if (evals[i] > evals[zIndex]) {
304 zIndex = i;
305 }
306 if (evals[i] < evals[xIndex]) {
307 xIndex = i;
308 }
309 }
310 int yIndex = 3 - zIndex - xIndex;
311
312 double[] zHat = normalize(new double[]{evecs[0][zIndex], evecs[1][zIndex], evecs[2][zIndex]});
313 double[] xHat = normalize(new double[]{evecs[0][xIndex], evecs[1][xIndex], evecs[2][xIndex]});
314 double[] yHat = normalize(cross(zHat, xHat));
315 xHat = normalize(cross(yHat, zHat));
316
317 return new double[][]{xHat, yHat, zHat};
318 }
319
320
321
322
323 private void applyTransformation(Atom[] atoms, double[] com, double[][] rotationMatrix) {
324 for (Atom atom : atoms) {
325 double[] r = atom.getXYZ().get();
326
327
328 double tx = r[0] - com[0];
329 double ty = r[1] - com[1];
330 double tz = r[2] - com[2];
331
332
333 double[] rotated = new double[3];
334 for (int i = 0; i < 3; i++) {
335 rotated[i] = rotationMatrix[i][0] * tx
336 + rotationMatrix[i][1] * ty
337 + rotationMatrix[i][2] * tz;
338 }
339
340 atom.setXYZ(rotated);
341 }
342 }
343
344 private static double[][] captureCoordinates(Atom[] atoms) {
345 double[][] coords = new double[atoms.length][3];
346 for (int i = 0; i < atoms.length; i++) {
347 double[] r = atoms[i].getXYZ().get();
348 coords[i][0] = r[0];
349 coords[i][1] = r[1];
350 coords[i][2] = r[2];
351 }
352 return coords;
353 }
354
355 private static void restoreCoordinates(Atom[] atoms, double[][] coords) {
356 for (int i = 0; i < atoms.length; i++) {
357 atoms[i].setXYZ(coords[i]);
358 }
359 }
360
361 private void runBoreschMode(Molecule host, Molecule guest) {
362
363 if (H1Index == null || H2Index == null || H3Index == null) {
364 logger.severe("Boresch mode requires --H1 --H2 --H3 indices.");
365 return;
366 }
367
368 Atom H1 = findAtomByIndex(host, H1Index);
369 Atom H2 = findAtomByIndex(host, H2Index);
370 Atom H3 = findAtomByIndex(host, H3Index);
371
372 if (H1 == null || H2 == null || H3 == null) {
373 logger.severe("Could not locate host anchor atoms.");
374 return;
375 }
376
377
378 Atom[] hostAtoms = host.getAtomList().toArray(new Atom[0]);
379 Atom[] guestAtoms = guest.getAtomList().toArray(new Atom[0]);
380
381
382 double[] com = getCOMNonHydrogen(hostAtoms);
383 logger.info(format("Host COM (non-H): (%.3f, %.3f, %.3f)", com[0], com[1], com[2]));
384
385 double[][] rotationMatrix = getAlignmentRotationMatrix(hostAtoms, com);
386
387
388 double[][] hostCoords = captureCoordinates(hostAtoms);
389 double[][] guestCoords = captureCoordinates(guestAtoms);
390 applyTransformation(hostAtoms, com, rotationMatrix);
391 applyTransformation(guestAtoms, com, rotationMatrix);
392
393 Atom G1 = selectG1(guest);
394 if (G1 == null) return;
395
396 Atom G2 = selectG2(guest, G1);
397 if (G2 == null) return;
398
399 Atom G3 = selectG3(guest, G1, G2);
400 if (G3 == null) return;
401
402
403 restoreCoordinates(hostAtoms, hostCoords);
404 restoreCoordinates(guestAtoms, guestCoords);
405
406
407 logger.info(format("\n=== Selected Boresch Anchors ==="));
408 logger.info(format("Host anchors:"));
409 logger.info(format(" H1: %s (index %d, residue %d)", H1.getName(), H1.getIndex(), H1.getResidueNumber()));
410 logger.info(format(" H2: %s (index %d, residue %d)", H2.getName(), H2.getIndex(), H2.getResidueNumber()));
411 logger.info(format(" H3: %s (index %d, residue %d)", H3.getName(), H3.getIndex(), H3.getResidueNumber()));
412 logger.info(format("Guest anchors:"));
413 logger.info(format(" G1: %s (index %d, residue %d)", G1.getName(), G1.getIndex(), G1.getResidueNumber()));
414 logger.info(format(" G2: %s (index %d, residue %d)", G2.getName(), G2.getIndex(), G2.getResidueNumber()));
415 logger.info(format(" G3: %s (index %d, residue %d)", G3.getName(), G3.getIndex(), G3.getResidueNumber()));
416
417
418
419
420 double r = distance(H1.getXYZ().get(), G1.getXYZ().get());
421 double thetaA = angle(H2, H1, G1);
422 double thetaB = angle(H1, G1, G2);
423 double phiA = dihedral(H3, H2, H1, G1);
424 double phiB = dihedral(H2, H1, G1, G2);
425 double phiC = dihedral(H1, G1, G2, G3);
426
427 logger.info(format("\n=== Boresch Restraint Parameters ==="));
428 logger.info(format("Distance H1-G1 (r): %.3f Å", r));
429 logger.info(format("Angle H2-H1-G1 (θA): %.2f°", thetaA));
430 logger.info(format("Angle H1-G1-G2 (θB): %.2f°", thetaB));
431 logger.info(format("Dihedral H3-H2-H1-G1 (φA): %.2f°", phiA));
432 logger.info(format("Dihedral H2-H1-G1-G2 (φB): %.2f°", phiB));
433 logger.info(format("Dihedral H1-G1-G2-G3 (φC): %.2f°", phiC));
434
435
436 if (thetaA < 20.0 || thetaA > 160.0) {
437 logger.warning(format("Angle θA = %.2f° is near-degenerate.", thetaA));
438 }
439 if (thetaB < 20.0 || thetaB > 160.0) {
440 logger.warning(format("Angle θB = %.2f° is near-degenerate.", thetaB));
441 }
442 }
443
444
445
446
447
448
449 private static double dihedral(Atom a, Atom b, Atom c, Atom d) {
450 double[] v1 = subtract(a.getXYZ().get(), b.getXYZ().get());
451 double[] v2 = subtract(c.getXYZ().get(), b.getXYZ().get());
452 double[] v3 = subtract(d.getXYZ().get(), c.getXYZ().get());
453
454 double[] n1 = cross(v1, v2);
455 double[] n2 = cross(v2, v3);
456
457 double[] m1 = cross(n1, v2);
458
459 double x = dot(n1, n2);
460 double y = dot(m1, n2);
461
462 return toDegrees(atan2(y, x));
463 }
464
465 private static double[] cross(double[] a, double[] b) {
466 return new double[]{
467 a[1]*b[2] - a[2]*b[1],
468 a[2]*b[0] - a[0]*b[2],
469 a[0]*b[1] - a[1]*b[0]
470 };
471 }
472
473 private static double dot(double[] a, double[] b) {
474 return a[0]*b[0] + a[1]*b[1] + a[2]*b[2];
475 }
476
477
478
479
480
481
482 private Atom selectG1(Molecule guest) {
483 double radius = l1Range / 2.0;
484 double radius2 = radius * radius;
485 double halfHeight = l1Range / 2.0;
486
487
488 Atom G1 = null;
489 double bestR2 = Double.MAX_VALUE;
490
491
492 for (Atom atom : guest.getAtomList()) {
493 if (!atom.isHeavy()) continue;
494
495 double[] xyz = atom.getXYZ().get();
496 double r2 = xyz[0]*xyz[0] + xyz[1]*xyz[1];
497
498 if (Math.abs(xyz[2]) <= halfHeight && r2 <= radius2) {
499 if (r2 < bestR2) {
500 bestR2 = r2;
501 G1 = atom;
502 }
503 }
504 }
505
506 if (G1 == null) {
507 logger.severe("anch1 error: No valid G1 anchor found.");
508 return null;
509 }
510
511 return G1;
512 }
513
514 private Atom selectG2(Molecule guest, Atom G1) {
515
516 Atom best = null;
517 double bestProj = Double.MAX_VALUE;
518
519 double[] g1 = G1.getXYZ().get();
520
521 for (Atom atom : guest.getAtomList()) {
522 if (atom == G1 || !atom.isHeavy()) continue;
523
524 double d = distance(g1, atom.getXYZ().get());
525 if (d < minAdis || d > maxAdis) continue;
526
527 double[] diff = subtract(atom.getXYZ().get(), g1);
528 double proj2 = diff[0] * diff[0] + diff[1] * diff[1];
529
530 if (proj2 < bestProj) {
531 best = atom;
532 bestProj = proj2;
533 }
534 }
535 if (best == null) {
536 logger.severe("anch2 error: Could not identify valid G2 anchor.");
537 }
538
539 return best;
540 }
541
542 private Atom selectG3(Molecule guest, Atom G1, Atom G2) {
543 Atom best = null;
544 double bestAngleDiff = Double.MAX_VALUE;
545
546 List<Atom> candidates = new ArrayList<>();
547
548 for (Atom atom : guest.getAtomList()) {
549 if (atom == G1 || atom == G2 || !atom.isHeavy()) continue;
550
551
552 double d = distance(G2.getXYZ().get(), atom.getXYZ().get());
553 if (d < minAdis || d > maxAdis) continue;
554
555
556 double d_g1 = distance(G1.getXYZ().get(), atom.getXYZ().get());
557 if (d_g1 < minAdis) continue;
558
559 candidates.add(atom);
560 }
561
562 if (candidates.isEmpty()) {
563 logger.warning("No valid G3 candidates found");
564 return null;
565 }
566
567
568 for (Atom atom : candidates) {
569 double ang = angle(G1, G2, atom);
570
571
572 if (ang < 20.0 || ang > 160.0) continue;
573
574 double diff = Math.abs(ang - 90.0);
575
576 if (diff < bestAngleDiff) {
577 bestAngleDiff = diff;
578 best = atom;
579 }
580 }
581
582 if (best == null) {
583 logger.warning("No G3 candidate with acceptable angle found");
584 } else {
585 logger.info(String.format("Selected G3: %s (index %d)",
586 best.getName(), best.getIndex()));
587 }
588
589 return best;
590 }
591
592
593
594
595
596 private static double distance(double[] a, double[] b) {
597 return sqrt(pow(a[0] - b[0], 2)
598 + pow(a[1] - b[1], 2)
599 + pow(a[2] - b[2], 2));
600 }
601
602 private static double[] subtract(double[] a, double[] b) {
603 return new double[]{a[0] - b[0], a[1] - b[1], a[2] - b[2]};
604 }
605
606 private static double[] normalize(double[] v) {
607 double norm = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
608 if (norm == 0.0) {
609 return new double[]{0.0, 0.0, 0.0};
610 }
611 return new double[]{v[0] / norm, v[1] / norm, v[2] / norm};
612 }
613
614 private static double angle(Atom a, Atom b, Atom c) {
615 double[] v1 = subtract(a.getXYZ().get(), b.getXYZ().get());
616 double[] v2 = subtract(c.getXYZ().get(), b.getXYZ().get());
617
618 double dot = v1[0]*v2[0] + v1[1]*v2[1] + v1[2]*v2[2];
619 double mag1 = sqrt(v1[0]*v1[0] + v1[1]*v1[1] + v1[2]*v1[2]);
620 double mag2 = sqrt(v2[0]*v2[0] + v2[1]*v2[1] + v2[2]*v2[2]);
621
622 return toDegrees(acos(dot / (mag1 * mag2)));
623 }
624
625 private static Atom findAtomByIndex(Molecule mol, int index) {
626 for (Atom atom : mol.getAtomList()) {
627 if (atom.getIndex() == index) return atom;
628 }
629 return null;
630 }
631
632
633
634
635
636
637
638 private static double[] getCOM(Atom[] atoms) {
639
640 double[] COM = new double[3];
641 double totalMass = 0.0;
642 for (Atom s : atoms) {
643 double[] pos = s.getXYZ().get();
644 COM[0] += pos[0] * s.getMass();
645 COM[1] += pos[1] * s.getMass();
646 COM[2] += pos[2] * s.getMass();
647 totalMass += s.getMass();
648 }
649 totalMass = 1 / totalMass;
650 COM[0] *= totalMass;
651 COM[1] *= totalMass;
652 COM[2] *= totalMass;
653
654 return COM;
655 }
656
657
658
659
660
661
662
663 private static double[] getCOMNonHydrogen(Atom[] atoms) {
664 double[] COM = new double[3];
665 double totalMass = 0.0;
666 for (Atom s : atoms) {
667 if (!s.isHeavy()) continue;
668 double[] pos = s.getXYZ().get();
669 COM[0] += pos[0] * s.getMass();
670 COM[1] += pos[1] * s.getMass();
671 COM[2] += pos[2] * s.getMass();
672 totalMass += s.getMass();
673 }
674 if (totalMass == 0.0) {
675 return new double[]{0.0, 0.0, 0.0};
676 }
677 totalMass = 1 / totalMass;
678 COM[0] *= totalMass;
679 COM[1] *= totalMass;
680 COM[2] *= totalMass;
681
682 return COM;
683 }
684
685
686
687
688
689
690
691
692 private static boolean contains(int[] array, int value) {
693 return Arrays.stream(array).anyMatch(element -> element == value);
694 }
695 }