diff --git a/CHANGELOG.md b/CHANGELOG.md index 4341de8635..2866007019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +- 2026-08-01: Added `dnascan` module for mutagenesis scanning of DNA base pairs - Issue #1634 - 2026-09-02: Fixed identical seed per sampling_factor replica in flexref, mdref and emref - Issue #1685 - 2026-08-05: Harmonised running mode of test config files - Issue #1655 - 2026-08-10: Added missing improper for HYP - Issue #1662 diff --git a/docs/titles.yaml b/docs/titles.yaml index 061e01b0dc..9b8ee4233f 100644 --- a/docs/titles.yaml +++ b/docs/titles.yaml @@ -29,6 +29,7 @@ modules: seletop: "Selection of top models module" seletopclusts: "Selection of top clusters module" alascan: "Alanine Scanning module" + dnascan: "DNA Scanning module" rnascan: "RNA Scanning module" ilrmsdmatrix: "Interface Ligand RMSD Matrix calculation module" exit: "Exit module" diff --git a/examples/analysis/data/protdna_complex_1.pdb b/examples/analysis/data/protdna_complex_1.pdb new file mode 100644 index 0000000000..6402789f03 --- /dev/null +++ b/examples/analysis/data/protdna_complex_1.pdb @@ -0,0 +1,1383 @@ +REMARK FILENAME="emref_1.pdb" +REMARK =============================================================== +REMARK HADDOCK run for /Users/rodrigo/repos/haddock3/examples/docking-protein-DNA/run1/06_emref/emref_1.pdb +REMARK initial structure: /Users/rodrigo/repos/haddock3/examples/docking-protein-DNA/run1/04_flexref/flexref_1.pdb +REMARK =============================================================== +REMARK total,bonds,angles,improper,dihe,vdw,elec,air,cdih,coup,rdcs,vean,dani,xpcs,rg +REMARK energies: -28.1618, 0, 0, 0, 0, -20.2361, -201.17, 181.515, 0, 0, 0, 0, 0, 0, 0 +REMARK =============================================================== +REMARK bonds,angles,impropers,dihe,air,cdih,coup,rdcs,vean,dani,xpcs +REMARK rms-dev.: 0,0,0,0,0.553508,0.305435,0, 0, 0, 0, 0 +REMARK =============================================================== +REMARK air,cdih,coup,rdcs,vean,dani,xpcs +REMARK >0.3,>5,>1,>0,>5,>0.2,>0.2 +REMARK violations.: 8, 0, 0, 0, 0, 0, 0 +REMARK =============================================================== +REMARK CVpartition#,violations,rms +REMARK AIRs cross-validation: 0, 0, 0 +REMARK =============================================================== +REMARK NCS energy: 0 +REMARK =============================================================== +REMARK Symmetry energy: 0 +REMARK =============================================================== +REMARK Membrane restraining energy: 0 +REMARK =============================================================== +REMARK Local cross-correlation: 0.0000 +REMARK =============================================================== +REMARK Desolvation energy: 16.1364 +REMARK Internal energy free molecules: 881.902 +REMARK Internal energy complex: 958.931 +REMARK Binding energy: -128.241 +REMARK =============================================================== +REMARK buried surface area: 1205.18 +REMARK =============================================================== +REMARK water - chain_1: 0 0 0 +REMARK water - chain_2: 0 0 0 +REMARK =============================================================== +REMARK water - water: 0 0 0 +REMARK =============================================================== +REMARK DATE:27-Dec-2021 13:23:29 created by user: unknown +REMARK VERSION:1.3U +ATOM 1 N MET A -1 15.935 1.055 -11.263 1.00 15.00 A N +ATOM 2 HN MET A -1 16.630 1.339 -11.898 1.00 15.00 A H +ATOM 3 CA MET A -1 14.551 0.974 -11.719 1.00 15.00 A C +ATOM 4 CB MET A -1 13.753 0.015 -10.838 1.00 15.00 A C +ATOM 5 CG MET A -1 14.037 -1.451 -11.131 1.00 15.00 A C +ATOM 6 SD MET A -1 13.483 -2.548 -9.814 1.00 15.00 A S +ATOM 7 CE MET A -1 13.769 -4.149 -10.562 1.00 15.00 A C +ATOM 8 C MET A -1 13.910 2.358 -11.761 1.00 15.00 A C +ATOM 9 O MET A -1 13.021 2.617 -12.572 1.00 15.00 A O +ATOM 10 N GLN A 0 14.372 3.232 -10.864 1.00 15.00 A N +ATOM 11 HN GLN A 0 15.058 2.931 -10.238 1.00 15.00 A H +ATOM 12 CA GLN A 0 13.907 4.623 -10.773 1.00 15.00 A C +ATOM 13 CB GLN A 0 14.175 5.397 -12.071 1.00 15.00 A C +ATOM 14 CG GLN A 0 15.587 5.967 -12.194 1.00 15.00 A C +ATOM 15 CD GLN A 0 16.681 4.968 -11.848 1.00 15.00 A C +ATOM 16 OE1 GLN A 0 17.071 4.144 -12.673 1.00 15.00 A O +ATOM 17 NE2 GLN A 0 17.198 5.048 -10.628 1.00 15.00 A N +ATOM 18 HE21 GLN A 0 16.855 5.737 -10.022 1.00 15.00 A H +ATOM 19 HE22 GLN A 0 17.908 4.418 -10.388 1.00 15.00 A H +ATOM 20 C GLN A 0 12.457 4.792 -10.304 1.00 15.00 A C +ATOM 21 O GLN A 0 12.200 5.528 -9.352 1.00 15.00 A O +ATOM 22 N THR A 1 11.518 4.117 -10.964 1.00 15.00 A N +ATOM 23 HN THR A 1 11.788 3.522 -11.702 1.00 15.00 A H +ATOM 24 CA THR A 1 10.101 4.227 -10.612 1.00 15.00 A C +ATOM 25 CB THR A 1 9.174 3.565 -11.655 1.00 15.00 A C +ATOM 26 OG1 THR A 1 7.807 3.724 -11.266 1.00 15.00 A O +ATOM 27 HG1 THR A 1 7.370 2.866 -11.279 1.00 15.00 A H +ATOM 28 CG2 THR A 1 9.488 2.088 -11.815 1.00 15.00 A C +ATOM 29 C THR A 1 9.801 3.722 -9.192 1.00 15.00 A C +ATOM 30 O THR A 1 10.418 2.766 -8.718 1.00 15.00 A O +ATOM 31 N LEU A 2 8.836 4.382 -8.538 1.00 15.00 A N +ATOM 32 HN LEU A 2 8.361 5.099 -9.010 1.00 15.00 A H +ATOM 33 CA LEU A 2 8.441 4.072 -7.157 1.00 15.00 A C +ATOM 34 CB LEU A 2 7.205 4.888 -6.756 1.00 15.00 A C +ATOM 35 CG LEU A 2 7.355 6.410 -6.743 1.00 15.00 A C +ATOM 36 CD1 LEU A 2 6.872 7.016 -8.052 1.00 15.00 A C +ATOM 37 CD2 LEU A 2 6.615 7.012 -5.558 1.00 15.00 A C +ATOM 38 C LEU A 2 8.145 2.595 -6.934 1.00 15.00 A C +ATOM 39 O LEU A 2 8.721 1.962 -6.046 1.00 15.00 A O +ATOM 40 N SER A 3 7.239 2.065 -7.746 1.00 15.00 A N +ATOM 41 HN SER A 3 6.841 2.626 -8.443 1.00 15.00 A H +ATOM 42 CA SER A 3 6.820 0.675 -7.651 1.00 15.00 A C +ATOM 43 CB SER A 3 5.824 0.374 -8.771 1.00 15.00 A C +ATOM 44 OG SER A 3 6.251 0.956 -9.993 1.00 15.00 A O +ATOM 45 HG SER A 3 5.861 0.471 -10.726 1.00 15.00 A H +ATOM 46 C SER A 3 8.001 -0.287 -7.718 1.00 15.00 A C +ATOM 47 O SER A 3 8.264 -1.030 -6.771 1.00 15.00 A O +ATOM 48 N GLU A 4 8.721 -0.247 -8.832 1.00 15.00 A N +ATOM 49 HN GLU A 4 8.469 0.390 -9.534 1.00 15.00 A H +ATOM 50 CA GLU A 4 9.867 -1.120 -9.039 1.00 15.00 A C +ATOM 51 CB GLU A 4 10.461 -0.914 -10.433 1.00 15.00 A C +ATOM 52 CG GLU A 4 9.675 -1.583 -11.551 1.00 15.00 A C +ATOM 53 CD GLU A 4 9.884 -3.083 -11.598 1.00 15.00 A C +ATOM 54 OE1 GLU A 4 9.044 -3.818 -11.044 1.00 15.00 A O +ATOM 55 OE2 GLU A 4 10.887 -3.531 -12.191 1.00 15.00 A O +ATOM 56 C GLU A 4 10.930 -0.952 -7.950 1.00 15.00 A C +ATOM 57 O GLU A 4 11.453 -1.942 -7.436 1.00 15.00 A O +ATOM 58 N ARG A 5 11.227 0.296 -7.584 1.00 15.00 A N +ATOM 59 HN ARG A 5 10.762 1.050 -8.019 1.00 15.00 A H +ATOM 60 CA ARG A 5 12.228 0.576 -6.552 1.00 15.00 A C +ATOM 61 CB ARG A 5 12.389 2.086 -6.354 1.00 15.00 A C +ATOM 62 CG ARG A 5 13.797 2.527 -5.986 1.00 15.00 A C +ATOM 63 CD ARG A 5 14.775 2.271 -7.122 1.00 15.00 A C +ATOM 64 NE ARG A 5 16.020 3.010 -6.935 1.00 15.00 A N +ATOM 65 HE ARG A 5 15.995 3.782 -6.328 1.00 15.00 A H +ATOM 66 CZ ARG A 5 17.169 2.705 -7.532 1.00 15.00 A C +ATOM 67 NH1 ARG A 5 17.244 1.669 -8.361 1.00 15.00 A N +ATOM 68 HH11 ARG A 5 16.403 1.086 -8.549 1.00 15.00 A H +ATOM 69 HH12 ARG A 5 18.145 1.437 -8.826 1.00 15.00 A H +ATOM 70 NH2 ARG A 5 18.247 3.444 -7.300 1.00 15.00 A N +ATOM 71 HH21 ARG A 5 18.194 4.260 -6.652 1.00 15.00 A H +ATOM 72 HH22 ARG A 5 19.148 3.212 -7.762 1.00 15.00 A H +ATOM 73 C ARG A 5 11.867 -0.099 -5.227 1.00 15.00 A C +ATOM 74 O ARG A 5 12.713 -0.739 -4.595 1.00 15.00 A O +ATOM 75 N LEU A 6 10.610 0.041 -4.816 1.00 15.00 A N +ATOM 76 HN LEU A 6 9.981 0.571 -5.362 1.00 15.00 A H +ATOM 77 CA LEU A 6 10.138 -0.563 -3.573 1.00 15.00 A C +ATOM 78 CB LEU A 6 8.736 -0.057 -3.235 1.00 15.00 A C +ATOM 79 CG LEU A 6 8.229 -0.367 -1.825 1.00 15.00 A C +ATOM 80 CD1 LEU A 6 9.038 0.393 -0.785 1.00 15.00 A C +ATOM 81 CD2 LEU A 6 6.749 -0.032 -1.704 1.00 15.00 A C +ATOM 82 C LEU A 6 10.128 -2.085 -3.682 1.00 15.00 A C +ATOM 83 O LEU A 6 10.600 -2.788 -2.783 1.00 15.00 A O +ATOM 84 N LYS A 7 9.587 -2.579 -4.795 1.00 15.00 A N +ATOM 85 HN LYS A 7 9.213 -1.952 -5.459 1.00 15.00 A H +ATOM 86 CA LYS A 7 9.510 -4.011 -5.063 1.00 15.00 A C +ATOM 87 CB LYS A 7 8.910 -4.222 -6.456 1.00 15.00 A C +ATOM 88 CG LYS A 7 8.932 -5.654 -6.960 1.00 15.00 A C +ATOM 89 CD LYS A 7 8.437 -5.728 -8.395 1.00 15.00 A C +ATOM 90 CE LYS A 7 9.379 -6.544 -9.268 1.00 15.00 A C +ATOM 91 NZ LYS A 7 9.125 -6.312 -10.717 1.00 15.00 A N +ATOM 92 HZ1 LYS A 7 8.204 -6.704 -10.989 1.00 15.00 A H +ATOM 93 HZ2 LYS A 7 9.866 -6.756 -11.290 1.00 15.00 A H +ATOM 94 HZ3 LYS A 7 9.121 -5.275 -10.919 1.00 15.00 A H +ATOM 95 C LYS A 7 10.894 -4.652 -4.988 1.00 15.00 A C +ATOM 96 O LYS A 7 11.080 -5.687 -4.337 1.00 15.00 A O +ATOM 97 N LYS A 8 11.857 -4.015 -5.650 1.00 15.00 A N +ATOM 98 HN LYS A 8 11.626 -3.192 -6.148 1.00 15.00 A H +ATOM 99 CA LYS A 8 13.235 -4.490 -5.676 1.00 15.00 A C +ATOM 100 CB LYS A 8 14.097 -3.546 -6.519 1.00 15.00 A C +ATOM 101 CG LYS A 8 15.549 -3.972 -6.664 1.00 15.00 A C +ATOM 102 CD LYS A 8 16.335 -2.960 -7.484 1.00 15.00 A C +ATOM 103 CE LYS A 8 17.794 -3.364 -7.619 1.00 15.00 A C +ATOM 104 NZ LYS A 8 18.498 -3.353 -6.308 1.00 15.00 A N +ATOM 105 HZ1 LYS A 8 18.155 -4.133 -5.710 1.00 15.00 A H +ATOM 106 HZ2 LYS A 8 18.321 -2.454 -5.815 1.00 15.00 A H +ATOM 107 HZ3 LYS A 8 19.522 -3.462 -6.451 1.00 15.00 A H +ATOM 108 C LYS A 8 13.811 -4.602 -4.270 1.00 15.00 A C +ATOM 109 O LYS A 8 14.405 -5.620 -3.914 1.00 15.00 A O +ATOM 110 N ARG A 9 13.623 -3.555 -3.470 1.00 15.00 A N +ATOM 111 HN ARG A 9 13.123 -2.774 -3.804 1.00 15.00 A H +ATOM 112 CA ARG A 9 14.133 -3.544 -2.105 1.00 15.00 A C +ATOM 113 CB ARG A 9 13.980 -2.159 -1.469 1.00 15.00 A C +ATOM 114 CG ARG A 9 14.720 -1.981 -0.148 1.00 15.00 A C +ATOM 115 CD ARG A 9 16.233 -2.029 -0.327 1.00 15.00 A C +ATOM 116 NE ARG A 9 16.727 -3.396 -0.493 1.00 15.00 A N +ATOM 117 HE ARG A 9 16.056 -4.114 -0.526 1.00 15.00 A H +ATOM 118 CZ ARG A 9 18.017 -3.721 -0.578 1.00 15.00 A C +ATOM 119 NH1 ARG A 9 18.954 -2.783 -0.506 1.00 15.00 A N +ATOM 120 HH11 ARG A 9 18.685 -1.786 -0.380 1.00 15.00 A H +ATOM 121 HH12 ARG A 9 19.960 -3.041 -0.571 1.00 15.00 A H +ATOM 122 NH2 ARG A 9 18.369 -4.989 -0.744 1.00 15.00 A N +ATOM 123 HH21 ARG A 9 17.640 -5.727 -0.809 1.00 15.00 A H +ATOM 124 HH22 ARG A 9 19.374 -5.249 -0.808 1.00 15.00 A H +ATOM 125 C ARG A 9 13.457 -4.615 -1.253 1.00 15.00 A C +ATOM 126 O ARG A 9 14.104 -5.241 -0.412 1.00 15.00 A O +ATOM 127 N ARG A 10 12.163 -4.828 -1.492 1.00 15.00 A N +ATOM 128 HN ARG A 10 11.709 -4.285 -2.179 1.00 15.00 A H +ATOM 129 CA ARG A 10 11.392 -5.830 -0.759 1.00 15.00 A C +ATOM 130 CB ARG A 10 9.892 -5.723 -1.094 1.00 15.00 A C +ATOM 131 CG ARG A 10 9.038 -6.837 -0.496 1.00 15.00 A C +ATOM 132 CD ARG A 10 7.544 -6.560 -0.623 1.00 15.00 A C +ATOM 133 NE ARG A 10 7.022 -6.854 -1.960 1.00 15.00 A N +ATOM 134 HE ARG A 10 7.589 -6.619 -2.724 1.00 15.00 A H +ATOM 135 CZ ARG A 10 5.825 -7.413 -2.194 1.00 15.00 A C +ATOM 136 NH1 ARG A 10 5.028 -7.745 -1.183 1.00 15.00 A N +ATOM 137 HH11 ARG A 10 5.331 -7.572 -0.206 1.00 15.00 A H +ATOM 138 HH12 ARG A 10 4.095 -8.178 -1.369 1.00 15.00 A H +ATOM 139 NH2 ARG A 10 5.422 -7.631 -3.441 1.00 15.00 A N +ATOM 140 HH21 ARG A 10 4.485 -8.068 -3.625 1.00 15.00 A H +ATOM 141 HH22 ARG A 10 6.031 -7.368 -4.242 1.00 15.00 A H +ATOM 142 C ARG A 10 11.899 -7.246 -1.038 1.00 15.00 A C +ATOM 143 O ARG A 10 12.277 -7.973 -0.116 1.00 15.00 A O +ATOM 144 N ILE A 11 11.927 -7.622 -2.315 1.00 15.00 A N +ATOM 145 HN ILE A 11 11.647 -6.980 -3.010 1.00 15.00 A H +ATOM 146 CA ILE A 11 12.365 -8.960 -2.717 1.00 15.00 A C +ATOM 147 CB ILE A 11 12.159 -9.200 -4.231 1.00 15.00 A C +ATOM 148 CG1 ILE A 11 10.691 -8.972 -4.608 1.00 15.00 A C +ATOM 149 CG2 ILE A 11 12.593 -10.610 -4.619 1.00 15.00 A C +ATOM 150 CD1 ILE A 11 10.426 -8.988 -6.100 1.00 15.00 A C +ATOM 151 C ILE A 11 13.824 -9.239 -2.343 1.00 15.00 A C +ATOM 152 O ILE A 11 14.144 -10.305 -1.813 1.00 15.00 A O +ATOM 153 N ALA A 12 14.697 -8.264 -2.586 1.00 15.00 A N +ATOM 154 HN ALA A 12 14.375 -7.417 -2.969 1.00 15.00 A H +ATOM 155 CA ALA A 12 16.123 -8.417 -2.297 1.00 15.00 A C +ATOM 156 CB ALA A 12 16.916 -7.263 -2.891 1.00 15.00 A C +ATOM 157 C ALA A 12 16.424 -8.571 -0.803 1.00 15.00 A C +ATOM 158 O ALA A 12 17.500 -9.040 -0.431 1.00 15.00 A O +ATOM 159 N LEU A 13 15.485 -8.168 0.050 1.00 15.00 A N +ATOM 160 HN LEU A 13 14.647 -7.789 -0.299 1.00 15.00 A H +ATOM 161 CA LEU A 13 15.676 -8.274 1.496 1.00 15.00 A C +ATOM 162 CB LEU A 13 15.251 -6.983 2.201 1.00 15.00 A C +ATOM 163 CG LEU A 13 16.202 -5.791 2.072 1.00 15.00 A C +ATOM 164 CD1 LEU A 13 15.629 -4.574 2.783 1.00 15.00 A C +ATOM 165 CD2 LEU A 13 17.576 -6.137 2.625 1.00 15.00 A C +ATOM 166 C LEU A 13 14.912 -9.457 2.085 1.00 15.00 A C +ATOM 167 O LEU A 13 15.004 -9.721 3.285 1.00 15.00 A O +ATOM 168 N LYS A 14 14.167 -10.167 1.232 1.00 15.00 A N +ATOM 169 HN LYS A 14 14.164 -9.915 0.284 1.00 15.00 A H +ATOM 170 CA LYS A 14 13.365 -11.317 1.661 1.00 15.00 A C +ATOM 171 CB LYS A 14 14.251 -12.467 2.164 1.00 15.00 A C +ATOM 172 CG LYS A 14 14.813 -13.355 1.062 1.00 15.00 A C +ATOM 173 CD LYS A 14 16.165 -12.863 0.569 1.00 15.00 A C +ATOM 174 CE LYS A 14 17.264 -13.143 1.582 1.00 15.00 A C +ATOM 175 NZ LYS A 14 18.599 -12.713 1.086 1.00 15.00 A N +ATOM 176 HZ1 LYS A 14 19.325 -12.893 1.809 1.00 15.00 A H +ATOM 177 HZ2 LYS A 14 18.587 -11.696 0.867 1.00 15.00 A H +ATOM 178 HZ3 LYS A 14 18.847 -13.240 0.223 1.00 15.00 A H +ATOM 179 C LYS A 14 12.338 -10.918 2.722 1.00 15.00 A C +ATOM 180 O LYS A 14 11.989 -11.712 3.596 1.00 15.00 A O +ATOM 181 N MET A 15 11.846 -9.688 2.625 1.00 15.00 A N +ATOM 182 HN MET A 15 12.139 -9.113 1.884 1.00 15.00 A H +ATOM 183 CA MET A 15 10.870 -9.175 3.580 1.00 15.00 A C +ATOM 184 CB MET A 15 11.289 -7.775 4.055 1.00 15.00 A C +ATOM 185 CG MET A 15 10.297 -7.108 4.998 1.00 15.00 A C +ATOM 186 SD MET A 15 10.713 -5.391 5.363 1.00 15.00 A S +ATOM 187 CE MET A 15 12.233 -5.597 6.289 1.00 15.00 A C +ATOM 188 C MET A 15 9.475 -9.117 2.967 1.00 15.00 A C +ATOM 189 O MET A 15 9.324 -8.837 1.779 1.00 15.00 A O +ATOM 190 N THR A 16 8.457 -9.395 3.778 1.00 15.00 A N +ATOM 191 HN THR A 16 8.639 -9.637 4.713 1.00 15.00 A H +ATOM 192 CA THR A 16 7.075 -9.341 3.310 1.00 15.00 A C +ATOM 193 CB THR A 16 6.181 -10.425 3.961 1.00 15.00 A C +ATOM 194 OG1 THR A 16 4.792 -10.120 3.757 1.00 15.00 A O +ATOM 195 HG1 THR A 16 4.396 -10.786 3.172 1.00 15.00 A H +ATOM 196 CG2 THR A 16 6.468 -10.568 5.448 1.00 15.00 A C +ATOM 197 C THR A 16 6.495 -7.939 3.531 1.00 15.00 A C +ATOM 198 O THR A 16 6.937 -7.206 4.423 1.00 15.00 A O +ATOM 199 N GLN A 17 5.499 -7.574 2.726 1.00 15.00 A N +ATOM 200 HN GLN A 17 5.150 -8.222 2.080 1.00 15.00 A H +ATOM 201 CA GLN A 17 4.887 -6.249 2.812 1.00 15.00 A C +ATOM 202 CB GLN A 17 3.951 -5.997 1.627 1.00 15.00 A C +ATOM 203 CG GLN A 17 3.621 -4.528 1.395 1.00 15.00 A C +ATOM 204 CD GLN A 17 2.696 -4.313 0.211 1.00 15.00 A C +ATOM 205 OE1 GLN A 17 1.913 -5.190 -0.144 1.00 15.00 A O +ATOM 206 NE2 GLN A 17 2.773 -3.137 -0.400 1.00 15.00 A N +ATOM 207 HE21 GLN A 17 3.413 -2.477 -0.062 1.00 15.00 A H +ATOM 208 HE22 GLN A 17 2.186 -2.980 -1.170 1.00 15.00 A H +ATOM 209 C GLN A 17 4.165 -6.039 4.140 1.00 15.00 A C +ATOM 210 O GLN A 17 4.150 -4.930 4.679 1.00 15.00 A O +ATOM 211 N THR A 18 3.571 -7.103 4.670 1.00 15.00 A N +ATOM 212 HN THR A 18 3.600 -7.962 4.191 1.00 15.00 A H +ATOM 213 CA THR A 18 2.868 -7.014 5.946 1.00 15.00 A C +ATOM 214 CB THR A 18 1.996 -8.261 6.234 1.00 15.00 A C +ATOM 215 OG1 THR A 18 1.289 -8.085 7.468 1.00 15.00 A O +ATOM 216 HG1 THR A 18 1.712 -8.607 8.157 1.00 15.00 A H +ATOM 217 CG2 THR A 18 2.843 -9.522 6.320 1.00 15.00 A C +ATOM 218 C THR A 18 3.852 -6.777 7.092 1.00 15.00 A C +ATOM 219 O THR A 18 3.517 -6.139 8.092 1.00 15.00 A O +ATOM 220 N GLU A 19 5.078 -7.268 6.924 1.00 15.00 A N +ATOM 221 HN GLU A 19 5.292 -7.741 6.092 1.00 15.00 A H +ATOM 222 CA GLU A 19 6.114 -7.112 7.935 1.00 15.00 A C +ATOM 223 CB GLU A 19 7.295 -8.018 7.601 1.00 15.00 A C +ATOM 224 CG GLU A 19 8.322 -8.152 8.707 1.00 15.00 A C +ATOM 225 CD GLU A 19 9.640 -8.683 8.188 1.00 15.00 A C +ATOM 226 OE1 GLU A 19 9.620 -9.487 7.233 1.00 15.00 A O +ATOM 227 OE2 GLU A 19 10.695 -8.285 8.730 1.00 15.00 A O +ATOM 228 C GLU A 19 6.573 -5.662 7.981 1.00 15.00 A C +ATOM 229 O GLU A 19 6.724 -5.074 9.055 1.00 15.00 A O +ATOM 230 N LEU A 20 6.781 -5.089 6.798 1.00 15.00 A N +ATOM 231 HN LEU A 20 6.647 -5.619 5.980 1.00 15.00 A H +ATOM 232 CA LEU A 20 7.203 -3.701 6.684 1.00 15.00 A C +ATOM 233 CB LEU A 20 7.489 -3.355 5.219 1.00 15.00 A C +ATOM 234 CG LEU A 20 7.862 -1.899 4.913 1.00 15.00 A C +ATOM 235 CD1 LEU A 20 9.142 -1.506 5.633 1.00 15.00 A C +ATOM 236 CD2 LEU A 20 8.004 -1.690 3.411 1.00 15.00 A C +ATOM 237 C LEU A 20 6.122 -2.785 7.243 1.00 15.00 A C +ATOM 238 O LEU A 20 6.413 -1.826 7.955 1.00 15.00 A O +ATOM 239 N ALA A 21 4.871 -3.109 6.929 1.00 15.00 A N +ATOM 240 HN ALA A 21 4.708 -3.891 6.356 1.00 15.00 A H +ATOM 241 CA ALA A 21 3.736 -2.330 7.395 1.00 15.00 A C +ATOM 242 CB ALA A 21 2.467 -2.804 6.716 1.00 15.00 A C +ATOM 243 C ALA A 21 3.595 -2.423 8.907 1.00 15.00 A C +ATOM 244 O ALA A 21 3.266 -1.441 9.570 1.00 15.00 A O +ATOM 245 N THR A 22 3.859 -3.607 9.448 1.00 15.00 A N +ATOM 246 HN THR A 22 4.121 -4.354 8.861 1.00 15.00 A H +ATOM 247 CA THR A 22 3.768 -3.829 10.886 1.00 15.00 A C +ATOM 248 CB THR A 22 3.882 -5.330 11.230 1.00 15.00 A C +ATOM 249 OG1 THR A 22 2.832 -6.049 10.572 1.00 15.00 A O +ATOM 250 HG1 THR A 22 2.978 -6.031 9.615 1.00 15.00 A H +ATOM 251 CG2 THR A 22 3.776 -5.559 12.732 1.00 15.00 A C +ATOM 252 C THR A 22 4.852 -3.046 11.624 1.00 15.00 A C +ATOM 253 O THR A 22 4.590 -2.425 12.653 1.00 15.00 A O +ATOM 254 N LYS A 23 6.066 -3.069 11.079 1.00 15.00 A N +ATOM 255 HN LYS A 23 6.213 -3.591 10.254 1.00 15.00 A H +ATOM 256 CA LYS A 23 7.190 -2.357 11.676 1.00 15.00 A C +ATOM 257 CB LYS A 23 8.507 -2.804 11.039 1.00 15.00 A C +ATOM 258 CG LYS A 23 8.968 -4.177 11.502 1.00 15.00 A C +ATOM 259 CD LYS A 23 10.105 -4.709 10.645 1.00 15.00 A C +ATOM 260 CE LYS A 23 10.651 -6.008 11.213 1.00 15.00 A C +ATOM 261 NZ LYS A 23 11.698 -6.605 10.341 1.00 15.00 A N +ATOM 262 HZ1 LYS A 23 12.334 -7.202 10.903 1.00 15.00 A H +ATOM 263 HZ2 LYS A 23 12.253 -5.857 9.882 1.00 15.00 A H +ATOM 264 HZ3 LYS A 23 11.251 -7.205 9.595 1.00 15.00 A H +ATOM 265 C LYS A 23 7.019 -0.839 11.575 1.00 15.00 A C +ATOM 266 O LYS A 23 7.561 -0.090 12.389 1.00 15.00 A O +ATOM 267 N ALA A 24 6.266 -0.394 10.570 1.00 15.00 A N +ATOM 268 HN ALA A 24 5.887 -1.041 9.935 1.00 15.00 A H +ATOM 269 CA ALA A 24 6.009 1.030 10.370 1.00 15.00 A C +ATOM 270 CB ALA A 24 5.826 1.331 8.890 1.00 15.00 A C +ATOM 271 C ALA A 24 4.783 1.484 11.164 1.00 15.00 A C +ATOM 272 O ALA A 24 4.607 2.674 11.423 1.00 15.00 A O +ATOM 273 N GLY A 25 3.937 0.526 11.539 1.00 15.00 A N +ATOM 274 HN GLY A 25 4.130 -0.405 11.293 1.00 15.00 A H +ATOM 275 CA GLY A 25 2.741 0.836 12.306 1.00 15.00 A C +ATOM 276 C GLY A 25 1.539 1.146 11.430 1.00 15.00 A C +ATOM 277 O GLY A 25 0.632 1.869 11.844 1.00 15.00 A O +ATOM 278 N VAL A 26 1.527 0.591 10.223 1.00 15.00 A N +ATOM 279 HN VAL A 26 2.270 -0.003 9.960 1.00 15.00 A H +ATOM 280 CA VAL A 26 0.433 0.818 9.282 1.00 15.00 A C +ATOM 281 CB VAL A 26 0.862 1.733 8.112 1.00 15.00 A C +ATOM 282 CG1 VAL A 26 0.959 3.179 8.572 1.00 15.00 A C +ATOM 283 CG2 VAL A 26 2.184 1.269 7.517 1.00 15.00 A C +ATOM 284 C VAL A 26 -0.099 -0.502 8.729 1.00 15.00 A C +ATOM 285 O VAL A 26 0.235 -1.573 9.235 1.00 15.00 A O +ATOM 286 N LYS A 27 -0.931 -0.420 7.693 1.00 15.00 A N +ATOM 287 HN LYS A 27 -1.163 0.462 7.332 1.00 15.00 A H +ATOM 288 CA LYS A 27 -1.501 -1.611 7.075 1.00 15.00 A C +ATOM 289 CB LYS A 27 -3.000 -1.439 6.852 1.00 15.00 A C +ATOM 290 CG LYS A 27 -3.826 -1.303 8.119 1.00 15.00 A C +ATOM 291 CD LYS A 27 -3.801 -2.578 8.945 1.00 15.00 A C +ATOM 292 CE LYS A 27 -5.004 -2.653 9.872 1.00 15.00 A C +ATOM 293 NZ LYS A 27 -5.255 -1.359 10.566 1.00 15.00 A N +ATOM 294 HZ1 LYS A 27 -6.118 -1.422 11.141 1.00 15.00 A H +ATOM 295 HZ2 LYS A 27 -5.379 -0.595 9.865 1.00 15.00 A H +ATOM 296 HZ3 LYS A 27 -4.454 -1.122 11.184 1.00 15.00 A H +ATOM 297 C LYS A 27 -0.820 -1.922 5.748 1.00 15.00 A C +ATOM 298 O LYS A 27 -0.389 -1.016 5.031 1.00 15.00 A O +ATOM 299 N GLN A 28 -0.739 -3.209 5.424 1.00 15.00 A N +ATOM 300 HN GLN A 28 -1.108 -3.877 6.039 1.00 15.00 A H +ATOM 301 CA GLN A 28 -0.109 -3.661 4.186 1.00 15.00 A C +ATOM 302 CB GLN A 28 -0.083 -5.190 4.143 1.00 15.00 A C +ATOM 303 CG GLN A 28 0.581 -5.761 2.902 1.00 15.00 A C +ATOM 304 CD GLN A 28 0.238 -7.216 2.667 1.00 15.00 A C +ATOM 305 OE1 GLN A 28 0.855 -8.106 3.241 1.00 15.00 A O +ATOM 306 NE2 GLN A 28 -0.736 -7.466 1.805 1.00 15.00 A N +ATOM 307 HE21 GLN A 28 -1.176 -6.708 1.368 1.00 15.00 A H +ATOM 308 HE22 GLN A 28 -0.976 -8.402 1.638 1.00 15.00 A H +ATOM 309 C GLN A 28 -0.850 -3.137 2.961 1.00 15.00 A C +ATOM 310 O GLN A 28 -0.234 -2.683 1.995 1.00 15.00 A O +ATOM 311 N GLN A 29 -2.176 -3.182 3.023 1.00 15.00 A N +ATOM 312 HN GLN A 29 -2.604 -3.518 3.839 1.00 15.00 A H +ATOM 313 CA GLN A 29 -3.018 -2.740 1.917 1.00 15.00 A C +ATOM 314 CB GLN A 29 -4.475 -3.108 2.175 1.00 15.00 A C +ATOM 315 CG GLN A 29 -4.701 -4.585 2.452 1.00 15.00 A C +ATOM 316 CD GLN A 29 -5.437 -4.822 3.755 1.00 15.00 A C +ATOM 317 OE1 GLN A 29 -5.227 -4.108 4.738 1.00 15.00 A O +ATOM 318 NE2 GLN A 29 -6.316 -5.815 3.767 1.00 15.00 A N +ATOM 319 HE21 GLN A 29 -6.441 -6.336 2.944 1.00 15.00 A H +ATOM 320 HE22 GLN A 29 -6.816 -5.979 4.593 1.00 15.00 A H +ATOM 321 C GLN A 29 -2.868 -1.245 1.638 1.00 15.00 A C +ATOM 322 O GLN A 29 -3.112 -0.789 0.519 1.00 15.00 A O +ATOM 323 N SER A 30 -2.463 -0.489 2.654 1.00 15.00 A N +ATOM 324 HN SER A 30 -2.304 -0.904 3.527 1.00 15.00 A H +ATOM 325 CA SER A 30 -2.255 0.944 2.502 1.00 15.00 A C +ATOM 326 CB SER A 30 -2.099 1.611 3.870 1.00 15.00 A C +ATOM 327 OG SER A 30 -3.312 1.576 4.599 1.00 15.00 A O +ATOM 328 HG SER A 30 -3.517 2.469 4.915 1.00 15.00 A H +ATOM 329 C SER A 30 -1.007 1.191 1.664 1.00 15.00 A C +ATOM 330 O SER A 30 -0.969 2.089 0.822 1.00 15.00 A O +ATOM 331 N ILE A 31 0.005 0.357 1.889 1.00 15.00 A N +ATOM 332 HN ILE A 31 -0.105 -0.352 2.560 1.00 15.00 A H +ATOM 333 CA ILE A 31 1.267 0.457 1.168 1.00 15.00 A C +ATOM 334 CB ILE A 31 2.372 -0.385 1.848 1.00 15.00 A C +ATOM 335 CG1 ILE A 31 2.377 -0.162 3.366 1.00 15.00 A C +ATOM 336 CG2 ILE A 31 3.741 -0.073 1.257 1.00 15.00 A C +ATOM 337 CD1 ILE A 31 2.648 1.268 3.785 1.00 15.00 A C +ATOM 338 C ILE A 31 1.087 -0.020 -0.271 1.00 15.00 A C +ATOM 339 O ILE A 31 1.789 0.427 -1.179 1.00 15.00 A O +ATOM 340 N GLN A 32 0.127 -0.923 -0.471 1.00 15.00 A N +ATOM 341 HN GLN A 32 -0.394 -1.241 0.299 1.00 15.00 A H +ATOM 342 CA GLN A 32 -0.162 -1.465 -1.797 1.00 15.00 A C +ATOM 343 CB GLN A 32 -1.286 -2.491 -1.726 1.00 15.00 A C +ATOM 344 CG GLN A 32 -0.898 -3.791 -1.055 1.00 15.00 A C +ATOM 345 CD GLN A 32 -1.987 -4.827 -1.176 1.00 15.00 A C +ATOM 346 OE1 GLN A 32 -2.861 -4.930 -0.318 1.00 15.00 A O +ATOM 347 NE2 GLN A 32 -1.948 -5.595 -2.251 1.00 15.00 A N +ATOM 348 HE21 GLN A 32 -1.222 -5.454 -2.898 1.00 15.00 A H +ATOM 349 HE22 GLN A 32 -2.642 -6.279 -2.353 1.00 15.00 A H +ATOM 350 C GLN A 32 -0.535 -0.368 -2.782 1.00 15.00 A C +ATOM 351 O GLN A 32 -0.254 -0.473 -3.976 1.00 15.00 A O +ATOM 352 N LEU A 33 -1.164 0.690 -2.276 1.00 15.00 A N +ATOM 353 HN LEU A 33 -1.362 0.719 -1.314 1.00 15.00 A H +ATOM 354 CA LEU A 33 -1.560 1.810 -3.119 1.00 15.00 A C +ATOM 355 CB LEU A 33 -2.403 2.820 -2.339 1.00 15.00 A C +ATOM 356 CG LEU A 33 -3.814 2.384 -1.935 1.00 15.00 A C +ATOM 357 CD1 LEU A 33 -4.591 3.571 -1.397 1.00 15.00 A C +ATOM 358 CD2 LEU A 33 -4.549 1.755 -3.109 1.00 15.00 A C +ATOM 359 C LEU A 33 -0.333 2.491 -3.722 1.00 15.00 A C +ATOM 360 O LEU A 33 -0.424 3.157 -4.752 1.00 15.00 A O +ATOM 361 N ILE A 34 0.816 2.323 -3.072 1.00 15.00 A N +ATOM 362 HN ILE A 34 0.826 1.791 -2.246 1.00 15.00 A H +ATOM 363 CA ILE A 34 2.062 2.899 -3.563 1.00 15.00 A C +ATOM 364 CB ILE A 34 3.138 2.969 -2.458 1.00 15.00 A C +ATOM 365 CG1 ILE A 34 2.585 3.678 -1.218 1.00 15.00 A C +ATOM 366 CG2 ILE A 34 4.385 3.680 -2.969 1.00 15.00 A C +ATOM 367 CD1 ILE A 34 3.488 3.594 -0.005 1.00 15.00 A C +ATOM 368 C ILE A 34 2.582 2.054 -4.721 1.00 15.00 A C +ATOM 369 O ILE A 34 2.954 2.579 -5.771 1.00 15.00 A O +ATOM 370 N GLU A 35 2.575 0.734 -4.525 1.00 15.00 A N +ATOM 371 HN GLU A 35 2.255 0.382 -3.667 1.00 15.00 A H +ATOM 372 CA GLU A 35 3.027 -0.205 -5.549 1.00 15.00 A C +ATOM 373 CB GLU A 35 3.046 -1.630 -4.993 1.00 15.00 A C +ATOM 374 CG GLU A 35 4.430 -2.127 -4.610 1.00 15.00 A C +ATOM 375 CD GLU A 35 5.222 -2.627 -5.803 1.00 15.00 A C +ATOM 376 OE1 GLU A 35 5.933 -3.642 -5.657 1.00 15.00 A O +ATOM 377 OE2 GLU A 35 5.127 -2.009 -6.885 1.00 15.00 A O +ATOM 378 C GLU A 35 2.129 -0.131 -6.780 1.00 15.00 A C +ATOM 379 O GLU A 35 2.588 -0.276 -7.911 1.00 15.00 A O +ATOM 380 N ALA A 36 0.841 0.112 -6.547 1.00 15.00 A N +ATOM 381 HN ALA A 36 0.533 0.197 -5.615 1.00 15.00 A H +ATOM 382 CA ALA A 36 -0.129 0.229 -7.630 1.00 15.00 A C +ATOM 383 CB ALA A 36 -1.544 0.064 -7.095 1.00 15.00 A C +ATOM 384 C ALA A 36 0.019 1.564 -8.363 1.00 15.00 A C +ATOM 385 O ALA A 36 -0.672 1.822 -9.349 1.00 15.00 A O +ATOM 386 N GLY A 37 0.922 2.412 -7.871 1.00 15.00 A N +ATOM 387 HN GLY A 37 1.446 2.150 -7.081 1.00 15.00 A H +ATOM 388 CA GLY A 37 1.157 3.704 -8.493 1.00 15.00 A C +ATOM 389 C GLY A 37 0.078 4.726 -8.185 1.00 15.00 A C +ATOM 390 O GLY A 37 -0.115 5.678 -8.943 1.00 15.00 A O +ATOM 391 N VAL A 38 -0.618 4.538 -7.070 1.00 15.00 A N +ATOM 392 HN VAL A 38 -0.408 3.766 -6.494 1.00 15.00 A H +ATOM 393 CA VAL A 38 -1.682 5.454 -6.672 1.00 15.00 A C +ATOM 394 CB VAL A 38 -2.910 4.699 -6.116 1.00 15.00 A C +ATOM 395 CG1 VAL A 38 -4.069 5.657 -5.885 1.00 15.00 A C +ATOM 396 CG2 VAL A 38 -3.322 3.574 -7.054 1.00 15.00 A C +ATOM 397 C VAL A 38 -1.187 6.453 -5.627 1.00 15.00 A C +ATOM 398 O VAL A 38 -1.225 7.663 -5.848 1.00 15.00 A O +ATOM 399 N THR A 39 -0.717 5.938 -4.494 1.00 15.00 A N +ATOM 400 HN THR A 39 -0.701 4.961 -4.382 1.00 15.00 A H +ATOM 401 CA THR A 39 -0.218 6.786 -3.416 1.00 15.00 A C +ATOM 402 CB THR A 39 -0.075 6.004 -2.094 1.00 15.00 A C +ATOM 403 OG1 THR A 39 -1.361 5.539 -1.677 1.00 15.00 A O +ATOM 404 HG1 THR A 39 -1.867 6.278 -1.312 1.00 15.00 A H +ATOM 405 CG2 THR A 39 0.511 6.887 -1.003 1.00 15.00 A C +ATOM 406 C THR A 39 1.119 7.417 -3.790 1.00 15.00 A C +ATOM 407 O THR A 39 2.138 6.731 -3.898 1.00 15.00 A O +ATOM 408 N LYS A 40 1.101 8.728 -4.001 1.00 15.00 A N +ATOM 409 HN LYS A 40 0.250 9.212 -3.928 1.00 15.00 A H +ATOM 410 CA LYS A 40 2.306 9.458 -4.369 1.00 15.00 A C +ATOM 411 CB LYS A 40 2.097 10.190 -5.697 1.00 15.00 A C +ATOM 412 CG LYS A 40 1.778 9.264 -6.859 1.00 15.00 A C +ATOM 413 CD LYS A 40 1.101 10.007 -7.997 1.00 15.00 A C +ATOM 414 CE LYS A 40 0.560 9.038 -9.035 1.00 15.00 A C +ATOM 415 NZ LYS A 40 -0.182 9.737 -10.120 1.00 15.00 A N +ATOM 416 HZ1 LYS A 40 0.457 10.371 -10.639 1.00 15.00 A H +ATOM 417 HZ2 LYS A 40 -0.959 10.300 -9.716 1.00 15.00 A H +ATOM 418 HZ3 LYS A 40 -0.581 9.043 -10.784 1.00 15.00 A H +ATOM 419 C LYS A 40 2.730 10.440 -3.278 1.00 15.00 A C +ATOM 420 O LYS A 40 3.784 11.070 -3.378 1.00 15.00 A O +ATOM 421 N ARG A 41 1.911 10.569 -2.235 1.00 15.00 A N +ATOM 422 HN ARG A 41 1.088 10.038 -2.202 1.00 15.00 A H +ATOM 423 CA ARG A 41 2.219 11.480 -1.135 1.00 15.00 A C +ATOM 424 CB ARG A 41 1.546 12.842 -1.336 1.00 15.00 A C +ATOM 425 CG ARG A 41 2.379 13.833 -2.137 1.00 15.00 A C +ATOM 426 CD ARG A 41 3.684 14.155 -1.425 1.00 15.00 A C +ATOM 427 NE ARG A 41 4.574 14.981 -2.240 1.00 15.00 A N +ATOM 428 HE ARG A 41 4.342 15.091 -3.186 1.00 15.00 A H +ATOM 429 CZ ARG A 41 5.668 15.582 -1.770 1.00 15.00 A C +ATOM 430 NH1 ARG A 41 6.004 15.451 -0.493 1.00 15.00 A N +ATOM 431 HH11 ARG A 41 5.415 14.879 0.145 1.00 15.00 A H +ATOM 432 HH12 ARG A 41 6.857 15.919 -0.125 1.00 15.00 A H +ATOM 433 NH2 ARG A 41 6.428 16.311 -2.580 1.00 15.00 A N +ATOM 434 HH21 ARG A 41 7.282 16.780 -2.214 1.00 15.00 A H +ATOM 435 HH22 ARG A 41 6.174 16.415 -3.584 1.00 15.00 A H +ATOM 436 C ARG A 41 1.853 10.888 0.228 1.00 15.00 A C +ATOM 437 O ARG A 41 0.918 11.348 0.885 1.00 15.00 A O +ATOM 438 N PRO A 42 2.602 9.866 0.680 1.00 15.00 A N +ATOM 439 CA PRO A 42 2.359 9.213 1.963 1.00 15.00 A C +ATOM 440 CB PRO A 42 3.087 7.864 1.830 1.00 15.00 A C +ATOM 441 CG PRO A 42 3.693 7.845 0.460 1.00 15.00 A C +ATOM 442 CD PRO A 42 3.746 9.268 -0.011 1.00 15.00 A C +ATOM 443 C PRO A 42 2.953 10.010 3.122 1.00 15.00 A C +ATOM 444 O PRO A 42 3.978 10.675 2.970 1.00 15.00 A O +ATOM 445 N ARG A 43 2.306 9.942 4.281 1.00 15.00 A N +ATOM 446 HN ARG A 43 1.489 9.402 4.345 1.00 15.00 A H +ATOM 447 CA ARG A 43 2.785 10.655 5.461 1.00 15.00 A C +ATOM 448 CB ARG A 43 1.622 11.118 6.346 1.00 15.00 A C +ATOM 449 CG ARG A 43 0.716 12.169 5.715 1.00 15.00 A C +ATOM 450 CD ARG A 43 -0.346 11.535 4.829 1.00 15.00 A C +ATOM 451 NE ARG A 43 -1.018 10.424 5.501 1.00 15.00 A N +ATOM 452 HE ARG A 43 -1.389 10.597 6.392 1.00 15.00 A H +ATOM 453 CZ ARG A 43 -1.153 9.211 4.976 1.00 15.00 A C +ATOM 454 NH1 ARG A 43 -0.668 8.950 3.768 1.00 15.00 A N +ATOM 455 HH11 ARG A 43 -0.776 8.000 3.354 1.00 15.00 A H +ATOM 456 HH12 ARG A 43 -0.197 9.698 3.218 1.00 15.00 A H +ATOM 457 NH2 ARG A 43 -1.784 8.262 5.655 1.00 15.00 A N +ATOM 458 HH21 ARG A 43 -2.176 8.462 6.597 1.00 15.00 A H +ATOM 459 HH22 ARG A 43 -1.895 7.307 5.250 1.00 15.00 A H +ATOM 460 C ARG A 43 3.736 9.773 6.258 1.00 15.00 A C +ATOM 461 O ARG A 43 4.587 10.264 6.994 1.00 15.00 A O +ATOM 462 N PHE A 44 3.594 8.462 6.082 1.00 15.00 A N +ATOM 463 HN PHE A 44 2.905 8.140 5.468 1.00 15.00 A H +ATOM 464 CA PHE A 44 4.435 7.488 6.772 1.00 15.00 A C +ATOM 465 CB PHE A 44 3.641 6.210 7.078 1.00 15.00 A C +ATOM 466 CG PHE A 44 2.616 5.848 6.035 1.00 15.00 A C +ATOM 467 CD1 PHE A 44 2.996 5.263 4.839 1.00 15.00 A C +ATOM 468 CD2 PHE A 44 1.269 6.092 6.255 1.00 15.00 A C +ATOM 469 CE1 PHE A 44 2.056 4.929 3.881 1.00 15.00 A C +ATOM 470 CE2 PHE A 44 0.324 5.761 5.301 1.00 15.00 A C +ATOM 471 CZ PHE A 44 0.718 5.179 4.113 1.00 15.00 A C +ATOM 472 C PHE A 44 5.686 7.168 5.955 1.00 15.00 A C +ATOM 473 O PHE A 44 6.256 6.079 6.055 1.00 15.00 A O +ATOM 474 N LEU A 45 6.109 8.140 5.155 1.00 15.00 A N +ATOM 475 HN LEU A 45 5.614 8.988 5.144 1.00 15.00 A H +ATOM 476 CA LEU A 45 7.283 7.998 4.303 1.00 15.00 A C +ATOM 477 CB LEU A 45 7.436 9.254 3.441 1.00 15.00 A C +ATOM 478 CG LEU A 45 8.593 9.262 2.443 1.00 15.00 A C +ATOM 479 CD1 LEU A 45 8.336 8.275 1.314 1.00 15.00 A C +ATOM 480 CD2 LEU A 45 8.806 10.665 1.896 1.00 15.00 A C +ATOM 481 C LEU A 45 8.545 7.777 5.133 1.00 15.00 A C +ATOM 482 O LEU A 45 9.387 6.944 4.790 1.00 15.00 A O +ATOM 483 N PHE A 46 8.655 8.516 6.234 1.00 15.00 A N +ATOM 484 HN PHE A 46 7.931 9.136 6.462 1.00 15.00 A H +ATOM 485 CA PHE A 46 9.808 8.420 7.122 1.00 15.00 A C +ATOM 486 CB PHE A 46 9.733 9.522 8.184 1.00 15.00 A C +ATOM 487 CG PHE A 46 11.061 9.885 8.786 1.00 15.00 A C +ATOM 488 CD1 PHE A 46 11.994 10.605 8.055 1.00 15.00 A C +ATOM 489 CD2 PHE A 46 11.376 9.507 10.081 1.00 15.00 A C +ATOM 490 CE1 PHE A 46 13.217 10.940 8.604 1.00 15.00 A C +ATOM 491 CE2 PHE A 46 12.596 9.839 10.638 1.00 15.00 A C +ATOM 492 CZ PHE A 46 13.519 10.556 9.898 1.00 15.00 A C +ATOM 493 C PHE A 46 9.864 7.045 7.784 1.00 15.00 A C +ATOM 494 O PHE A 46 10.926 6.418 7.853 1.00 15.00 A O +ATOM 495 N GLU A 47 8.708 6.588 8.253 1.00 15.00 A N +ATOM 496 HN GLU A 47 7.901 7.158 8.173 1.00 15.00 A H +ATOM 497 CA GLU A 47 8.580 5.291 8.906 1.00 15.00 A C +ATOM 498 CB GLU A 47 7.126 5.057 9.333 1.00 15.00 A C +ATOM 499 CG GLU A 47 6.648 5.933 10.485 1.00 15.00 A C +ATOM 500 CD GLU A 47 6.668 7.417 10.165 1.00 15.00 A C +ATOM 501 OE1 GLU A 47 6.496 7.779 8.977 1.00 15.00 A O +ATOM 502 OE2 GLU A 47 6.868 8.219 11.097 1.00 15.00 A O +ATOM 503 C GLU A 47 9.032 4.170 7.979 1.00 15.00 A C +ATOM 504 O GLU A 47 9.847 3.328 8.363 1.00 15.00 A O +ATOM 505 N ILE A 48 8.506 4.172 6.753 1.00 15.00 A N +ATOM 506 HN ILE A 48 7.854 4.870 6.512 1.00 15.00 A H +ATOM 507 CA ILE A 48 8.868 3.162 5.762 1.00 15.00 A C +ATOM 508 CB ILE A 48 8.089 3.350 4.440 1.00 15.00 A C +ATOM 509 CG1 ILE A 48 6.587 3.184 4.673 1.00 15.00 A C +ATOM 510 CG2 ILE A 48 8.578 2.367 3.381 1.00 15.00 A C +ATOM 511 CD1 ILE A 48 5.749 3.483 3.449 1.00 15.00 A C +ATOM 512 C ILE A 48 10.364 3.212 5.465 1.00 15.00 A C +ATOM 513 O ILE A 48 11.035 2.180 5.453 1.00 15.00 A O +ATOM 514 N ALA A 49 10.879 4.420 5.248 1.00 15.00 A N +ATOM 515 HN ALA A 49 10.286 5.208 5.291 1.00 15.00 A H +ATOM 516 CA ALA A 49 12.295 4.613 4.947 1.00 15.00 A C +ATOM 517 CB ALA A 49 12.588 6.084 4.711 1.00 15.00 A C +ATOM 518 C ALA A 49 13.194 4.059 6.048 1.00 15.00 A C +ATOM 519 O ALA A 49 14.188 3.393 5.768 1.00 15.00 A O +ATOM 520 N MET A 50 12.834 4.329 7.298 1.00 15.00 A N +ATOM 521 HN MET A 50 12.021 4.863 7.462 1.00 15.00 A H +ATOM 522 CA MET A 50 13.621 3.853 8.433 1.00 15.00 A C +ATOM 523 CB MET A 50 13.246 4.596 9.717 1.00 15.00 A C +ATOM 524 CG MET A 50 13.807 6.007 9.784 1.00 15.00 A C +ATOM 525 SD MET A 50 15.607 6.029 9.667 1.00 15.00 A S +ATOM 526 CE MET A 50 15.907 7.750 9.278 1.00 15.00 A C +ATOM 527 C MET A 50 13.496 2.342 8.615 1.00 15.00 A C +ATOM 528 O MET A 50 14.493 1.647 8.816 1.00 15.00 A O +ATOM 529 N ALA A 51 12.268 1.837 8.526 1.00 15.00 A N +ATOM 530 HN ALA A 51 11.511 2.448 8.359 1.00 15.00 A H +ATOM 531 CA ALA A 51 12.007 0.405 8.677 1.00 15.00 A C +ATOM 532 CB ALA A 51 10.510 0.145 8.709 1.00 15.00 A C +ATOM 533 C ALA A 51 12.665 -0.410 7.565 1.00 15.00 A C +ATOM 534 O ALA A 51 13.150 -1.520 7.794 1.00 15.00 A O +ATOM 535 N LEU A 52 12.672 0.145 6.358 1.00 15.00 A N +ATOM 536 HN LEU A 52 12.244 1.025 6.230 1.00 15.00 A H +ATOM 537 CA LEU A 52 13.271 -0.525 5.209 1.00 15.00 A C +ATOM 538 CB LEU A 52 12.544 -0.113 3.926 1.00 15.00 A C +ATOM 539 CG LEU A 52 12.674 -1.061 2.733 1.00 15.00 A C +ATOM 540 CD1 LEU A 52 12.235 -2.467 3.115 1.00 15.00 A C +ATOM 541 CD2 LEU A 52 11.861 -0.547 1.556 1.00 15.00 A C +ATOM 542 C LEU A 52 14.766 -0.221 5.099 1.00 15.00 A C +ATOM 543 O LEU A 52 15.455 -0.766 4.233 1.00 15.00 A O +ATOM 544 N ASN A 53 15.258 0.641 5.991 1.00 15.00 A N +ATOM 545 HN ASN A 53 14.648 1.025 6.660 1.00 15.00 A H +ATOM 546 CA ASN A 53 16.671 1.033 6.017 1.00 15.00 A C +ATOM 547 CB ASN A 53 17.579 -0.145 6.384 1.00 15.00 A C +ATOM 548 CG ASN A 53 17.592 -0.424 7.872 1.00 15.00 A C +ATOM 549 OD1 ASN A 53 18.444 0.077 8.603 1.00 15.00 A O +ATOM 550 ND2 ASN A 53 16.638 -1.219 8.334 1.00 15.00 A N +ATOM 551 HD21 ASN A 53 15.985 -1.578 7.697 1.00 15.00 A H +ATOM 552 HD22 ASN A 53 16.621 -1.409 9.293 1.00 15.00 A H +ATOM 553 C ASN A 53 17.117 1.692 4.712 1.00 15.00 A C +ATOM 554 O ASN A 53 18.071 1.247 4.066 1.00 15.00 A O +ATOM 555 N CYS A 54 16.421 2.759 4.335 1.00 15.00 A N +ATOM 556 HN CYS A 54 15.669 3.067 4.896 1.00 15.00 A H +ATOM 557 CA CYS A 54 16.738 3.490 3.118 1.00 15.00 A C +ATOM 558 CB CYS A 54 16.059 2.853 1.902 1.00 15.00 A C +ATOM 559 SG CYS A 54 14.253 2.859 1.957 1.00 15.00 A S +ATOM 560 HG CYS A 54 13.839 4.069 1.607 1.00 15.00 A H +ATOM 561 C CYS A 54 16.345 4.957 3.248 1.00 15.00 A C +ATOM 562 O CYS A 54 15.815 5.383 4.275 1.00 15.00 A O +ATOM 563 N ASP A 55 16.604 5.717 2.196 1.00 15.00 A N +ATOM 564 HN ASP A 55 17.007 5.311 1.404 1.00 15.00 A H +ATOM 565 CA ASP A 55 16.298 7.139 2.176 1.00 15.00 A C +ATOM 566 CB ASP A 55 17.319 7.865 1.294 1.00 15.00 A C +ATOM 567 CG ASP A 55 17.038 9.343 1.149 1.00 15.00 A C +ATOM 568 OD1 ASP A 55 16.222 9.707 0.276 1.00 15.00 A O +ATOM 569 OD2 ASP A 55 17.630 10.137 1.909 1.00 15.00 A O +ATOM 570 C ASP A 55 14.874 7.384 1.670 1.00 15.00 A C +ATOM 571 O ASP A 55 14.422 6.722 0.731 1.00 15.00 A O +ATOM 572 N PRO A 56 14.143 8.330 2.299 1.00 15.00 A N +ATOM 573 CA PRO A 56 12.762 8.665 1.911 1.00 15.00 A C +ATOM 574 CB PRO A 56 12.414 9.850 2.818 1.00 15.00 A C +ATOM 575 CG PRO A 56 13.320 9.706 3.991 1.00 15.00 A C +ATOM 576 CD PRO A 56 14.598 9.134 3.450 1.00 15.00 A C +ATOM 577 C PRO A 56 12.638 9.078 0.442 1.00 15.00 A C +ATOM 578 O PRO A 56 11.801 8.548 -0.290 1.00 15.00 A O +ATOM 579 N VAL A 57 13.485 10.012 0.014 1.00 15.00 A N +ATOM 580 HN VAL A 57 14.162 10.363 0.633 1.00 15.00 A H +ATOM 581 CA VAL A 57 13.463 10.501 -1.364 1.00 15.00 A C +ATOM 582 CB VAL A 57 14.364 11.744 -1.529 1.00 15.00 A C +ATOM 583 CG1 VAL A 57 14.266 12.310 -2.939 1.00 15.00 A C +ATOM 584 CG2 VAL A 57 14.006 12.806 -0.500 1.00 15.00 A C +ATOM 585 C VAL A 57 13.926 9.404 -2.320 1.00 15.00 A C +ATOM 586 O VAL A 57 13.449 9.305 -3.453 1.00 15.00 A O +ATOM 587 N TRP A 58 14.851 8.580 -1.839 1.00 15.00 A N +ATOM 588 HN TRP A 58 15.202 8.744 -0.926 1.00 15.00 A H +ATOM 589 CA TRP A 58 15.389 7.467 -2.617 1.00 15.00 A C +ATOM 590 CB TRP A 58 16.447 6.719 -1.799 1.00 15.00 A C +ATOM 591 CG TRP A 58 17.159 5.617 -2.530 1.00 15.00 A C +ATOM 592 CD1 TRP A 58 18.294 5.726 -3.280 1.00 15.00 A C +ATOM 593 CD2 TRP A 58 16.792 4.233 -2.565 1.00 15.00 A C +ATOM 594 NE1 TRP A 58 18.653 4.498 -3.782 1.00 15.00 A N +ATOM 595 HE1 TRP A 58 19.434 4.319 -4.346 1.00 15.00 A H +ATOM 596 CE2 TRP A 58 17.745 3.565 -3.357 1.00 15.00 A C +ATOM 597 CE3 TRP A 58 15.746 3.494 -2.002 1.00 15.00 A C +ATOM 598 CZ2 TRP A 58 17.684 2.194 -3.601 1.00 15.00 A C +ATOM 599 CZ3 TRP A 58 15.686 2.134 -2.246 1.00 15.00 A C +ATOM 600 CH2 TRP A 58 16.649 1.498 -3.038 1.00 15.00 A C +ATOM 601 C TRP A 58 14.286 6.506 -3.070 1.00 15.00 A C +ATOM 602 O TRP A 58 14.284 6.049 -4.212 1.00 15.00 A O +ATOM 603 N LEU A 59 13.341 6.210 -2.181 1.00 15.00 A N +ATOM 604 HN LEU A 59 13.380 6.610 -1.282 1.00 15.00 A H +ATOM 605 CA LEU A 59 12.246 5.300 -2.519 1.00 15.00 A C +ATOM 606 CB LEU A 59 11.711 4.570 -1.274 1.00 15.00 A C +ATOM 607 CG LEU A 59 10.920 5.402 -0.257 1.00 15.00 A C +ATOM 608 CD1 LEU A 59 9.430 5.116 -0.375 1.00 15.00 A C +ATOM 609 CD2 LEU A 59 11.405 5.128 1.158 1.00 15.00 A C +ATOM 610 C LEU A 59 11.122 6.023 -3.267 1.00 15.00 A C +ATOM 611 O LEU A 59 10.159 5.399 -3.715 1.00 15.00 A O +ATOM 612 N GLN A 60 11.262 7.341 -3.403 1.00 15.00 A N +ATOM 613 HN GLN A 60 12.054 7.777 -3.020 1.00 15.00 A H +ATOM 614 CA GLN A 60 10.266 8.158 -4.091 1.00 15.00 A C +ATOM 615 CB GLN A 60 10.164 9.537 -3.438 1.00 15.00 A C +ATOM 616 CG GLN A 60 9.417 9.586 -2.121 1.00 15.00 A C +ATOM 617 CD GLN A 60 9.264 11.007 -1.623 1.00 15.00 A C +ATOM 618 OE1 GLN A 60 10.143 11.538 -0.950 1.00 15.00 A O +ATOM 619 NE2 GLN A 60 8.143 11.632 -1.953 1.00 15.00 A N +ATOM 620 HE21 GLN A 60 7.483 11.146 -2.490 1.00 15.00 A H +ATOM 621 HE22 GLN A 60 8.022 12.553 -1.648 1.00 15.00 A H +ATOM 622 C GLN A 60 10.614 8.364 -5.561 1.00 15.00 A C +ATOM 623 O GLN A 60 9.948 7.842 -6.450 1.00 15.00 A O +ATOM 624 N TYR A 61 11.666 9.135 -5.809 1.00 15.00 A N +ATOM 625 HN TYR A 61 12.181 9.498 -5.054 1.00 15.00 A H +ATOM 626 CA TYR A 61 12.080 9.454 -7.172 1.00 15.00 A C +ATOM 627 CB TYR A 61 12.216 10.975 -7.321 1.00 15.00 A C +ATOM 628 CG TYR A 61 11.078 11.741 -6.674 1.00 15.00 A C +ATOM 629 CD1 TYR A 61 9.790 11.666 -7.187 1.00 15.00 A C +ATOM 630 CD2 TYR A 61 11.286 12.519 -5.540 1.00 15.00 A C +ATOM 631 CE1 TYR A 61 8.742 12.342 -6.593 1.00 15.00 A C +ATOM 632 CE2 TYR A 61 10.244 13.200 -4.939 1.00 15.00 A C +ATOM 633 CZ TYR A 61 8.975 13.108 -5.470 1.00 15.00 A C +ATOM 634 OH TYR A 61 7.932 13.778 -4.869 1.00 15.00 A O +ATOM 635 HH TYR A 61 7.149 13.222 -4.878 1.00 15.00 A H +ATOM 636 C TYR A 61 13.378 8.748 -7.557 1.00 15.00 A C +ATOM 637 O TYR A 61 14.084 9.173 -8.471 1.00 15.00 A O +ATOM 638 N GLY A 62 13.678 7.658 -6.865 1.00 15.00 A N +ATOM 639 HN GLY A 62 13.074 7.358 -6.151 1.00 15.00 A H +ATOM 640 CA GLY A 62 14.887 6.907 -7.153 1.00 15.00 A C +ATOM 641 C GLY A 62 16.098 7.438 -6.410 1.00 15.00 A C +ATOM 642 O GLY A 62 17.162 6.812 -6.423 1.00 15.00 A O +ATOM 643 H5T DA B 1 -4.135 -19.085 7.962 1.00 15.00 B H +ATOM 644 O5' DA B 1 -3.293 -18.715 8.237 1.00 15.00 B O +ATOM 645 C5' DA B 1 -2.264 -19.446 7.562 1.00 15.00 B C +ATOM 646 H5' DA B 1 -2.646 -20.421 7.259 1.00 15.00 B H +ATOM 647 H5'' DA B 1 -1.419 -19.594 8.235 1.00 15.00 B H +ATOM 648 C4' DA B 1 -1.785 -18.705 6.336 1.00 15.00 B C +ATOM 649 H4' DA B 1 -0.717 -18.905 6.236 1.00 15.00 B H +ATOM 650 O4' DA B 1 -2.548 -19.125 5.172 1.00 15.00 B O +ATOM 651 C1' DA B 1 -2.980 -17.965 4.479 1.00 15.00 B C +ATOM 652 H1' DA B 1 -2.190 -17.659 3.791 1.00 15.00 B H +ATOM 653 N9 DA B 1 -4.180 -18.306 3.716 1.00 15.00 B N +ATOM 654 C4 DA B 1 -4.283 -18.385 2.348 1.00 15.00 B C +ATOM 655 N3 DA B 1 -3.312 -18.178 1.442 1.00 15.00 B N +ATOM 656 C2 DA B 1 -3.781 -18.338 0.206 1.00 15.00 B C +ATOM 657 H2 DA B 1 -3.055 -18.186 -0.593 1.00 15.00 B H +ATOM 658 N1 DA B 1 -5.019 -18.656 -0.195 1.00 15.00 B N +ATOM 659 C6 DA B 1 -5.971 -18.855 0.741 1.00 15.00 B C +ATOM 660 N6 DA B 1 -7.204 -19.166 0.341 1.00 15.00 B N +ATOM 661 H61 DA B 1 -7.924 -19.316 1.033 1.00 15.00 B H +ATOM 662 H62 DA B 1 -7.424 -19.228 -0.647 1.00 15.00 B H +ATOM 663 C5 DA B 1 -5.600 -18.719 2.089 1.00 15.00 B C +ATOM 664 N7 DA B 1 -6.314 -18.855 3.271 1.00 15.00 B N +ATOM 665 C8 DA B 1 -5.428 -18.603 4.204 1.00 15.00 B C +ATOM 666 H8 DA B 1 -5.659 -18.628 5.259 1.00 15.00 B H +ATOM 667 C2' DA B 1 -3.187 -16.947 5.571 1.00 15.00 B C +ATOM 668 H2' DA B 1 -4.089 -17.134 6.153 1.00 15.00 B H +ATOM 669 H2'' DA B 1 -3.237 -15.926 5.191 1.00 15.00 B H +ATOM 670 C3' DA B 1 -1.932 -17.182 6.386 1.00 15.00 B C +ATOM 671 H3' DA B 1 -2.037 -16.830 7.412 1.00 15.00 B H +ATOM 672 O3' DA B 1 -0.803 -16.548 5.779 1.00 15.00 B O +ATOM 673 P DG B 2 -0.396 -15.058 6.223 1.00 15.00 B P +ATOM 674 O1P DG B 2 -0.048 -15.117 7.665 1.00 15.00 B O +ATOM 675 O2P DG B 2 -1.458 -14.128 5.760 1.00 15.00 B O +ATOM 676 O5' DG B 2 0.935 -14.762 5.400 1.00 15.00 B O +ATOM 677 C5' DG B 2 1.425 -15.698 4.441 1.00 15.00 B C +ATOM 678 H5' DG B 2 0.927 -16.658 4.580 1.00 15.00 B H +ATOM 679 H5'' DG B 2 2.499 -15.830 4.572 1.00 15.00 B H +ATOM 680 C4' DG B 2 1.155 -15.200 3.041 1.00 15.00 B C +ATOM 681 H4' DG B 2 1.922 -15.629 2.395 1.00 15.00 B H +ATOM 682 O4' DG B 2 -0.191 -15.571 2.635 1.00 15.00 B O +ATOM 683 C1' DG B 2 -0.831 -14.424 2.099 1.00 15.00 B C +ATOM 684 H1' DG B 2 -0.602 -14.364 1.033 1.00 15.00 B H +ATOM 685 N9 DG B 2 -2.271 -14.584 2.276 1.00 15.00 B N +ATOM 686 C4 DG B 2 -3.199 -14.809 1.289 1.00 15.00 B C +ATOM 687 N3 DG B 2 -2.939 -14.931 -0.028 1.00 15.00 B N +ATOM 688 C2 DG B 2 -4.038 -15.132 -0.733 1.00 15.00 B C +ATOM 689 N2 DG B 2 -3.951 -15.276 -2.059 1.00 15.00 B N +ATOM 690 H21 DG B 2 -3.048 -15.231 -2.508 1.00 15.00 B H +ATOM 691 H22 DG B 2 -4.782 -15.405 -2.626 1.00 15.00 B H +ATOM 692 N1 DG B 2 -5.298 -15.208 -0.188 1.00 15.00 B N +ATOM 693 H1 DG B 2 -6.082 -15.357 -0.813 1.00 15.00 B H +ATOM 694 C6 DG B 2 -5.590 -15.091 1.167 1.00 15.00 B C +ATOM 695 O6 DG B 2 -6.764 -15.182 1.556 1.00 15.00 B O +ATOM 696 C5 DG B 2 -4.415 -14.872 1.937 1.00 15.00 B C +ATOM 697 N7 DG B 2 -4.254 -14.698 3.305 1.00 15.00 B N +ATOM 698 C8 DG B 2 -2.968 -14.533 3.457 1.00 15.00 B C +ATOM 699 H8 DG B 2 -2.497 -14.371 4.420 1.00 15.00 B H +ATOM 700 C2' DG B 2 -0.225 -13.277 2.865 1.00 15.00 B C +ATOM 701 H2' DG B 2 -0.610 -13.204 3.882 1.00 15.00 B H +ATOM 702 H2'' DG B 2 -0.381 -12.315 2.376 1.00 15.00 B H +ATOM 703 C3' DG B 2 1.234 -13.683 2.846 1.00 15.00 B C +ATOM 704 H3' DG B 2 1.799 -13.213 3.652 1.00 15.00 B H +ATOM 705 O3' DG B 2 1.843 -13.362 1.591 1.00 15.00 B O +ATOM 706 P DT B 3 2.670 -11.991 1.433 1.00 15.00 B P +ATOM 707 O1P DT B 3 4.048 -12.236 1.928 1.00 15.00 B O +ATOM 708 O2P DT B 3 1.871 -10.885 2.014 1.00 15.00 B O +ATOM 709 O5' DT B 3 2.750 -11.768 -0.140 1.00 15.00 B O +ATOM 710 C5' DT B 3 2.175 -12.715 -1.039 1.00 15.00 B C +ATOM 711 H5' DT B 3 1.732 -13.533 -0.472 1.00 15.00 B H +ATOM 712 H5'' DT B 3 2.949 -13.113 -1.696 1.00 15.00 B H +ATOM 713 C4' DT B 3 1.104 -12.057 -1.877 1.00 15.00 B C +ATOM 714 H4' DT B 3 1.234 -12.411 -2.901 1.00 15.00 B H +ATOM 715 O4' DT B 3 -0.211 -12.359 -1.335 1.00 15.00 B O +ATOM 716 C1' DT B 3 -0.938 -11.144 -1.250 1.00 15.00 B C +ATOM 717 H1' DT B 3 -1.401 -10.958 -2.222 1.00 15.00 B H +ATOM 718 N1 DT B 3 -1.996 -11.290 -0.236 1.00 15.00 B N +ATOM 719 C6 DT B 3 -1.734 -11.056 1.098 1.00 15.00 B C +ATOM 720 H6 DT B 3 -0.717 -10.794 1.390 1.00 15.00 B H +ATOM 721 C2 DT B 3 -3.250 -11.648 -0.670 1.00 15.00 B C +ATOM 722 O2 DT B 3 -3.507 -11.880 -1.840 1.00 15.00 B O +ATOM 723 N3 DT B 3 -4.196 -11.726 0.316 1.00 15.00 B N +ATOM 724 H3 DT B 3 -5.135 -11.962 0.020 1.00 15.00 B H +ATOM 725 C4 DT B 3 -4.017 -11.494 1.663 1.00 15.00 B C +ATOM 726 O4 DT B 3 -4.972 -11.583 2.431 1.00 15.00 B O +ATOM 727 C5 DT B 3 -2.672 -11.138 2.053 1.00 15.00 B C +ATOM 728 C7 DT B 3 -2.386 -10.871 3.497 1.00 15.00 B C +ATOM 729 H71 DT B 3 -1.469 -11.390 3.222 1.00 15.00 B H +ATOM 730 H72 DT B 3 -2.290 -11.541 4.352 1.00 15.00 B H +ATOM 731 H73 DT B 3 -3.417 -11.218 3.576 1.00 15.00 B H +ATOM 732 C2' DT B 3 0.110 -10.110 -0.942 1.00 15.00 B C +ATOM 733 H2' DT B 3 0.472 -10.172 0.085 1.00 15.00 B H +ATOM 734 H2'' DT B 3 -0.233 -9.091 -1.119 1.00 15.00 B H +ATOM 735 C3' DT B 3 1.171 -10.527 -1.939 1.00 15.00 B C +ATOM 736 H3' DT B 3 2.160 -10.159 -1.670 1.00 15.00 B H +ATOM 737 O3' DT B 3 0.823 -10.062 -3.246 1.00 15.00 B O +ATOM 738 P DA B 4 1.272 -8.593 -3.717 1.00 15.00 B P +ATOM 739 O1P DA B 4 2.730 -8.637 -4.002 1.00 15.00 B O +ATOM 740 O2P DA B 4 0.746 -7.609 -2.737 1.00 15.00 B O +ATOM 741 O5' DA B 4 0.489 -8.383 -5.088 1.00 15.00 B O +ATOM 742 C5' DA B 4 -0.131 -9.486 -5.750 1.00 15.00 B C +ATOM 743 H5' DA B 4 -0.033 -10.381 -5.135 1.00 15.00 B H +ATOM 744 H5'' DA B 4 0.356 -9.658 -6.709 1.00 15.00 B H +ATOM 745 C4' DA B 4 -1.598 -9.206 -5.982 1.00 15.00 B C +ATOM 746 H4' DA B 4 -1.904 -9.808 -6.840 1.00 15.00 B H +ATOM 747 O4' DA B 4 -2.365 -9.511 -4.785 1.00 15.00 B O +ATOM 748 C1' DA B 4 -3.227 -8.417 -4.513 1.00 15.00 B C +ATOM 749 H1' DA B 4 -4.154 -8.560 -5.073 1.00 15.00 B H +ATOM 750 N9 DA B 4 -3.523 -8.409 -3.081 1.00 15.00 B N +ATOM 751 C4 DA B 4 -4.770 -8.374 -2.502 1.00 15.00 B C +ATOM 752 N3 DA B 4 -5.960 -8.356 -3.126 1.00 15.00 B N +ATOM 753 C2 DA B 4 -6.955 -8.316 -2.240 1.00 15.00 B C +ATOM 754 H2 DA B 4 -7.959 -8.299 -2.664 1.00 15.00 B H +ATOM 755 N1 DA B 4 -6.896 -8.292 -0.902 1.00 15.00 B N +ATOM 756 C6 DA B 4 -5.685 -8.312 -0.307 1.00 15.00 B C +ATOM 757 N6 DA B 4 -5.631 -8.289 1.027 1.00 15.00 B N +ATOM 758 H61 DA B 4 -4.729 -8.301 1.480 1.00 15.00 B H +ATOM 759 H62 DA B 4 -6.484 -8.281 1.574 1.00 15.00 B H +ATOM 760 C5 DA B 4 -4.550 -8.354 -1.136 1.00 15.00 B C +ATOM 761 N7 DA B 4 -3.191 -8.384 -0.857 1.00 15.00 B N +ATOM 762 C8 DA B 4 -2.628 -8.422 -2.041 1.00 15.00 B C +ATOM 763 H8 DA B 4 -1.557 -8.461 -2.186 1.00 15.00 B H +ATOM 764 C2' DA B 4 -2.461 -7.218 -5.009 1.00 15.00 B C +ATOM 765 H2' DA B 4 -1.641 -6.942 -4.347 1.00 15.00 B H +ATOM 766 H2'' DA B 4 -3.093 -6.340 -5.147 1.00 15.00 B H +ATOM 767 C3' DA B 4 -1.958 -7.759 -6.331 1.00 15.00 B C +ATOM 768 H3' DA B 4 -1.090 -7.211 -6.697 1.00 15.00 B H +ATOM 769 O3' DA B 4 -2.996 -7.724 -7.316 1.00 15.00 B O +ATOM 770 P DC B 5 -3.192 -6.411 -8.224 1.00 15.00 B P +ATOM 771 O1P DC B 5 -2.767 -6.759 -9.603 1.00 15.00 B O +ATOM 772 O2P DC B 5 -2.568 -5.255 -7.533 1.00 15.00 B O +ATOM 773 O5' DC B 5 -4.770 -6.194 -8.236 1.00 15.00 B O +ATOM 774 C5' DC B 5 -5.658 -7.306 -8.142 1.00 15.00 B C +ATOM 775 H5' DC B 5 -5.234 -8.056 -7.473 1.00 15.00 B H +ATOM 776 H5'' DC B 5 -5.797 -7.748 -9.129 1.00 15.00 B H +ATOM 777 C4' DC B 5 -7.000 -6.867 -7.603 1.00 15.00 B C +ATOM 778 H4' DC B 5 -7.742 -7.566 -7.992 1.00 15.00 B H +ATOM 779 O4' DC B 5 -6.970 -6.834 -6.151 1.00 15.00 B O +ATOM 780 C1' DC B 5 -7.515 -5.597 -5.718 1.00 15.00 B C +ATOM 781 H1' DC B 5 -8.598 -5.706 -5.637 1.00 15.00 B H +ATOM 782 N1 DC B 5 -6.959 -5.289 -4.393 1.00 15.00 B N +ATOM 783 C6 DC B 5 -5.609 -5.206 -4.204 1.00 15.00 B C +ATOM 784 H6 DC B 5 -4.937 -5.369 -5.047 1.00 15.00 B H +ATOM 785 C2 DC B 5 -7.836 -5.083 -3.328 1.00 15.00 B C +ATOM 786 O2 DC B 5 -9.056 -5.168 -3.530 1.00 15.00 B O +ATOM 787 N3 DC B 5 -7.333 -4.799 -2.106 1.00 15.00 B N +ATOM 788 C4 DC B 5 -6.012 -4.723 -1.929 1.00 15.00 B C +ATOM 789 N4 DC B 5 -5.561 -4.450 -0.703 1.00 15.00 B N +ATOM 790 H41 DC B 5 -4.564 -4.389 -0.531 1.00 15.00 B H +ATOM 791 H42 DC B 5 -6.217 -4.305 0.059 1.00 15.00 B H +ATOM 792 C5 DC B 5 -5.095 -4.928 -3.000 1.00 15.00 B C +ATOM 793 H5 DC B 5 -4.018 -4.860 -2.845 1.00 15.00 B H +ATOM 794 C2' DC B 5 -7.139 -4.630 -6.811 1.00 15.00 B C +ATOM 795 H2' DC B 5 -6.085 -4.356 -6.783 1.00 15.00 B H +ATOM 796 H2'' DC B 5 -7.727 -3.712 -6.784 1.00 15.00 B H +ATOM 797 C3' DC B 5 -7.470 -5.473 -8.026 1.00 15.00 B C +ATOM 798 H3' DC B 5 -6.948 -5.130 -8.920 1.00 15.00 B H +ATOM 799 O3' DC B 5 -8.879 -5.477 -8.266 1.00 15.00 B O +ATOM 800 P DA B 6 -9.533 -4.335 -9.186 1.00 15.00 B P +ATOM 801 O1P DA B 6 -10.413 -5.018 -10.167 1.00 15.00 B O +ATOM 802 O2P DA B 6 -8.450 -3.441 -9.667 1.00 15.00 B O +ATOM 803 O5' DA B 6 -10.450 -3.516 -8.173 1.00 15.00 B O +ATOM 804 C5' DA B 6 -10.963 -4.135 -6.995 1.00 15.00 B C +ATOM 805 H5' DA B 6 -10.161 -4.667 -6.483 1.00 15.00 B H +ATOM 806 H5'' DA B 6 -11.746 -4.843 -7.264 1.00 15.00 B H +ATOM 807 C4' DA B 6 -11.537 -3.094 -6.062 1.00 15.00 B C +ATOM 808 H4' DA B 6 -12.561 -3.397 -5.839 1.00 15.00 B H +ATOM 809 O4' DA B 6 -10.713 -2.986 -4.870 1.00 15.00 B O +ATOM 810 C1' DA B 6 -10.453 -1.613 -4.626 1.00 15.00 B C +ATOM 811 H1' DA B 6 -11.273 -1.208 -4.029 1.00 15.00 B H +ATOM 812 N9 DA B 6 -9.208 -1.517 -3.862 1.00 15.00 B N +ATOM 813 C4 DA B 6 -9.104 -1.434 -2.495 1.00 15.00 B C +ATOM 814 N3 DA B 6 -10.108 -1.425 -1.602 1.00 15.00 B N +ATOM 815 C2 DA B 6 -9.633 -1.334 -0.364 1.00 15.00 B C +ATOM 816 H2 DA B 6 -10.385 -1.320 0.425 1.00 15.00 B H +ATOM 817 N1 DA B 6 -8.363 -1.256 0.051 1.00 15.00 B N +ATOM 818 C6 DA B 6 -7.376 -1.268 -0.869 1.00 15.00 B C +ATOM 819 N6 DA B 6 -6.110 -1.191 -0.444 1.00 15.00 B N +ATOM 820 H61 DA B 6 -5.350 -1.198 -1.107 1.00 15.00 B H +ATOM 821 H62 DA B 6 -5.913 -1.109 0.548 1.00 15.00 B H +ATOM 822 C5 DA B 6 -7.749 -1.362 -2.222 1.00 15.00 B C +ATOM 823 N7 DA B 6 -7.010 -1.399 -3.397 1.00 15.00 B N +ATOM 824 C8 DA B 6 -7.919 -1.491 -4.338 1.00 15.00 B C +ATOM 825 H8 DA B 6 -7.677 -1.545 -5.389 1.00 15.00 B H +ATOM 826 C2' DA B 6 -10.418 -0.999 -6.002 1.00 15.00 B C +ATOM 827 H2' DA B 6 -9.503 -1.238 -6.544 1.00 15.00 B H +ATOM 828 H2'' DA B 6 -10.525 0.086 -5.983 1.00 15.00 B H +ATOM 829 C3' DA B 6 -11.629 -1.672 -6.619 1.00 15.00 B C +ATOM 830 H3' DA B 6 -11.588 -1.671 -7.709 1.00 15.00 B H +ATOM 831 O3' DA B 6 -12.842 -1.047 -6.187 1.00 15.00 B O +ATOM 832 P DA B 7 -13.728 -0.221 -7.241 1.00 15.00 B P +ATOM 833 O1P DA B 7 -13.512 -0.835 -8.575 1.00 15.00 B O +ATOM 834 O2P DA B 7 -13.439 1.222 -7.042 1.00 15.00 B O +ATOM 835 O5' DA B 7 -15.237 -0.496 -6.804 1.00 15.00 B O +ATOM 836 C5' DA B 7 -15.568 -1.568 -5.920 1.00 15.00 B C +ATOM 837 H5' DA B 7 -14.760 -2.299 -5.914 1.00 15.00 B H +ATOM 838 H5'' DA B 7 -16.485 -2.051 -6.258 1.00 15.00 B H +ATOM 839 C4' DA B 7 -15.771 -1.043 -4.519 1.00 15.00 B C +ATOM 840 H4' DA B 7 -16.358 -1.791 -3.984 1.00 15.00 B H +ATOM 841 O4' DA B 7 -14.486 -0.793 -3.892 1.00 15.00 B O +ATOM 842 C1' DA B 7 -14.508 0.502 -3.312 1.00 15.00 B C +ATOM 843 H1' DA B 7 -14.891 0.424 -2.293 1.00 15.00 B H +ATOM 844 N9 DA B 7 -13.137 1.001 -3.282 1.00 15.00 B N +ATOM 845 C4 DA B 7 -12.355 1.203 -2.171 1.00 15.00 B C +ATOM 846 N3 DA B 7 -12.690 0.989 -0.887 1.00 15.00 B N +ATOM 847 C2 DA B 7 -11.676 1.296 -0.081 1.00 15.00 B C +ATOM 848 H2 DA B 7 -11.864 1.153 0.983 1.00 15.00 B H +ATOM 849 N1 DA B 7 -10.457 1.755 -0.392 1.00 15.00 B N +ATOM 850 C6 DA B 7 -10.154 1.955 -1.693 1.00 15.00 B C +ATOM 851 N6 DA B 7 -8.941 2.406 -2.008 1.00 15.00 B N +ATOM 852 H61 DA B 7 -8.705 2.558 -2.977 1.00 15.00 B H +ATOM 853 H62 DA B 7 -8.252 2.574 -1.283 1.00 15.00 B H +ATOM 854 C5 DA B 7 -11.144 1.671 -2.644 1.00 15.00 B C +ATOM 855 N7 DA B 7 -11.160 1.768 -4.028 1.00 15.00 B N +ATOM 856 C8 DA B 7 -12.362 1.360 -4.356 1.00 15.00 B C +ATOM 857 H8 DA B 7 -12.711 1.310 -5.381 1.00 15.00 B H +ATOM 858 C2' DA B 7 -15.438 1.286 -4.202 1.00 15.00 B C +ATOM 859 H2' DA B 7 -14.977 1.560 -5.150 1.00 15.00 B H +ATOM 860 H2'' DA B 7 -15.803 2.198 -3.729 1.00 15.00 B H +ATOM 861 C3' DA B 7 -16.547 0.271 -4.390 1.00 15.00 B C +ATOM 862 H3' DA B 7 -17.141 0.471 -5.282 1.00 15.00 B H +ATOM 863 O3' DA B 7 -17.401 0.243 -3.243 1.00 15.00 B O +ATOM 864 P DA B 8 -18.719 1.160 -3.206 1.00 15.00 B P +ATOM 865 O1P DA B 8 -19.886 0.261 -3.399 1.00 15.00 B O +ATOM 866 O2P DA B 8 -18.523 2.316 -4.120 1.00 15.00 B O +ATOM 867 O5' DA B 8 -18.757 1.714 -1.714 1.00 15.00 B O +ATOM 868 C5' DA B 8 -18.209 0.951 -0.640 1.00 15.00 B C +ATOM 869 H5' DA B 8 -17.503 0.220 -1.035 1.00 15.00 B H +ATOM 870 H5'' DA B 8 -19.009 0.428 -0.118 1.00 15.00 B H +ATOM 871 C4' DA B 8 -17.489 1.855 0.333 1.00 15.00 B C +ATOM 872 H4' DA B 8 -17.484 1.343 1.297 1.00 15.00 B H +ATOM 873 O4' DA B 8 -16.151 2.158 -0.146 1.00 15.00 B O +ATOM 874 C1' DA B 8 -15.948 3.559 -0.049 1.00 15.00 B C +ATOM 875 H1' DA B 8 -15.589 3.787 0.957 1.00 15.00 B H +ATOM 876 N9 DA B 8 -14.926 3.939 -1.023 1.00 15.00 B N +ATOM 877 C4 DA B 8 -13.630 4.294 -0.740 1.00 15.00 B C +ATOM 878 N3 DA B 8 -13.049 4.364 0.470 1.00 15.00 B N +ATOM 879 C2 DA B 8 -11.780 4.746 0.358 1.00 15.00 B C +ATOM 880 H2 DA B 8 -11.232 4.829 1.298 1.00 15.00 B H +ATOM 881 N1 DA B 8 -11.079 5.041 -0.743 1.00 15.00 B N +ATOM 882 C6 DA B 8 -11.691 4.957 -1.942 1.00 15.00 B C +ATOM 883 N6 DA B 8 -10.986 5.243 -3.038 1.00 15.00 B N +ATOM 884 H61 DA B 8 -11.439 5.179 -3.939 1.00 15.00 B H +ATOM 885 H62 DA B 8 -10.012 5.510 -2.969 1.00 15.00 B H +ATOM 886 C5 DA B 8 -13.039 4.567 -1.961 1.00 15.00 B C +ATOM 887 N7 DA B 8 -13.947 4.389 -2.996 1.00 15.00 B N +ATOM 888 C8 DA B 8 -15.047 4.017 -2.388 1.00 15.00 B C +ATOM 889 H8 DA B 8 -15.964 3.790 -2.913 1.00 15.00 B H +ATOM 890 C2' DA B 8 -17.315 4.141 -0.291 1.00 15.00 B C +ATOM 891 H2' DA B 8 -17.615 4.086 -1.337 1.00 15.00 B H +ATOM 892 H2'' DA B 8 -17.396 5.180 0.030 1.00 15.00 B H +ATOM 893 C3' DA B 8 -18.142 3.218 0.582 1.00 15.00 B C +ATOM 894 H3' DA B 8 -19.193 3.213 0.297 1.00 15.00 B H +ATOM 895 O3' DA B 8 -18.020 3.583 1.959 1.00 15.00 B O +ATOM 896 P DC B 9 -19.205 4.396 2.677 1.00 15.00 B P +ATOM 897 O1P DC B 9 -20.104 3.395 3.307 1.00 15.00 B O +ATOM 898 O2P DC B 9 -19.762 5.381 1.715 1.00 15.00 B O +ATOM 899 O5' DC B 9 -18.465 5.193 3.840 1.00 15.00 B O +ATOM 900 C5' DC B 9 -17.292 4.661 4.453 1.00 15.00 B C +ATOM 901 H5' DC B 9 -16.869 3.883 3.818 1.00 15.00 B H +ATOM 902 H5'' DC B 9 -17.547 4.233 5.421 1.00 15.00 B H +ATOM 903 C4' DC B 9 -16.264 5.751 4.649 1.00 15.00 B C +ATOM 904 H4' DC B 9 -15.691 5.492 5.541 1.00 15.00 B H +ATOM 905 O4' DC B 9 -15.431 5.886 3.466 1.00 15.00 B O +ATOM 906 C1' DC B 9 -15.359 7.263 3.127 1.00 15.00 B C +ATOM 907 H1' DC B 9 -14.549 7.717 3.701 1.00 15.00 B H +ATOM 908 N1 DC B 9 -15.055 7.371 1.692 1.00 15.00 B N +ATOM 909 C6 DC B 9 -16.037 7.224 0.753 1.00 15.00 B C +ATOM 910 H6 DC B 9 -17.059 7.016 1.071 1.00 15.00 B H +ATOM 911 C2 DC B 9 -13.742 7.631 1.306 1.00 15.00 B C +ATOM 912 O2 DC B 9 -12.873 7.752 2.180 1.00 15.00 B O +ATOM 913 N3 DC B 9 -13.450 7.744 -0.010 1.00 15.00 B N +ATOM 914 C4 DC B 9 -14.414 7.602 -0.921 1.00 15.00 B C +ATOM 915 N4 DC B 9 -14.077 7.723 -2.207 1.00 15.00 B N +ATOM 916 H41 DC B 9 -14.778 7.622 -2.924 1.00 15.00 B H +ATOM 917 H42 DC B 9 -13.113 7.911 -2.464 1.00 15.00 B H +ATOM 918 C5 DC B 9 -15.765 7.330 -0.555 1.00 15.00 B C +ATOM 919 H5 DC B 9 -16.544 7.211 -1.307 1.00 15.00 B H +ATOM 920 C2' DC B 9 -16.701 7.806 3.542 1.00 15.00 B C +ATOM 921 H2' DC B 9 -17.502 7.503 2.869 1.00 15.00 B H +ATOM 922 H2'' DC B 9 -16.713 8.894 3.614 1.00 15.00 B H +ATOM 923 C3' DC B 9 -16.830 7.151 4.903 1.00 15.00 B C +ATOM 924 H3' DC B 9 -17.865 7.103 5.239 1.00 15.00 B H +ATOM 925 O3' DC B 9 -16.043 7.846 5.874 1.00 15.00 B O +ATOM 926 P DT B 10 -16.755 8.861 6.896 1.00 15.00 B P +ATOM 927 O1P DT B 10 -17.412 8.046 7.949 1.00 15.00 B O +ATOM 928 O2P DT B 10 -17.561 9.829 6.106 1.00 15.00 B O +ATOM 929 O5' DT B 10 -15.542 9.647 7.565 1.00 15.00 B O +ATOM 930 C5' DT B 10 -14.221 9.109 7.539 1.00 15.00 B C +ATOM 931 H5' DT B 10 -14.206 8.204 6.932 1.00 15.00 B H +ATOM 932 H5'' DT B 10 -13.903 8.865 8.552 1.00 15.00 B H +ATOM 933 C4' DT B 10 -13.260 10.113 6.946 1.00 15.00 B C +ATOM 934 H4' DT B 10 -12.283 9.923 7.397 1.00 15.00 B H +ATOM 935 O4' DT B 10 -13.239 10.000 5.497 1.00 15.00 B O +ATOM 936 C1' DT B 10 -13.370 11.303 4.949 1.00 15.00 B C +ATOM 937 H1' DT B 10 -12.376 11.751 4.889 1.00 15.00 B H +ATOM 938 N1 DT B 10 -13.929 11.182 3.591 1.00 15.00 B N +ATOM 939 C6 DT B 10 -15.275 10.949 3.405 1.00 15.00 B C +ATOM 940 H6 DT B 10 -15.911 10.845 4.285 1.00 15.00 B H +ATOM 941 C2 DT B 10 -13.068 11.314 2.529 1.00 15.00 B C +ATOM 942 O2 DT B 10 -11.873 11.515 2.665 1.00 15.00 B O +ATOM 943 N3 DT B 10 -13.657 11.203 1.298 1.00 15.00 B N +ATOM 944 H3 DT B 10 -13.047 11.307 0.496 1.00 15.00 B H +ATOM 945 C4 DT B 10 -14.991 10.975 1.031 1.00 15.00 B C +ATOM 946 O4 DT B 10 -15.378 10.897 -0.133 1.00 15.00 B O +ATOM 947 C5 DT B 10 -15.838 10.843 2.194 1.00 15.00 B C +ATOM 948 C7 DT B 10 -17.302 10.608 2.000 1.00 15.00 B C +ATOM 949 H71 DT B 10 -17.724 11.414 1.400 1.00 15.00 B H +ATOM 950 H72 DT B 10 -17.798 10.581 2.971 1.00 15.00 B H +ATOM 951 H73 DT B 10 -17.454 9.657 1.489 1.00 15.00 B H +ATOM 952 C2' DT B 10 -14.240 12.028 5.941 1.00 15.00 B C +ATOM 953 H2' DT B 10 -15.283 11.715 5.891 1.00 15.00 B H +ATOM 954 H2'' DT B 10 -14.205 13.111 5.822 1.00 15.00 B H +ATOM 955 C3' DT B 10 -13.581 11.582 7.231 1.00 15.00 B C +ATOM 956 H3' DT B 10 -14.242 11.687 8.090 1.00 15.00 B H +ATOM 957 O3' DT B 10 -12.379 12.324 7.456 1.00 15.00 B O +ATOM 958 P DT B 11 -12.385 13.558 8.484 1.00 15.00 B P +ATOM 959 O1P DT B 11 -12.340 12.999 9.859 1.00 15.00 B O +ATOM 960 O2P DT B 11 -13.482 14.485 8.102 1.00 15.00 B O +ATOM 961 O5' DT B 11 -10.999 14.289 8.205 1.00 15.00 B O +ATOM 962 C5' DT B 11 -10.006 13.679 7.382 1.00 15.00 B C +ATOM 963 H5' DT B 11 -10.395 12.748 6.969 1.00 15.00 B H +ATOM 964 H5'' DT B 11 -9.120 13.462 7.977 1.00 15.00 B H +ATOM 965 C4' DT B 11 -9.628 14.603 6.248 1.00 15.00 B C +ATOM 966 H4' DT B 11 -8.561 14.460 6.065 1.00 15.00 B H +ATOM 967 O4' DT B 11 -10.440 14.325 5.075 1.00 15.00 B O +ATOM 968 C1' DT B 11 -10.942 15.556 4.579 1.00 15.00 B C +ATOM 969 H1' DT B 11 -10.197 15.987 3.909 1.00 15.00 B H +ATOM 970 N1 DT B 11 -12.174 15.277 3.818 1.00 15.00 B N +ATOM 971 C6 DT B 11 -13.359 15.019 4.476 1.00 15.00 B C +ATOM 972 H6 DT B 11 -13.360 15.026 5.566 1.00 15.00 B H +ATOM 973 C2 DT B 11 -12.099 15.282 2.445 1.00 15.00 B C +ATOM 974 O2 DT B 11 -11.067 15.511 1.836 1.00 15.00 B O +ATOM 975 N3 DT B 11 -13.281 15.011 1.807 1.00 15.00 B N +ATOM 976 H3 DT B 11 -13.255 15.008 0.795 1.00 15.00 B H +ATOM 977 C4 DT B 11 -14.501 14.743 2.393 1.00 15.00 B C +ATOM 978 O4 DT B 11 -15.479 14.505 1.688 1.00 15.00 B O +ATOM 979 C5 DT B 11 -14.508 14.758 3.837 1.00 15.00 B C +ATOM 980 C7 DT B 11 -15.794 14.492 4.555 1.00 15.00 B C +ATOM 981 H71 DT B 11 -15.589 14.282 5.605 1.00 15.00 B H +ATOM 982 H72 DT B 11 -16.292 13.635 4.103 1.00 15.00 B H +ATOM 983 H73 DT B 11 -16.440 15.368 4.480 1.00 15.00 B H +ATOM 984 C2' DT B 11 -11.123 16.397 5.816 1.00 15.00 B C +ATOM 985 H2' DT B 11 -11.983 16.089 6.411 1.00 15.00 B H +ATOM 986 H2'' DT B 11 -11.229 17.459 5.594 1.00 15.00 B H +ATOM 987 C3' DT B 11 -9.815 16.098 6.521 1.00 15.00 B C +ATOM 988 H3' DT B 11 -9.868 16.301 7.591 1.00 15.00 B H +ATOM 989 O3' DT B 11 -8.741 16.849 5.945 1.00 15.00 B O +ATOM 990 P DA B 28 -15.679 16.500 -9.223 1.00 15.00 B P +ATOM 991 O1P DA B 28 -16.205 17.250 -10.393 1.00 15.00 B O +ATOM 992 O2P DA B 28 -16.178 15.127 -8.947 1.00 15.00 B O +ATOM 993 O5' DA B 28 -14.093 16.437 -9.348 1.00 15.00 B O +ATOM 994 C5' DA B 28 -13.287 17.562 -9.002 1.00 15.00 B C +ATOM 995 H5' DA B 28 -13.910 18.333 -8.547 1.00 15.00 B H +ATOM 996 H5'' DA B 28 -12.818 17.966 -9.898 1.00 15.00 B H +ATOM 997 C4' DA B 28 -12.213 17.154 -8.021 1.00 15.00 B C +ATOM 998 H4' DA B 28 -11.431 17.914 -8.071 1.00 15.00 B H +ATOM 999 O4' DA B 28 -12.769 17.009 -6.686 1.00 15.00 B O +ATOM 1000 C1' DA B 28 -12.324 15.772 -6.150 1.00 15.00 B C +ATOM 1001 H1' DA B 28 -11.350 15.931 -5.682 1.00 15.00 B H +ATOM 1002 N9 DA B 28 -13.283 15.343 -5.132 1.00 15.00 B N +ATOM 1003 C4 DA B 28 -13.082 15.353 -3.773 1.00 15.00 B C +ATOM 1004 N3 DA B 28 -11.982 15.757 -3.113 1.00 15.00 B N +ATOM 1005 C2 DA B 28 -12.147 15.622 -1.799 1.00 15.00 B C +ATOM 1006 H2 DA B 28 -11.296 15.926 -1.188 1.00 15.00 B H +ATOM 1007 N1 DA B 28 -13.207 15.165 -1.120 1.00 15.00 B N +ATOM 1008 C6 DA B 28 -14.296 14.767 -1.813 1.00 15.00 B C +ATOM 1009 N6 DA B 28 -15.351 14.311 -1.136 1.00 15.00 B N +ATOM 1010 H61 DA B 28 -16.169 14.010 -1.645 1.00 15.00 B H +ATOM 1011 H62 DA B 28 -15.339 14.271 -0.123 1.00 15.00 B H +ATOM 1012 C5 DA B 28 -14.248 14.859 -3.216 1.00 15.00 B C +ATOM 1013 N7 DA B 28 -15.170 14.545 -4.205 1.00 15.00 B N +ATOM 1014 C8 DA B 28 -14.550 14.850 -5.320 1.00 15.00 B C +ATOM 1015 H8 DA B 28 -14.996 14.726 -6.297 1.00 15.00 B H +ATOM 1016 C2' DA B 28 -12.218 14.874 -7.353 1.00 15.00 B C +ATOM 1017 H2' DA B 28 -13.192 14.559 -7.727 1.00 15.00 B H +ATOM 1018 H2'' DA B 28 -11.623 13.981 -7.163 1.00 15.00 B H +ATOM 1019 C3' DA B 28 -11.524 15.819 -8.315 1.00 15.00 B C +ATOM 1020 H3' DA B 28 -11.659 15.521 -9.355 1.00 15.00 B H +ATOM 1021 O3' DA B 28 -10.127 15.904 -8.020 1.00 15.00 B O +ATOM 1022 P DA B 29 -9.084 14.991 -8.832 1.00 15.00 B P +ATOM 1023 O1P DA B 29 -8.291 15.900 -9.699 1.00 15.00 B O +ATOM 1024 O2P DA B 29 -9.824 13.857 -9.440 1.00 15.00 B O +ATOM 1025 O5' DA B 29 -8.120 14.416 -7.703 1.00 15.00 B O +ATOM 1026 C5' DA B 29 -7.338 15.293 -6.893 1.00 15.00 B C +ATOM 1027 H5' DA B 29 -7.894 16.212 -6.708 1.00 15.00 B H +ATOM 1028 H5'' DA B 29 -6.408 15.535 -7.407 1.00 15.00 B H +ATOM 1029 C4' DA B 29 -7.016 14.636 -5.571 1.00 15.00 B C +ATOM 1030 H4' DA B 29 -6.319 15.296 -5.050 1.00 15.00 B H +ATOM 1031 O4' DA B 29 -8.238 14.407 -4.818 1.00 15.00 B O +ATOM 1032 C1' DA B 29 -8.232 13.069 -4.345 1.00 15.00 B C +ATOM 1033 H1' DA B 29 -7.710 13.042 -3.387 1.00 15.00 B H +ATOM 1034 N9 DA B 29 -9.619 12.647 -4.155 1.00 15.00 B N +ATOM 1035 C4 DA B 29 -10.267 12.460 -2.957 1.00 15.00 B C +ATOM 1036 N3 DA B 29 -9.768 12.634 -1.721 1.00 15.00 B N +ATOM 1037 C2 DA B 29 -10.687 12.353 -0.799 1.00 15.00 B C +ATOM 1038 H2 DA B 29 -10.362 12.469 0.236 1.00 15.00 B H +ATOM 1039 N1 DA B 29 -11.955 11.949 -0.961 1.00 15.00 B N +ATOM 1040 C6 DA B 29 -12.424 11.784 -2.216 1.00 15.00 B C +ATOM 1041 N6 DA B 29 -13.685 11.379 -2.377 1.00 15.00 B N +ATOM 1042 H61 DA B 29 -14.038 11.253 -3.315 1.00 15.00 B H +ATOM 1043 H62 DA B 29 -14.278 11.201 -1.575 1.00 15.00 B H +ATOM 1044 C5 DA B 29 -11.548 12.050 -3.282 1.00 15.00 B C +ATOM 1045 N7 DA B 29 -11.708 11.984 -4.659 1.00 15.00 B N +ATOM 1046 C8 DA B 29 -10.540 12.348 -5.129 1.00 15.00 B C +ATOM 1047 H8 DA B 29 -10.321 12.409 -6.186 1.00 15.00 B H +ATOM 1048 C2' DA B 29 -7.479 12.313 -5.411 1.00 15.00 B C +ATOM 1049 H2' DA B 29 -8.071 12.160 -6.312 1.00 15.00 B H +ATOM 1050 H2'' DA B 29 -7.128 11.340 -5.068 1.00 15.00 B H +ATOM 1051 C3' DA B 29 -6.329 13.271 -5.651 1.00 15.00 B C +ATOM 1052 H3' DA B 29 -5.867 13.118 -6.627 1.00 15.00 B H +ATOM 1053 O3' DA B 29 -5.333 13.144 -4.629 1.00 15.00 B O +ATOM 1054 P DG B 30 -4.004 12.284 -4.914 1.00 15.00 B P +ATOM 1055 O1P DG B 30 -2.970 13.206 -5.449 1.00 15.00 B O +ATOM 1056 O2P DG B 30 -4.377 11.073 -5.689 1.00 15.00 B O +ATOM 1057 O5' DG B 30 -3.532 11.812 -3.469 1.00 15.00 B O +ATOM 1058 C5' DG B 30 -3.801 12.605 -2.314 1.00 15.00 B C +ATOM 1059 H5' DG B 30 -4.564 13.347 -2.550 1.00 15.00 B H +ATOM 1060 H5'' DG B 30 -2.891 13.116 -2.000 1.00 15.00 B H +ATOM 1061 C4' DG B 30 -4.294 11.730 -1.186 1.00 15.00 B C +ATOM 1062 H4' DG B 30 -4.062 12.247 -0.254 1.00 15.00 B H +ATOM 1063 O4' DG B 30 -5.715 11.470 -1.346 1.00 15.00 B O +ATOM 1064 C1' DG B 30 -5.932 10.077 -1.187 1.00 15.00 B C +ATOM 1065 H1' DG B 30 -6.060 9.868 -0.123 1.00 15.00 B H +ATOM 1066 N9 DG B 30 -7.156 9.721 -1.896 1.00 15.00 B N +ATOM 1067 C4 DG B 30 -8.348 9.353 -1.324 1.00 15.00 B C +ATOM 1068 N3 DG B 30 -8.594 9.266 -0.001 1.00 15.00 B N +ATOM 1069 C2 DG B 30 -9.833 8.881 0.247 1.00 15.00 B C +ATOM 1070 N2 DG B 30 -10.238 8.748 1.515 1.00 15.00 B N +ATOM 1071 H21 DG B 30 -9.594 8.943 2.267 1.00 15.00 B H +ATOM 1072 H22 DG B 30 -11.184 8.452 1.732 1.00 15.00 B H +ATOM 1073 N1 DG B 30 -10.760 8.601 -0.728 1.00 15.00 B N +ATOM 1074 H1 DG B 30 -11.687 8.307 -0.438 1.00 15.00 B H +ATOM 1075 C6 DG B 30 -10.531 8.686 -2.098 1.00 15.00 B C +ATOM 1076 O6 DG B 30 -11.442 8.414 -2.893 1.00 15.00 B O +ATOM 1077 C5 DG B 30 -9.199 9.099 -2.378 1.00 15.00 B C +ATOM 1078 N7 DG B 30 -8.556 9.309 -3.591 1.00 15.00 B N +ATOM 1079 C8 DG B 30 -7.350 9.678 -3.255 1.00 15.00 B C +ATOM 1080 H8 DG B 30 -6.581 9.926 -3.972 1.00 15.00 B H +ATOM 1081 C2' DG B 30 -4.675 9.448 -1.725 1.00 15.00 B C +ATOM 1082 H2' DG B 30 -4.622 9.481 -2.813 1.00 15.00 B H +ATOM 1083 H2'' DG B 30 -4.555 8.410 -1.412 1.00 15.00 B H +ATOM 1084 C3' DG B 30 -3.640 10.349 -1.083 1.00 15.00 B C +ATOM 1085 H3' DG B 30 -2.688 10.322 -1.613 1.00 15.00 B H +ATOM 1086 O3' DG B 30 -3.435 9.984 0.285 1.00 15.00 B O +ATOM 1087 P DT B 31 -2.351 8.856 0.653 1.00 15.00 B P +ATOM 1088 O1P DT B 31 -1.014 9.501 0.723 1.00 15.00 B O +ATOM 1089 O2P DT B 31 -2.560 7.697 -0.253 1.00 15.00 B O +ATOM 1090 O5' DT B 31 -2.750 8.396 2.124 1.00 15.00 B O +ATOM 1091 C5' DT B 31 -3.781 9.064 2.852 1.00 15.00 B C +ATOM 1092 H5' DT B 31 -4.291 9.773 2.199 1.00 15.00 B H +ATOM 1093 H5'' DT B 31 -3.346 9.603 3.694 1.00 15.00 B H +ATOM 1094 C4' DT B 31 -4.783 8.061 3.371 1.00 15.00 B C +ATOM 1095 H4' DT B 31 -5.172 8.455 4.311 1.00 15.00 B H +ATOM 1096 O4' DT B 31 -5.824 7.846 2.383 1.00 15.00 B O +ATOM 1097 C1' DT B 31 -6.007 6.449 2.220 1.00 15.00 B C +ATOM 1098 H1' DT B 31 -6.730 6.103 2.960 1.00 15.00 B H +ATOM 1099 N1 DT B 31 -6.542 6.216 0.871 1.00 15.00 B N +ATOM 1100 C6 DT B 31 -5.742 6.390 -0.238 1.00 15.00 B C +ATOM 1101 H6 DT B 31 -4.708 6.704 -0.089 1.00 15.00 B H +ATOM 1102 C2 DT B 31 -7.851 5.826 0.759 1.00 15.00 B C +ATOM 1103 O2 DT B 31 -8.580 5.667 1.724 1.00 15.00 B O +ATOM 1104 N3 DT B 31 -8.281 5.628 -0.525 1.00 15.00 B N +ATOM 1105 H3 DT B 31 -9.247 5.349 -0.639 1.00 15.00 B H +ATOM 1106 C4 DT B 31 -7.545 5.778 -1.682 1.00 15.00 B C +ATOM 1107 O4 DT B 31 -8.068 5.557 -2.772 1.00 15.00 B O +ATOM 1108 C5 DT B 31 -6.175 6.193 -1.490 1.00 15.00 B C +ATOM 1109 C7 DT B 31 -5.301 6.384 -2.689 1.00 15.00 B C +ATOM 1110 H71 DT B 31 -4.303 6.681 -2.367 1.00 15.00 B H +ATOM 1111 H72 DT B 31 -5.239 5.449 -3.246 1.00 15.00 B H +ATOM 1112 H73 DT B 31 -5.722 7.160 -3.327 1.00 15.00 B H +ATOM 1113 C2' DT B 31 -4.638 5.872 2.466 1.00 15.00 B C +ATOM 1114 H2' DT B 31 -3.957 6.039 1.632 1.00 15.00 B H +ATOM 1115 H2'' DT B 31 -4.663 4.801 2.672 1.00 15.00 B H +ATOM 1116 C3' DT B 31 -4.234 6.669 3.690 1.00 15.00 B C +ATOM 1117 H3' DT B 31 -3.153 6.692 3.823 1.00 15.00 B H +ATOM 1118 O3' DT B 31 -4.848 6.137 4.869 1.00 15.00 B O +ATOM 1119 P DT B 32 -3.972 5.263 5.894 1.00 15.00 B P +ATOM 1120 O1P DT B 32 -3.316 6.213 6.827 1.00 15.00 B O +ATOM 1121 O2P DT B 32 -3.139 4.329 5.097 1.00 15.00 B O +ATOM 1122 O5' DT B 32 -5.040 4.408 6.711 1.00 15.00 B O +ATOM 1123 C5' DT B 32 -6.256 4.995 7.180 1.00 15.00 B C +ATOM 1124 H5' DT B 32 -6.481 5.886 6.594 1.00 15.00 B H +ATOM 1125 H5'' DT B 32 -6.150 5.273 8.228 1.00 15.00 B H +ATOM 1126 C4' DT B 32 -7.394 4.011 7.038 1.00 15.00 B C +ATOM 1127 H4' DT B 32 -8.190 4.349 7.704 1.00 15.00 B H +ATOM 1128 O4' DT B 32 -7.818 3.952 5.651 1.00 15.00 B O +ATOM 1129 C1' DT B 32 -7.914 2.591 5.259 1.00 15.00 B C +ATOM 1130 H1' DT B 32 -8.920 2.228 5.478 1.00 15.00 B H +ATOM 1131 N1 DT B 32 -7.665 2.521 3.809 1.00 15.00 B N +ATOM 1132 C6 DT B 32 -6.372 2.634 3.345 1.00 15.00 B C +ATOM 1133 H6 DT B 32 -5.572 2.774 4.071 1.00 15.00 B H +ATOM 1134 C2 DT B 32 -8.726 2.345 2.950 1.00 15.00 B C +ATOM 1135 O2 DT B 32 -9.882 2.246 3.327 1.00 15.00 B O +ATOM 1136 N3 DT B 32 -8.380 2.288 1.623 1.00 15.00 B N +ATOM 1137 H3 DT B 32 -9.138 2.155 0.965 1.00 15.00 B H +ATOM 1138 C4 DT B 32 -7.111 2.391 1.089 1.00 15.00 B C +ATOM 1139 O4 DT B 32 -6.948 2.316 -0.125 1.00 15.00 B O +ATOM 1140 C5 DT B 32 -6.052 2.579 2.047 1.00 15.00 B C +ATOM 1141 C7 DT B 32 -4.647 2.707 1.554 1.00 15.00 B C +ATOM 1142 H71 DT B 32 -3.955 2.524 2.376 1.00 15.00 B H +ATOM 1143 H72 DT B 32 -4.489 3.712 1.162 1.00 15.00 B H +ATOM 1144 H73 DT B 32 -4.472 1.977 0.762 1.00 15.00 B H +ATOM 1145 C2' DT B 32 -6.864 1.901 6.094 1.00 15.00 B C +ATOM 1146 H2' DT B 32 -5.854 2.081 5.727 1.00 15.00 B H +ATOM 1147 H2'' DT B 32 -7.014 0.823 6.148 1.00 15.00 B H +ATOM 1148 C3' DT B 32 -7.097 2.563 7.436 1.00 15.00 B C +ATOM 1149 H3' DT B 32 -6.216 2.505 8.075 1.00 15.00 B H +ATOM 1150 O3' DT B 32 -8.211 1.965 8.112 1.00 15.00 B O +ATOM 1151 P DT B 33 -7.949 0.813 9.206 1.00 15.00 B P +ATOM 1152 O1P DT B 33 -7.753 1.478 10.518 1.00 15.00 B O +ATOM 1153 O2P DT B 33 -6.900 -0.102 8.688 1.00 15.00 B O +ATOM 1154 O5' DT B 33 -9.319 0.002 9.271 1.00 15.00 B O +ATOM 1155 C5' DT B 33 -10.530 0.559 8.762 1.00 15.00 B C +ATOM 1156 H5' DT B 33 -10.321 1.521 8.294 1.00 15.00 B H +ATOM 1157 H5'' DT B 33 -11.238 0.705 9.578 1.00 15.00 B H +ATOM 1158 C4' DT B 33 -11.140 -0.367 7.736 1.00 15.00 B C +ATOM 1159 H4' DT B 33 -12.212 -0.164 7.722 1.00 15.00 B H +ATOM 1160 O4' DT B 33 -10.515 -0.153 6.443 1.00 15.00 B O +ATOM 1161 C1' DT B 33 -10.154 -1.415 5.902 1.00 15.00 B C +ATOM 1162 H1' DT B 33 -11.006 -1.811 5.346 1.00 15.00 B H +ATOM 1163 N1 DT B 33 -9.024 -1.209 4.981 1.00 15.00 B N +ATOM 1164 C6 DT B 33 -7.741 -1.105 5.475 1.00 15.00 B C +ATOM 1165 H6 DT B 33 -7.591 -1.187 6.551 1.00 15.00 B H +ATOM 1166 C2 DT B 33 -9.285 -1.122 3.634 1.00 15.00 B C +ATOM 1167 O2 DT B 33 -10.407 -1.212 3.165 1.00 15.00 B O +ATOM 1168 N3 DT B 33 -8.179 -0.924 2.851 1.00 15.00 B N +ATOM 1169 H3 DT B 33 -8.336 -0.881 1.851 1.00 15.00 B H +ATOM 1170 C4 DT B 33 -6.872 -0.800 3.271 1.00 15.00 B C +ATOM 1171 O4 DT B 33 -5.985 -0.562 2.454 1.00 15.00 B O +ATOM 1172 C5 DT B 33 -6.671 -0.907 4.694 1.00 15.00 B C +ATOM 1173 C7 DT B 33 -5.286 -0.777 5.237 1.00 15.00 B C +ATOM 1174 H71 DT B 33 -4.711 -0.097 4.609 1.00 15.00 B H +ATOM 1175 H72 DT B 33 -5.330 -0.384 6.253 1.00 15.00 B H +ATOM 1176 H73 DT B 33 -4.807 -1.756 5.247 1.00 15.00 B H +ATOM 1177 C2' DT B 33 -9.839 -2.253 7.113 1.00 15.00 B C +ATOM 1178 H2' DT B 33 -8.879 -1.999 7.561 1.00 15.00 B H +ATOM 1179 H2'' DT B 33 -9.837 -3.322 6.897 1.00 15.00 B H +ATOM 1180 C3' DT B 33 -10.997 -1.866 8.011 1.00 15.00 B C +ATOM 1181 H3' DT B 33 -10.781 -2.057 9.062 1.00 15.00 B H +ATOM 1182 O3' DT B 33 -12.186 -2.570 7.633 1.00 15.00 B O +ATOM 1183 P DG B 34 -12.509 -3.997 8.298 1.00 15.00 B P +ATOM 1184 O1P DG B 34 -13.374 -3.752 9.480 1.00 15.00 B O +ATOM 1185 O2P DG B 34 -11.233 -4.739 8.463 1.00 15.00 B O +ATOM 1186 O5' DG B 34 -13.377 -4.755 7.199 1.00 15.00 B O +ATOM 1187 C5' DG B 34 -14.413 -4.082 6.486 1.00 15.00 B C +ATOM 1188 H5' DG B 34 -14.182 -3.019 6.423 1.00 15.00 B H +ATOM 1189 H5'' DG B 34 -15.361 -4.212 7.009 1.00 15.00 B H +ATOM 1190 C4' DG B 34 -14.534 -4.647 5.090 1.00 15.00 B C +ATOM 1191 H4' DG B 34 -15.502 -4.326 4.701 1.00 15.00 B H +ATOM 1192 O4' DG B 34 -13.424 -4.190 4.272 1.00 15.00 B O +ATOM 1193 C1' DG B 34 -12.868 -5.309 3.599 1.00 15.00 B C +ATOM 1194 H1' DG B 34 -13.404 -5.455 2.660 1.00 15.00 B H +ATOM 1195 N9 DG B 34 -11.468 -5.013 3.319 1.00 15.00 B N +ATOM 1196 C4 DG B 34 -10.872 -4.913 2.084 1.00 15.00 B C +ATOM 1197 N3 DG B 34 -11.483 -5.071 0.890 1.00 15.00 B N +ATOM 1198 C2 DG B 34 -10.641 -4.918 -0.120 1.00 15.00 B C +ATOM 1199 N2 DG B 34 -11.082 -5.049 -1.379 1.00 15.00 B N +ATOM 1200 H21 DG B 34 -12.054 -5.263 -1.547 1.00 15.00 B H +ATOM 1201 H22 DG B 34 -10.448 -4.948 -2.163 1.00 15.00 B H +ATOM 1202 N1 DG B 34 -9.308 -4.628 0.031 1.00 15.00 B N +ATOM 1203 H1 DG B 34 -8.742 -4.530 -0.805 1.00 15.00 B H +ATOM 1204 C6 DG B 34 -8.661 -4.456 1.250 1.00 15.00 B C +ATOM 1205 O6 DG B 34 -7.454 -4.191 1.274 1.00 15.00 B O +ATOM 1206 C5 DG B 34 -9.548 -4.623 2.340 1.00 15.00 B C +ATOM 1207 N7 DG B 34 -9.316 -4.544 3.706 1.00 15.00 B N +ATOM 1208 C8 DG B 34 -10.480 -4.779 4.244 1.00 15.00 B C +ATOM 1209 H8 DG B 34 -10.652 -4.786 5.310 1.00 15.00 B H +ATOM 1210 C2' DG B 34 -13.089 -6.452 4.555 1.00 15.00 B C +ATOM 1211 H2' DG B 34 -12.406 -6.426 5.404 1.00 15.00 B H +ATOM 1212 H2'' DG B 34 -12.994 -7.427 4.076 1.00 15.00 B H +ATOM 1213 C3' DG B 34 -14.517 -6.173 4.977 1.00 15.00 B C +ATOM 1214 H3' DG B 34 -14.762 -6.643 5.929 1.00 15.00 B H +ATOM 1215 O3' DG B 34 -15.437 -6.617 3.976 1.00 15.00 B O +ATOM 1216 P DT B 35 -16.000 -8.122 4.023 1.00 15.00 B P +ATOM 1217 O1P DT B 35 -17.405 -8.054 4.497 1.00 15.00 B O +ATOM 1218 O2P DT B 35 -15.022 -8.972 4.748 1.00 15.00 B O +ATOM 1219 O5' DT B 35 -16.013 -8.573 2.496 1.00 15.00 B O +ATOM 1220 C5' DT B 35 -15.896 -7.608 1.450 1.00 15.00 B C +ATOM 1221 H5' DT B 35 -15.388 -6.722 1.827 1.00 15.00 B H +ATOM 1222 H5'' DT B 35 -16.889 -7.329 1.099 1.00 15.00 B H +ATOM 1223 C4' DT B 35 -15.105 -8.180 0.296 1.00 15.00 B C +ATOM 1224 H4' DT B 35 -15.590 -7.843 -0.622 1.00 15.00 B H +ATOM 1225 O4' DT B 35 -13.717 -7.760 0.385 1.00 15.00 B O +ATOM 1226 C1' DT B 35 -12.893 -8.902 0.211 1.00 15.00 B C +ATOM 1227 H1' DT B 35 -12.739 -9.052 -0.860 1.00 15.00 B H +ATOM 1228 N1 DT B 35 -11.592 -8.639 0.852 1.00 15.00 B N +ATOM 1229 C6 DT B 35 -11.474 -8.622 2.226 1.00 15.00 B C +ATOM 1230 H6 DT B 35 -12.367 -8.806 2.825 1.00 15.00 B H +ATOM 1231 C2 DT B 35 -10.508 -8.415 0.036 1.00 15.00 B C +ATOM 1232 O2 DT B 35 -10.580 -8.420 -1.182 1.00 15.00 B O +ATOM 1233 N3 DT B 35 -9.331 -8.181 0.697 1.00 15.00 B N +ATOM 1234 H3 DT B 35 -8.514 -8.026 0.120 1.00 15.00 B H +ATOM 1235 C4 DT B 35 -9.135 -8.146 2.062 1.00 15.00 B C +ATOM 1236 O4 DT B 35 -8.014 -7.909 2.514 1.00 15.00 B O +ATOM 1237 C5 DT B 35 -10.316 -8.390 2.860 1.00 15.00 B C +ATOM 1238 C7 DT B 35 -10.197 -8.371 4.352 1.00 15.00 B C +ATOM 1239 H71 DT B 35 -11.183 -8.824 4.253 1.00 15.00 B H +ATOM 1240 H72 DT B 35 -10.735 -7.808 5.114 1.00 15.00 B H +ATOM 1241 H73 DT B 35 -9.836 -7.366 4.127 1.00 15.00 B H +ATOM 1242 C2' DT B 35 -13.698 -10.026 0.809 1.00 15.00 B C +ATOM 1243 H2' DT B 35 -13.713 -9.996 1.898 1.00 15.00 B H +ATOM 1244 H2'' DT B 35 -13.343 -11.011 0.503 1.00 15.00 B H +ATOM 1245 C3' DT B 35 -15.057 -9.709 0.219 1.00 15.00 B C +ATOM 1246 H3' DT B 35 -15.870 -10.162 0.787 1.00 15.00 B H +ATOM 1247 O3' DT B 35 -15.127 -10.141 -1.143 1.00 15.00 B O +ATOM 1248 P DA B 36 -15.814 -11.549 -1.498 1.00 15.00 B P +ATOM 1249 O1P DA B 36 -17.249 -11.287 -1.779 1.00 15.00 B O +ATOM 1250 O2P DA B 36 -15.435 -12.536 -0.454 1.00 15.00 B O +ATOM 1251 O5' DA B 36 -15.115 -11.981 -2.862 1.00 15.00 B O +ATOM 1252 C5' DA B 36 -14.606 -11.000 -3.763 1.00 15.00 B C +ATOM 1253 H5' DA B 36 -14.479 -10.053 -3.238 1.00 15.00 B H +ATOM 1254 H5'' DA B 36 -15.307 -10.861 -4.585 1.00 15.00 B H +ATOM 1255 C4' DA B 36 -13.272 -11.441 -4.317 1.00 15.00 B C +ATOM 1256 H4' DA B 36 -13.146 -10.938 -5.277 1.00 15.00 B H +ATOM 1257 O4' DA B 36 -12.204 -11.134 -3.380 1.00 15.00 B O +ATOM 1258 C1' DA B 36 -11.394 -12.290 -3.231 1.00 15.00 B C +ATOM 1259 H1' DA B 36 -10.639 -12.288 -4.020 1.00 15.00 B H +ATOM 1260 N9 DA B 36 -10.734 -12.218 -1.928 1.00 15.00 B N +ATOM 1261 C4 DA B 36 -9.386 -12.080 -1.703 1.00 15.00 B C +ATOM 1262 N3 DA B 36 -8.409 -11.978 -2.621 1.00 15.00 B N +ATOM 1263 C2 DA B 36 -7.221 -11.861 -2.031 1.00 15.00 B C +ATOM 1264 H2 DA B 36 -6.369 -11.775 -2.707 1.00 15.00 B H +ATOM 1265 N1 DA B 36 -6.922 -11.836 -0.726 1.00 15.00 B N +ATOM 1266 C6 DA B 36 -7.926 -11.939 0.169 1.00 15.00 B C +ATOM 1267 N6 DA B 36 -7.627 -11.914 1.468 1.00 15.00 B N +ATOM 1268 H61 DA B 36 -8.376 -11.992 2.140 1.00 15.00 B H +ATOM 1269 H62 DA B 36 -6.667 -11.827 1.782 1.00 15.00 B H +ATOM 1270 C5 DA B 36 -9.233 -12.069 -0.328 1.00 15.00 B C +ATOM 1271 N7 DA B 36 -10.463 -12.194 0.304 1.00 15.00 B N +ATOM 1272 C8 DA B 36 -11.318 -12.277 -0.687 1.00 15.00 B C +ATOM 1273 H8 DA B 36 -12.384 -12.378 -0.541 1.00 15.00 B H +ATOM 1274 C2' DA B 36 -12.362 -13.432 -3.395 1.00 15.00 B C +ATOM 1275 H2' DA B 36 -13.007 -13.563 -2.526 1.00 15.00 B H +ATOM 1276 H2'' DA B 36 -11.865 -14.382 -3.594 1.00 15.00 B H +ATOM 1277 C3' DA B 36 -13.136 -12.939 -4.602 1.00 15.00 B C +ATOM 1278 H3' DA B 36 -14.111 -13.416 -4.688 1.00 15.00 B H +ATOM 1279 O3' DA B 36 -12.390 -13.154 -5.803 1.00 15.00 B O +ATOM 1280 P DC B 37 -12.780 -14.374 -6.775 1.00 15.00 B P +ATOM 1281 O1P DC B 37 -13.708 -13.838 -7.803 1.00 15.00 B O +ATOM 1282 O2P DC B 37 -13.193 -15.534 -5.944 1.00 15.00 B O +ATOM 1283 O5' DC B 37 -11.408 -14.735 -7.496 1.00 15.00 B O +ATOM 1284 C5' DC B 37 -10.443 -13.722 -7.773 1.00 15.00 B C +ATOM 1285 H5' DC B 37 -10.667 -12.835 -7.181 1.00 15.00 B H +ATOM 1286 H5'' DC B 37 -10.476 -13.463 -8.831 1.00 15.00 B H +ATOM 1287 C4' DC B 37 -9.057 -14.210 -7.425 1.00 15.00 B C +ATOM 1288 H4' DC B 37 -8.355 -13.645 -8.041 1.00 15.00 B H +ATOM 1289 O4' DC B 37 -8.805 -14.058 -6.002 1.00 15.00 B O +ATOM 1290 C1' DC B 37 -8.269 -15.278 -5.519 1.00 15.00 B C +ATOM 1291 H1' DC B 37 -7.190 -15.267 -5.687 1.00 15.00 B H +ATOM 1292 N1 DC B 37 -8.527 -15.361 -4.076 1.00 15.00 B N +ATOM 1293 C6 DC B 37 -9.791 -15.551 -3.593 1.00 15.00 B C +ATOM 1294 H6 DC B 37 -10.625 -15.637 -4.290 1.00 15.00 B H +ATOM 1295 C2 DC B 37 -7.449 -15.247 -3.205 1.00 15.00 B C +ATOM 1296 O2 DC B 37 -6.317 -15.073 -3.677 1.00 15.00 B O +ATOM 1297 N3 DC B 37 -7.662 -15.330 -1.874 1.00 15.00 B N +ATOM 1298 C4 DC B 37 -8.898 -15.515 -1.407 1.00 15.00 B C +ATOM 1299 N4 DC B 37 -9.058 -15.590 -0.083 1.00 15.00 B N +ATOM 1300 H41 DC B 37 -9.977 -15.732 0.307 1.00 15.00 B H +ATOM 1301 H42 DC B 37 -8.257 -15.489 0.530 1.00 15.00 B H +ATOM 1302 C5 DC B 37 -10.023 -15.631 -2.277 1.00 15.00 B C +ATOM 1303 H5 DC B 37 -11.030 -15.777 -1.885 1.00 15.00 B H +ATOM 1304 C2' DC B 37 -8.941 -16.329 -6.358 1.00 15.00 B C +ATOM 1305 H2' DC B 37 -9.987 -16.475 -6.090 1.00 15.00 B H +ATOM 1306 H2'' DC B 37 -8.439 -17.297 -6.306 1.00 15.00 B H +ATOM 1307 C3' DC B 37 -8.790 -15.688 -7.721 1.00 15.00 B C +ATOM 1308 H3' DC B 37 -9.494 -16.085 -8.451 1.00 15.00 B H +ATOM 1309 O3' DC B 37 -7.454 -15.864 -8.194 1.00 15.00 B O +ATOM 1310 P DT B 38 -7.132 -17.006 -9.273 1.00 15.00 B P +ATOM 1311 O1P DT B 38 -7.965 -16.754 -10.475 1.00 15.00 B O +ATOM 1312 O2P DT B 38 -7.206 -18.324 -8.588 1.00 15.00 B O +ATOM 1313 O5' DT B 38 -5.610 -16.726 -9.650 1.00 15.00 B O +ATOM 1314 C5' DT B 38 -4.834 -15.803 -8.886 1.00 15.00 B C +ATOM 1315 H5' DT B 38 -5.493 -15.203 -8.260 1.00 15.00 B H +ATOM 1316 H5'' DT B 38 -4.280 -15.146 -9.556 1.00 15.00 B H +ATOM 1317 C4' DT B 38 -3.860 -16.547 -8.004 1.00 15.00 B C +ATOM 1318 H4' DT B 38 -2.921 -15.990 -8.025 1.00 15.00 B H +ATOM 1319 O4' DT B 38 -4.397 -16.690 -6.660 1.00 15.00 B O +ATOM 1320 C1' DT B 38 -4.245 -18.045 -6.261 1.00 15.00 B C +ATOM 1321 H1' DT B 38 -3.247 -18.166 -5.833 1.00 15.00 B H +ATOM 1322 N1 DT B 38 -5.253 -18.339 -5.225 1.00 15.00 B N +ATOM 1323 C6 DT B 38 -6.564 -18.595 -5.571 1.00 15.00 B C +ATOM 1324 H6 DT B 38 -6.839 -18.570 -6.626 1.00 15.00 B H +ATOM 1325 C2 DT B 38 -4.840 -18.350 -3.915 1.00 15.00 B C +ATOM 1326 O2 DT B 38 -3.688 -18.132 -3.580 1.00 15.00 B O +ATOM 1327 N3 DT B 38 -5.827 -18.631 -3.006 1.00 15.00 B N +ATOM 1328 H3 DT B 38 -5.553 -18.636 -2.031 1.00 15.00 B H +ATOM 1329 C4 DT B 38 -7.153 -18.899 -3.274 1.00 15.00 B C +ATOM 1330 O4 DT B 38 -7.928 -19.137 -2.352 1.00 15.00 B O +ATOM 1331 C5 DT B 38 -7.517 -18.874 -4.670 1.00 15.00 B C +ATOM 1332 C7 DT B 38 -8.932 -19.176 -5.049 1.00 15.00 B C +ATOM 1333 H71 DT B 38 -9.357 -19.880 -4.333 1.00 15.00 B H +ATOM 1334 H72 DT B 38 -9.514 -18.256 -5.043 1.00 15.00 B H +ATOM 1335 H73 DT B 38 -8.956 -19.615 -6.047 1.00 15.00 B H +ATOM 1336 C2' DT B 38 -4.391 -18.818 -7.545 1.00 15.00 B C +ATOM 1337 H2' DT B 38 -5.423 -18.859 -7.895 1.00 15.00 B H +ATOM 1338 H2'' DT B 38 -4.018 -19.841 -7.468 1.00 15.00 B H +ATOM 1339 H3' DT B 38 -3.754 -18.121 -9.508 1.00 15.00 B H +ATOM 1340 C3' DT B 38 -3.521 -17.970 -8.450 1.00 15.00 B C +ATOM 1341 O3' DT B 38 -2.129 -18.270 -8.281 1.00 15.00 B O +ATOM 1342 H3T DT B 38 -1.772 -18.470 -9.150 1.00 15.00 B H +END diff --git a/examples/analysis/dnascan-test.cfg b/examples/analysis/dnascan-test.cfg new file mode 100644 index 0000000000..bfc2590d1f --- /dev/null +++ b/examples/analysis/dnascan-test.cfg @@ -0,0 +1,33 @@ +# ==================================================================== +# DNA base-pair scan with HADDOCK3 +# +# This example workflow performs a DNA base-pair scan of all DNA +# nucleotides at the interface. As DNA is double stranded, each +# mutation is performed as a Watson-Crick double mutation: the +# selected interface nucleotide and its base-pairing partner are +# mutated together to preserve a valid base pair (e.g. A:T -> G:C). +# ==================================================================== + +# General parameters +run_dir = "run1-dna-scan" +ncores = 10 + +# Input +molecules = ["data/protdna_complex_1.pdb"] + +# Workflow definition +# ==================================================================== +[topoaa] +autohis = true + +[emscoring] + +[dnascan] +# Output the PDB file containing tested mutations +output_mutants = true +# Generate a plot of the energetics +plot = true +# Do not split the plots for each energetical component +splitplot = false +# Manually define the interaction cutoff used to detect the interface +int_cutoff = 3.9 diff --git a/integration_tests/test_alascan.py b/integration_tests/test_alascan.py index 5a91a233ee..74b2d6f1e5 100644 --- a/integration_tests/test_alascan.py +++ b/integration_tests/test_alascan.py @@ -9,7 +9,7 @@ DEFAULT_CONFIG as DEFAULT_ALASCAN_CONFIG, HaddockModule as AlascanModule, ) -from haddock.modules.analysis.alascan.scan import RES_CODES +from haddock.modules.analysis.alascan.alascan import RES_CODES from haddock.libs.libio import read_from_yaml from haddock.libs.libontology import PDBFile from . import GOLDEN_DATA diff --git a/src/haddock/libs/libscan.py b/src/haddock/libs/libscan.py index 1e306ed059..163b513654 100644 --- a/src/haddock/libs/libscan.py +++ b/src/haddock/libs/libscan.py @@ -9,20 +9,231 @@ import io import os +import shutil from contextlib import redirect_stdout from dataclasses import dataclass from pathlib import Path -from typing import Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union import numpy as np import pandas as pd from haddock import log from haddock.clis import cli_score +from haddock.core.exceptions import ConfigurationError from haddock.libs.libontology import PDBFile from haddock.libs.libplots import make_alascan_plot, make_rnascan_plot +# --------------------------------------------------------------------------- +# Nucleic-acid mutation helpers (shared by the rnascan and dnascan modules) +# --------------------------------------------------------------------------- +# The two nucleic-acid scan modules mutate bases the same way; only the residue +# naming (one-letter RNA vs two-letter DNA) and the backbone atom set (RNA keeps +# the 2'-hydroxyl O2') differ. The chemistry-invariant pieces live here so both +# modules share a single implementation. + +# Base ring atoms shared by both purines. Preserving them when mutating one +# purine into the other keeps the base plane and glycosidic orientation, so CNS +# only has to rebuild the differing substituents. +PURINE_BASE_ATOMS = ["N9", "C8", "N7", "C5", "C4", "N3", "C2", "N1", "C6"] + +# Base ring atoms shared by both pyrimidines. Same rationale as for the purines. +PYRIMIDINE_BASE_ATOMS = ["N1", "C2", "O2", "N3", "C4", "C5", "C6"] + +# Glycosidic-region anchor atoms kept (and renamed) on cross-type mutations +# (purine <-> pyrimidine). Because the purine and pyrimidine ring systems do not +# share atom names, these three atoms are preserved and renamed to their +# counterpart in the target ring so that CNS rebuilds the new base with the +# correct glycosidic orientation. The correspondence is: +# pyrimidine N1 <-> purine N9 (glycosidic nitrogen) +# pyrimidine C2 <-> purine C4 +# pyrimidine C6 <-> purine C8 +_PYRIMIDINE_TO_PURINE_ANCHORS = {"N1": "N9", "C2": "C4", "C6": "C8"} +_PURINE_TO_PYRIMIDINE_ANCHORS = {"N9": "N1", "C4": "C2", "C8": "C6"} + + +def norm_atom_name(atom_name: str) -> str: + """Normalise atom names so that primes are always written as `'`.""" + return atom_name.replace("*", "'") + + +def get_atoms_to_keep( + ori_resname: str, + target_resname: str, + *, + backbone_atoms: List[str], + purines: Tuple[str, ...], + pyrimidines: Tuple[str, ...], +) -> Dict[str, str]: + """Return the atoms to preserve when mutating one nucleic-acid base into + another. + + The result maps each atom name to keep (as found in the original residue) to + the atom name it must be written with in the mutated residue. Backbone atoms + and same-ring-type base atoms keep their name (mapped to themselves). + + The sugar-phosphate backbone (``backbone_atoms``) is always kept. When the + original and target bases are of the same ring type (both purines or both + pyrimidines) the base ring atoms common to that type are kept as well, so + that the base orientation is preserved and CNS only rebuilds the differing + substituents. For cross-type mutations (purine <-> pyrimidine) the three + glycosidic-region anchor atoms are kept and renamed to their counterpart in + the target ring system. + + Parameters + ---------- + ori_resname : str + Original (wild-type) residue name. + target_resname : str + Target base residue name. + backbone_atoms : list of str + Sugar-phosphate backbone atoms to always keep (RNA keeps ``O2'``, DNA + does not). + purines, pyrimidines : tuple of str + Residue names classified as purines / pyrimidines for this module. + + Returns + ------- + dict of str -> str + Mapping of original atom name -> atom name to write for the mutated + nucleotide. + """ + # Backbone is always kept, with unchanged atom names. + atoms_to_keep: Dict[str, str] = {atom: atom for atom in backbone_atoms} + if ori_resname in purines and target_resname in purines: + atoms_to_keep.update({atom: atom for atom in PURINE_BASE_ATOMS}) + elif ori_resname in pyrimidines and target_resname in pyrimidines: + atoms_to_keep.update({atom: atom for atom in PYRIMIDINE_BASE_ATOMS}) + elif ori_resname in pyrimidines and target_resname in purines: + atoms_to_keep.update(_PYRIMIDINE_TO_PURINE_ANCHORS) + elif ori_resname in purines and target_resname in pyrimidines: + atoms_to_keep.update(_PURINE_TO_PYRIMIDINE_ANCHORS) + return atoms_to_keep + + +def validate_scan_bases( + scan_bases: List[str], + allowed: Tuple[str, ...], + *, + base_kind: str, + allowed_note: str, +) -> List[str]: + """Validate and normalise a list of target nucleic-acid bases. + + Entries are upper-cased and de-duplicated (keeping first-seen order) and + must belong to ``allowed``. + + Parameters + ---------- + scan_bases : list of str + Target bases requested by the user. + allowed : tuple of str + Canonical residue names accepted by the calling module. + base_kind : str + Human-readable base family (e.g. ``"RNA"`` or ``"DNA"``) used in the + "empty list" error message. + allowed_note : str + Sentence describing the accepted values (and why the other naming is + rejected) used in the "invalid entry" error message. + + Returns + ------- + list of str + Normalised, de-duplicated list of canonical residue names, in the order + first seen. + + Raises + ------ + ConfigurationError + If ``scan_bases`` is empty or contains an unrecognised base. + """ + if not scan_bases: + raise ConfigurationError( + f"'scan_bases' must contain at least one {base_kind} base " + f"among {', '.join(allowed)}." + ) + normalised: List[str] = [] + for base in scan_bases: + canonical = str(base).strip().upper() + if canonical not in allowed: + raise ConfigurationError( + f"Invalid 'scan_bases' entry {base!r}. Allowed values are the " + f"{allowed_note}" + ) + if canonical not in normalised: + normalised.append(canonical) + return normalised + + +def filter_interface( + interface: Dict[str, List[int]], + filter_resdic: Dict[str, List[int]], + user_chains: List[str], +) -> Dict[str, List[int]]: + """Restrict the detected interface to the user-selected residues/chains. + + If the user provided target residues (``resdic_*``), the interface is + intersected with them. Otherwise, if the user provided target chains, the + interface is restricted to those chains. With neither, the interface is + returned unchanged. + + Parameters + ---------- + interface : dict + Mapping of chain id to its list of interface residue numbers. + filter_resdic : dict + Mapping of chain id to user-requested residue numbers. The sentinel + ``{"_": []}`` means "no residue restriction". + user_chains : list of str + User-requested chain ids (used only when no residue restriction is set). + + Returns + ------- + dict + The filtered interface. + """ + # if user defined target residues, check they are in the interface + if filter_resdic != {"_": []}: + filtered_interface = {} + for chain in filter_resdic: + if chain in interface: + # intersection of user queried residues and interface residues + user_res_valid = list( + set(filter_resdic[chain]).intersection(set(interface[chain])) + ) + # keep the chain only if at least one residue survives + if user_res_valid: + filtered_interface[chain] = user_res_valid + return filtered_interface + # if (user defined target chains) & (no user target residues) - use them + if user_chains: + return {chain: res for chain, res in interface.items() if chain in user_chains} + return interface + + +def build_resname_dict(coords: Dict[Tuple, object]) -> Dict[str, str]: + """Map ``"{chain}-{resid}"`` to its residue name from a coordinate dict. + + Parameters + ---------- + coords : dict + Coordinate dictionary keyed by ``(chain, resid, atom_name, resname)`` + tuples, as returned by ``load_coords(..., add_resname=True)``. + + Returns + ------- + dict + Mapping of ``"{chain}-{resid}"`` to the residue name (first occurrence). + """ + resname_dict: Dict[str, str] = {} + for chain, resid, _atom, resname in coords.keys(): + key = f"{chain}-{resid}" + if key not in resname_dict: + resname_dict[key] = resname + return resname_dict + + def make_scan_plot( df: pd.DataFrame, clt_id, @@ -722,3 +933,192 @@ def group_scan_by_cluster( entry["delta_bsa"].append(delta_scores[4]) entry["frac_pr"] += 1 return clt_scan, clt_pops + + +# --------------------------------------------------------------------------- +# Shared interface-scan job infrastructure +# --------------------------------------------------------------------------- + + +class BaseInterfaceScanner: + """Common state shared by every module's ``InterfaceScanner``. + + Subclasses add their own mutation target (e.g. ``mutation_res`` for alascan + or ``scan_bases`` for the nucleic-acid modules) and implement ``run()``, + which detects the interface and builds the module-specific mutation jobs. + """ + + def __init__( + self, + model: Union[str, Path, Any], + params: Optional[Dict[str, Any]] = None, + ) -> None: + """Populate the attributes shared by all InterfaceScanner variants. + + Parameters + ---------- + model : str, Path, or model object + HADDOCK ``PDBFile`` model object or a path to a PDB file. + params : dict, optional + Module parameters (interface cutoff, chains, resdic filters, + ligand topology/parameter files, ...). + """ + self.model = model + self.params = params or {} + self.point_mutations_jobs: List[Any] = [] + self.ligand_param_fname = self.params.get("ligand_param_fname", "") + self.ligand_top_fname = self.params.get("ligand_top_fname", "") + self.filter_resdic = { + key[-1]: value + for key, value in self.params.items() + if key.startswith("resdic") + } + if isinstance(model, PDBFile): + self.model_path = model.rel_path + self.model_id = model.file_name.removesuffix(".pdb") + else: + # model given as a plain path + self.model_path = Path(model) + self.model_id = self.model_path.stem + + +class ModelPointMutation: + """Execute a single point mutation and score it against the native model. + + Shared by the alascan and rnascan modules, whose per-model scoring flow is + identical; only the ``mutate`` primitive differs (protein residue vs RNA + base). Each module keeps a thin ``run()`` that forwards its own module-level + ``mutate`` and ``calc_score`` to :meth:`_run` (so both stay patchable in the + module namespace for tests). + """ + + def __init__( + self, + model_path: Path, + model_id: str, + chain: str, + resid: int, + ori_resname: str, + target_resname: str, + native_scores: Tuple[float, float, float, float, float], + output_mutants: bool = False, + ligand_param_fname: Union[Path, str] = "", + ligand_top_fname: Union[Path, str] = "", + ) -> None: + """Initialize a single point mutation job. + + Parameters + ---------- + model_path : Path + Path to the PDB file. + model_id : str + Identifier for the model. + chain : str + Chain identifier. + resid : int + Residue number. + ori_resname : str + Original residue name. + target_resname : str + Target residue/base name for the mutation. + native_scores : tuple + Native model scores (score, vdw, elec, desolv, bsa). + output_mutants : bool + Whether to keep the mutant PDB files. + ligand_param_fname, ligand_top_fname : Union[Path, str] + Additional CNS parameter/topology files. + """ + self.model_path = Path(model_path) + self.model_id = model_id + self.chain = chain + self.resid = resid + self.ori_resname = ori_resname + self.target_resname = target_resname + self.native_scores = native_scores + self.output_mutants = output_mutants + self.ligand_param_fname = ligand_param_fname + self.ligand_top_fname = ligand_top_fname + + def _run(self, mutate_func, calc_score_func) -> MutationResult: + """Run the mutation with the given ``mutate``/``calc_score`` callables. + + Parameters + ---------- + mutate_func : callable + ``mutate(model_path, chain, resid, target_resname) -> Path`` that + writes the mutated PDB and returns its path. + calc_score_func : callable + The module's ``calc_score`` (kept as an argument so it stays + patchable in the module namespace during tests). + """ + mutation_id = f"{self.model_id}_{self.chain}{self.resid}{self.target_resname}" + + try: + # Setup working directory + sc_dir = f"haddock3-score-{mutation_id}" + os.makedirs(sc_dir, exist_ok=True) + + # Perform point mutation on pdb file + mut_pdb = mutate_func( + self.model_path, self.chain, self.resid, self.target_resname + ) + + # Calculate mutant scores + mutant_scores = calc_score_func( + mut_pdb, + run_dir=sc_dir, + outputpdb=self.output_mutants, + ligand_param_fname=self.ligand_param_fname, + ligand_top_fname=self.ligand_top_fname, + ) + + # Calculate deltas (native - mutant) + n_score, n_vdw, n_elec, n_des, n_bsa = self.native_scores + m_score, m_vdw, m_elec, m_des, m_bsa = mutant_scores + delta_scores = ( + n_score - m_score, + n_vdw - m_vdw, + n_elec - m_elec, + n_des - m_des, + n_bsa - m_bsa, + ) + + # Handle output files + em_mut_pdb = Path(f"{mut_pdb.stem}_hs.pdb") + if not self.output_mutants: + # if output_mutants = False, then remove both files + if os.path.exists(mut_pdb): + os.remove(mut_pdb) + if em_mut_pdb.exists(): + os.remove(em_mut_pdb) + else: + # otherwise keep energy-minimized pdb + if os.path.exists(em_mut_pdb): + shutil.move(em_mut_pdb, mut_pdb) + # clean up scoring dir + if os.path.exists(sc_dir): + shutil.rmtree(sc_dir) + + return MutationResult( + model_id=self.model_id, + chain=self.chain, + resid=self.resid, + ori_resname=self.ori_resname, + target_resname=self.target_resname, + mutant_scores=mutant_scores, + delta_scores=delta_scores, + success=True, + ) + + except Exception as e: + return MutationResult( + model_id=self.model_id, + chain=self.chain, + resid=self.resid, + ori_resname=self.ori_resname, + target_resname=self.target_resname, + mutant_scores=(0, 0, 0, 0, 0), + delta_scores=(0, 0, 0, 0, 0), + success=False, + error_msg=str(e), + ) diff --git a/src/haddock/modules/analysis/alascan/__init__.py b/src/haddock/modules/analysis/alascan/__init__.py index d3dba1b287..825cb33bfb 100644 --- a/src/haddock/modules/analysis/alascan/__init__.py +++ b/src/haddock/modules/analysis/alascan/__init__.py @@ -55,7 +55,7 @@ from haddock.libs.libparallel import GenericTask from haddock.modules import BaseHaddockModule, get_engine from haddock.modules.analysis import get_analysis_exec_mode -from haddock.modules.analysis.alascan.scan import ( +from haddock.modules.analysis.alascan.alascan import ( AddDeltaBFactor, ClusterOutputer, group_scan_by_cluster, diff --git a/src/haddock/modules/analysis/alascan/scan.py b/src/haddock/modules/analysis/alascan/alascan.py similarity index 56% rename from src/haddock/modules/analysis/alascan/scan.py rename to src/haddock/modules/analysis/alascan/alascan.py index 18574e3de2..c485f6cb73 100644 --- a/src/haddock/modules/analysis/alascan/scan.py +++ b/src/haddock/modules/analysis/alascan/alascan.py @@ -3,20 +3,23 @@ import os import shutil from pathlib import Path -from typing import Tuple, Dict +from typing import Dict from haddock import log from haddock.core.typing import Any, Optional, Union from haddock.libs.libalign import get_atoms, load_coords -from haddock.libs.libontology import PDBFile from haddock.libs.libscan import ( - MutationResult, + MutationResult, # noqa: F401 re-exported for the module's public API calc_score, AddDeltaBFactor as _AddDeltaBFactor, ClusterOutputer as _ClusterOutputer, write_scan_out as _write_scan_out, group_scan_by_cluster as _group_scan_by_cluster, + BaseInterfaceScanner as _BaseInterfaceScanner, + ModelPointMutation as _ModelPointMutation, + filter_interface, + build_resname_dict, ) from haddock.libs.libcapri import CAPRI @@ -164,7 +167,7 @@ def write_scan_out(results, model_id): ) -class InterfaceScanner: +class InterfaceScanner(_BaseInterfaceScanner): """Scan interface of a model to get tartget residues and create corresponding mutation jobs. """ @@ -188,24 +191,8 @@ def __init__( Additional parameters for interface detection (list on top of alascan/__inint__.py) """ - self.model = model + super().__init__(model, params) self.mutation_res = mutation_res - self.params = params or {} - self.point_mutations_jobs = [] - self.ligand_param_fname = self.params.get("ligand_param_fname", "") - self.ligand_top_fname = self.params.get("ligand_top_fname", "") - self.filter_resdic = { - key[-1]: value - for key, value in self.params.items() - if key.startswith("resdic") - } - if isinstance(model, PDBFile): - self.model_path = model.rel_path - self.model_id = model.file_name.removesuffix(".pdb") - else: - # model given as a plain path - self.model_path = Path(model) - self.model_id = self.model_path.stem @staticmethod def _iter_mutations(interface, resname_dict, mutation_res): @@ -272,44 +259,15 @@ def run(self): add_resname=True, ) - # Determine target residues: get interface, then apply user filers, if given - # Get all interface residues + # Determine target residues: get interface, then apply user filters cutoff = self.params.get("int_cutoff", 5.0) interface = CAPRI.identify_interface(self.model_path, cutoff=cutoff) + interface = filter_interface( + interface, self.filter_resdic, self.params.get("chains", []) + ) - # get user_chains for the check down the line - user_chains = self.params.get("chains", []) - - # if user defined target residues, check they are in the interface - if self.filter_resdic != {"_": []}: - filtered_interface = {} - for chain in self.filter_resdic: - if chain in interface: - # Search for the intersection of user queried residues and interface residues - user_res_valid = list( - set(self.filter_resdic[chain]).intersection( - set(interface[chain]) - ) - ) - # If at least one residue must be analyzed, add it to residues to be scanned - if user_res_valid: - filtered_interface[chain] = user_res_valid - interface = filtered_interface - - # if (user defined target chains) & (no user target residues) - do use user chains - elif user_chains: - interface = { - chain: res - for chain, res in interface.items() - if chain in user_chains - } - - # get all atoms of the model to verifiy residue type down the line - resname_dict = {} - for chain, resid, _atom, resname in coords.keys(): - key = f"{chain}-{resid}" - if key not in resname_dict: - resname_dict[key] = resname + # residue type lookup used to verify residue type down the line + resname_dict = build_resname_dict(coords) # Create mutation output_mutants = self.params.get("output_mutants", False) @@ -337,129 +295,14 @@ def run(self): raise -class ModelPointMutation: - """Executes a single point mutation.""" - - def __init__( - self, - model_path: Path, - model_id: str, - chain: str, - resid: int, - ori_resname: str, - target_resname: str, - native_scores: Tuple[float, float, float, float, float], - output_mutants: bool = False, - ligand_param_fname: Union[Path, str] = "", - ligand_top_fname: Union[Path, str] = "", - ) -> None: - """ - Initialize a single point mutation job. +class ModelPointMutation(_ModelPointMutation): + """Execute a single alascan (protein) point mutation. - Parameters - ---------- - model_path : Path - Path to the PDB file - model_id : str - Identifier for the model - chain : str - Chain identifier - resid : int - Residue number - ori_resname : str - Original residue name - target_resname : str - Target residue name for mutation - native_scores : tuple - Native model scores (score, vdw, elec, desolv, bsa) - output_mutants : bool - Whether to keep mutant PDB files - ligand_param_fname : Union[Path, str] - Path to additional parameter file used by CNS - ligand_top_fname : Union[Path, str] - Path to additional topology file used by CNS - """ - self.model_path = Path(model_path) - self.model_id = model_id - self.chain = chain - self.resid = resid - self.ori_resname = ori_resname - self.target_resname = target_resname - self.native_scores = native_scores - self.output_mutants = output_mutants - self.ligand_param_fname = ligand_param_fname - self.ligand_top_fname = ligand_top_fname + Shares its scoring flow with :class:`haddock.libs.libscan.ModelPointMutation` + and only forwards this module's ``mutate``/``calc_score`` (which stay + patchable in the module namespace). + """ def run(self): """Execute the point mutation.""" - mutation_id = f"{self.model_id}_{self.chain}{self.resid}{self.target_resname}" - - try: - # Setup working directory - sc_dir = f"haddock3-score-{mutation_id}" - os.makedirs(sc_dir, exist_ok=True) - - # Perform point mutation on pdb file - mut_pdb = mutate( - self.model_path, self.chain, self.resid, self.target_resname - ) - - # Calculate mutant scores - mutant_scores = calc_score( - mut_pdb, - run_dir=sc_dir, - outputpdb=self.output_mutants, - ligand_param_fname=self.ligand_param_fname, - ligand_top_fname=self.ligand_top_fname, - ) - - # Calculate deltas (native - mutant) - n_score, n_vdw, n_elec, n_des, n_bsa = self.native_scores - m_score, m_vdw, m_elec, m_des, m_bsa = mutant_scores - delta_scores = ( - n_score - m_score, - n_vdw - m_vdw, - n_elec - m_elec, - n_des - m_des, - n_bsa - m_bsa, - ) - - # Handle output files - em_mut_pdb = Path(f"{mut_pdb.stem}_hs.pdb") - if not self.output_mutants: - # if output_mutants = False, then remove both files - if os.path.exists(mut_pdb): - os.remove(mut_pdb) - if em_mut_pdb.exists(): - os.remove(em_mut_pdb) - else: - # othervise keep energy-minimized pdb - if os.path.exists(em_mut_pdb): - shutil.move(em_mut_pdb, mut_pdb) - # clean up scoring dir - if os.path.exists(sc_dir): - shutil.rmtree(sc_dir) - - return MutationResult( - model_id=self.model_id, - chain=self.chain, - resid=self.resid, - ori_resname=self.ori_resname, - target_resname=self.target_resname, - mutant_scores=mutant_scores, - delta_scores=delta_scores, - success=True, - ) - - except Exception as e: - return MutationResult( - model_id=self.model_id, - chain=self.chain, - resid=self.resid, - ori_resname=self.ori_resname, - target_resname=self.target_resname, - mutant_scores=(0, 0, 0, 0, 0), - delta_scores=(0, 0, 0, 0, 0), - success=False, - error_msg=str(e), - ) + return self._run(mutate, calc_score) diff --git a/src/haddock/modules/analysis/dnascan/__init__.py b/src/haddock/modules/analysis/dnascan/__init__.py new file mode 100644 index 0000000000..1f200bf04a --- /dev/null +++ b/src/haddock/modules/analysis/dnascan/__init__.py @@ -0,0 +1,248 @@ +""" +HADDOCK3 module for DNA base-pair scan. + +This module performs a mutagenesis scan of DNA base pairs for the model(s) +generated in the previous step of the workflow. DNA is double stranded and its +bases are engaged in Watson-Crick base pairs (A:T and G:C). Mutating a single +base in isolation would break the base pair and is not physically meaningful, +so every mutation performed by this module is a *double* mutation: for each +selected interface nucleotide, the nucleotide is mutated into a target base and +its base-pairing partner on the complementary strand is simultaneously mutated +into the complementary base, so that a valid Watson-Crick pair is preserved +(e.g. A:T -> G:C). The differences in the haddock score and its individual +components between the wild type and each mutant base pair are calculated, thus +providing a measure of the impact of such mutations. By default, all four +possible bases are tested for each selected interface nucleotide (the wild-type +base is skipped). Such difference (delta_score) is always calculated as: + + delta_score = score_wildtype - score_mutant + +As the score are typically negative with lower values being better, +a _positive_ delta_score indicates that the mutation is stabilizing +while a _negative_ delta_score indicates that the mutation is destabilizing. + + +If cluster information is available, the module will also calculate the +average haddock score difference for each cluster of models. For each mutation, +a Z score is calculated as: + + Z = (delta_score - mean) / std + +where mean and std are the mean and standard deviation of the delta_score over +all the mutations. + +The module will also generate plots of the DNA scan data, showing the +distribution of the delta_score (and every component) for each base-pair +mutation at the interface. + +You can use the parameters below to customize the behavior of the module: + + * `scan_bases`: list of target bases to be tested for each nucleotide. + By default all four DNA bases (DA, DC, DG, DT) are tested. + * `bp_cutoff`: distance cutoff (Ã…) used to detect Watson-Crick base pairs + from the distance between the hydrogen-bonding ring nitrogens. + * `chains`: list of chains to be considered for the DNA scan. In some + cases you may want to limit the analysis to a single chain. + * `output_mutants`: if True, the module will output the models with the + mutations applied (only possible if there is only one model) + * `output_bfactor`: if True, the module will output the non-mutated models + with the rescaled delta_score in the B-factor column + * `plot`: if True, the module will generate plots of the DNA scan data + * `splitplot`: if True, the scan plot shows one panel per energy component; + if False (default) all components are overlaid in a single panel + * `resdic`: list of residues to be used for the scanning. An example is: + + >>> resdic_A = [1,2,3,4] + >>> resdic_B = [2,3,4] + +Only nucleic acid (DNA) residues at the interface are mutated; protein and +other residues are ignored. Interface nucleotides for which no base-pairing +partner can be found are skipped, since a double (base-pair) mutation cannot +be constructed for them. + +When applying a mutation, the deoxyribose-phosphate backbone is always kept for +both nucleotides of the pair. For mutations between bases of the same ring type +(purine <-> purine, i.e. DA <-> DG, or pyrimidine <-> pyrimidine, i.e. +DC <-> DT), the shared base ring atoms are also kept so that the base +orientation is preserved and CNS only rebuilds the differing substituents; the +whole base pair is then scored in a single CNS call. For cross-type mutations, +the two nucleotides swap ring type (one purine -> pyrimidine and its partner +pyrimidine -> purine). Rebuilding a whole purine ring from its three anchor +atoms in the same pass as the partner mutation is unreliable, so such a base pair +is +mutated in two sequential, CNS-regularised steps: first the purine -> +pyrimidine mutation is applied and energy-minimised by CNS, then the +pyrimidine -> purine mutation is applied to that minimised intermediate and +scored by CNS. The score and energies of the second (final) CNS call are the +ones reported and plotted. In both cases the glycosidic-region anchor atoms are +kept and, for cross-type mutations, renamed to their counterpart in the target +ring system (pyrimidine N1/C2/C6 <-> purine N9/C4/C8). + +To keep the delta_score comparison consistent, each mutant is compared against a +wild-type baseline that went through the same number of CNS minimisation passes: +a one-pass baseline for same-ring-type mutants and a two-pass baseline (the wild +type minimised and then re-scored) for cross-ring-type mutants. +""" + +from collections import defaultdict +from pathlib import Path + +from haddock import log +from haddock.core.defaults import MODULE_DEFAULT_YAML +from haddock.libs.libparallel import GenericTask +from haddock.modules import BaseHaddockModule, get_engine +from haddock.modules.analysis import get_analysis_exec_mode +from haddock.modules.analysis.dnascan.dnascan import ( + AddDeltaBFactor, + ClusterOutputer, + group_scan_by_cluster, + InterfaceScanner, + validate_scan_bases, + write_scan_out, +) + + +RECIPE_PATH = Path(__file__).resolve().parent +DEFAULT_CONFIG = Path(RECIPE_PATH, MODULE_DEFAULT_YAML) + + +class HaddockModule(BaseHaddockModule): + """HADDOCK3 module for DNA base-pair scan.""" + + name = RECIPE_PATH.name + + def __init__(self, order, path, *ignore, init_params=DEFAULT_CONFIG, **everything): + super().__init__(order, path, init_params) + + @classmethod + def confirm_installation(cls): + """Confirm if module is installed.""" + return + + def validate_ouput_mutant_parameter(self, nmodels: int) -> None: + """Validate the output mutant parameter. + + This parameter can be set to True if only one input model is provided, + otherewise we risk to generate too many PDB files. + + Parameters + ---------- + nmodels: int + Number of input models. + """ + # output mutants is only possible if there is a single input model + if self.params["output_mutants"] and nmodels > 1: + log.warning( + "'output_mutants' parameter is set to True, " + "but more than one model was found. " + "Setting 'output_mutant' parameter to False." + ) + self.params["output_mutants"] = False + + def _run(self): + """Execute module.""" + # Validate and normalise the requested target DNA bases + try: + self.params["scan_bases"] = validate_scan_bases(self.params["scan_bases"]) + except Exception as e: + self.finish_with_error(e) + + # Get the models generated in previous step + try: + models = self.previous_io.retrieve_models(individualize=True) + except Exception as e: + self.finish_with_error(e) + + # Compute number of input model + nmodels = len(models) + + # Validate `output_mutant` parameter + self.validate_ouput_mutant_parameter(nmodels) + + # Step1: "get mutations" i.e. get target interface base pairs per input model + # 1 scan_obj per input model, merged into scan_objects to give to Engine + scan_objects = [ + InterfaceScanner( + scan_bases=self.params["scan_bases"], + model=model, + params=self.params, + ) + for model in models + ] + + log.info(f"Scanning {nmodels} models for possible base-pair mutations") + exec_mode = get_analysis_exec_mode(self.params["mode"]) + Engine = get_engine(exec_mode, self.params) + engine = Engine(scan_objects) + engine.run() + + # Step2: perform mutations + # Collect all base-pair mutations to be performed + mutation_objects = [] + for mutations_to_perform in engine.results: + if mutations_to_perform: + mutation_objects.extend(mutations_to_perform) + + total_mutations = len(mutation_objects) + log.info(f"Found {total_mutations} base-pair mutations") + + if not mutation_objects: + log.info("No interface base pairs found - skipping mutation analysis") + # Send models to the next step, no operation is done on them + self.output_models = models + self.export_io_models() + return + + # let engine take care of parallelization + engine = Engine(mutation_objects) + engine.run() + + # Organize engine output by model + results_by_model = defaultdict(list) + for result in engine.results: + if result and result.success: + results_by_model[result.model_id].append(result) + + # Save to .tsv + scan_writter_jobs = [ + GenericTask(write_scan_out, results, model_id) + for model_id, results in results_by_model.items() + ] + engine = Engine(scan_writter_jobs) + engine.run() + + # Generate output models with bfactors if requested + if self.params["output_bfactor"]: + update_with_bfactor_jobs = [] + for model in models: + model_id = model.file_name.removesuffix(".pdb") + # empty list when no data was computed for this model + model_results = results_by_model.get(model_id, []) + update_with_bfactor_jobs.append( + AddDeltaBFactor(model, self.path, model_results) + ) + engine = Engine(update_with_bfactor_jobs) + engine.run() + self.output_models = engine.results + else: + # Send models to the next step, no operation is done on them + self.output_models = models + + # Cluster-based analysis + clt_scan, clt_pops = group_scan_by_cluster(models, results_by_model) + dnascan_cluster_jobs = [ + ClusterOutputer( + clt_data, + clt_id, + clt_pops[clt_id], + scan_residue="DNA base pair", + generate_plot=self.params["plot"], + offline=self.params["offline"], + splitplot=self.params["splitplot"], + ) + for clt_id, clt_data in clt_scan.items() + ] + engine = Engine(dnascan_cluster_jobs) + engine.run() + + self.export_io_models() diff --git a/src/haddock/modules/analysis/dnascan/defaults.yaml b/src/haddock/modules/analysis/dnascan/defaults.yaml new file mode 100644 index 0000000000..d81ec99e97 --- /dev/null +++ b/src/haddock/modules/analysis/dnascan/defaults.yaml @@ -0,0 +1,115 @@ +scan_bases: + default: ["DA", "DC", "DG", "DT"] + type: list + minitems: 1 + maxitems: 4 + title: List of target DNA bases to test for each nucleotide. + short: List of target DNA bases tested for each selected interface nucleotide. + long: List of target DNA bases to be tested for each selected interface + nucleotide. Only the two-letter DNA residue names (DA, DC, DG, DT) are + accepted; one-letter names (A, C, G, U) denote RNA bases and are not + supported. By default all four bases are tested. The wild-type base is + automatically skipped for each nucleotide. As DNA is double stranded, the + base-pairing partner is always mutated to the Watson-Crick complement of + the target base. + group: analysis + explevel: easy +bp_cutoff: + default: 3.5 + type: float + min: 2.5 + max: 6.0 + precision: 3 + title: Distance cutoff (Ã…) used to detect Watson-Crick base pairs. + short: Distance cutoff (Ã…) used to detect Watson-Crick base pairs. + long: Distance cutoff (Ã…) used to detect Watson-Crick base pairs from the + distance between the hydrogen-bonding ring nitrogens (N1 of the purine and + N3 of the pyrimidine). A canonical Watson-Crick pair has an N1...N3 + distance of about 2.8 Ã…; a small margin is allowed to tolerate + energy-minimised or slightly distorted geometries. + group: analysis + explevel: expert +resdic_: + default: [] + type: list + minitems: 0 + maxitems: 100 + title: List of residues to be mutated + short: List of residues to be mutated. By default all the interface nucleotides are + mutated. + long: List of residues to be mutated. By default all the interface nucleotides are + mutated. resdic_* is an expandable parameter. You can provide resdic_A, + resdic_B, resdic_C, etc, where the last capital letter is the chain + identifier. + group: analysis + explevel: expert +int_cutoff: + default: 5.0 + type: float + min: 1.0 + max: 20. + precision: 3 + title: Distance cutoff (Ã…) used to define interface contacts. + short: Distance cutoff (Ã…) used to define interface contacts between two interacting molecules. + long: Distance cutoff (Ã…) used to define interface contacts between two interacting molecules. + group: analysis + explevel: easy +output_bfactor: + default: false + type: boolean + title: Dump the input models with the rescaled delta_score written in the b-factor column. + short: Dump the input models with the rescaled delta_score written in the b-factor column. + long: Dump the input models with the rescaled delta_score written in the b-factor column. Those files do not contain the mutations, but rather will display the normalized (between 0 and 100) delta HADDOCK score (100 * (delta_score - min_score) / (max_score - min_score)), averaged over the tested base pairs, in the b-factor column. Both nucleotides of each base pair receive the same value. The higher the b-factor, the more favourable (or less unfavourable) the mutations. You can color the PDB files according to this attribute (in pymol, type "spectrum b") + group: analysis + explevel: easy +plot: + default: false + type: boolean + title: Plot scanning data. + short: Plot scanning data. + long: Plot scanning data. + group: analysis + explevel: easy +splitplot: + default: false + type: boolean + title: Split the scan plot into one panel per energy component. + short: One plot panel per energy component. + long: If true, the scan plot shows one separate panel per energy component (HADDOCK score, van der Waals, electrostatics and desolvation). If false, all components are overlaid in a single panel. + group: analysis + explevel: easy +chains: + default: [] + type: list + minitems: 0 + maxitems: 10 + title: ChainIDs to be considered for the scanning. + short: ChainIDs to be considered for the scanning. + long: ChainIDs to be considered for the scanning. If empty, all the chains will be considered. If not empty, only residues part of the specified chains will be mutated. Do not use this parameter if you are using the resdic_* parameter. + group: analysis + explevel: easy +output_mutants: + default: false + type: boolean + title: Dump the mutated, energy-minimized PDB files. + short: Dump the mutated, energy-minimized PDB files. + long: Dump the mutated, energy-minimized PDB files. As the number of mutants can be very large, this option is allowed only when a single model is provided in input. + group: analysis + explevel: easy +ligand_param_fname: + default: '' + type: file + title: Custom ligand parameter file + short: Ligand parameter file in CNS format + long: Ligand parameter file in CNS format, for any ligand/residues/molecules not supported by default by HADDOCK. + group: 'force field' + explevel: easy +ligand_top_fname: + default: '' + type: file + title: Custom ligand topology file + short: Ligand topology file in CNS format + long: Ligand topology file in CNS format containing the ligand topologies + (atoms, masses, charges, bond definitions...) for any ligand not supported by default by HADDOCK + group: 'force field' + explevel: easy diff --git a/src/haddock/modules/analysis/dnascan/dnascan.py b/src/haddock/modules/analysis/dnascan/dnascan.py new file mode 100644 index 0000000000..3c93bebb38 --- /dev/null +++ b/src/haddock/modules/analysis/dnascan/dnascan.py @@ -0,0 +1,1043 @@ +"""dnascan module. + +Core logic for the DNA base-pair scan. + +Unlike the RNA scan, where each interface nucleotide is mutated independently, +DNA is double stranded and its bases are engaged in Watson-Crick base pairs +(A:T and G:C). Mutating a single base in isolation would break the base pair +and is not physically meaningful. Therefore every mutation performed by this +module is a *double* mutation: the selected interface nucleotide is mutated to a +target base and its base-pairing partner on the complementary strand is +mutated to the complementary base so that a valid Watson-Crick pair is +preserved (e.g. A:T -> G:C). + +Same-ring-type base-pair mutations are applied simultaneously and scored in a +single CNS call. Cross-ring-type base-pair mutations (where the two nucleotides +swap ring type, one purine -> pyrimidine and its partner pyrimidine -> purine) +are instead performed in two sequential, CNS-regularised steps: first the +purine -> pyrimidine mutation is applied and energy-minimised by CNS, then the +pyrimidine -> purine mutation is applied to that minimised intermediate and +scored by CNS. The score and energies of the second (final) CNS call are the +ones reported and plotted. + +To keep the comparison consistent, each mutant is compared against a wild-type +baseline that went through the same number of CNS minimisation passes: a +one-pass baseline for same-ring-type mutants and a two-pass baseline (the wild +type minimised and then re-scored) for cross-ring-type mutants. +""" + +import os +import shutil +from pathlib import Path +from typing import List, Tuple, Dict + +import numpy as np + + +from haddock import log +from haddock.core.typing import Any, Optional, Union +from haddock.libs.libalign import get_atoms, load_coords +from haddock.libs.libscan import ( + MutationResult, + calc_score, + AddDeltaBFactor as _AddDeltaBFactor, + ClusterOutputer as _ClusterOutputer, + write_scan_out as _write_scan_out, + group_scan_by_cluster as _group_scan_by_cluster, + BaseInterfaceScanner as _BaseInterfaceScanner, + PURINE_BASE_ATOMS, # noqa: F401 re-exported for the module's public API + PYRIMIDINE_BASE_ATOMS, # noqa: F401 re-exported for the module's public API + norm_atom_name as _norm_atom_name, + get_atoms_to_keep as _get_atoms_to_keep, + validate_scan_bases as _validate_scan_bases, + filter_interface, + build_resname_dict, +) +from haddock.libs.libcapri import CAPRI + +# Heavy atoms of the deoxyribose-phosphate backbone that are common to all DNA +# nucleotides. These atoms are always preserved when mutating a base. Unlike +# RNA, DNA has a 2'-deoxyribose, so there is no O2' backbone atom. Both the +# modern (OP1/OP2) and legacy (O1P/O2P) phosphate oxygen namings are accepted +# so that models from either convention are handled correctly. +BACKBONE_ATOMS = [ + "P", + "OP1", + "OP2", + "O1P", + "O2P", + "O5'", + "C5'", + "C4'", + "O4'", + "C1'", + "C2'", + "C3'", + "O3'", +] + +# DNA residues that can be scanned/mutated by this module. +DNA_RESIDUES = ("DA", "DC", "DG", "DT") + +# Ring-type classification of the DNA bases. +PURINES = ("DA", "DG") +PYRIMIDINES = ("DC", "DT") + +# Base ring atoms and glycosidic anchors are chemistry-invariant and shared with +# rnascan; they live in libscan (PURINE_BASE_ATOMS / PYRIMIDINE_BASE_ATOMS are +# re-exported above so this module's public names are preserved). + +# Watson-Crick base-pair complementarity. Mutating a base to a target +# implies mutating its partner to the complementary base to keep a valid pair. +COMPLEMENT = {"DA": "DT", "DT": "DA", "DG": "DC", "DC": "DG"} + +# Mapping of the two-letter DNA residue name to the short one-letter code used +# when building mutation identifiers and output file names. +RES_CODES = {"DA": "A", "DC": "C", "DG": "G", "DT": "T"} + +# Distance cutoff (Ã…) used when detecting Watson-Crick base pairs from the +# distance between the hydrogen-bonding ring nitrogens (N1 of the purine and +# N3 of the pyrimidine). A canonical WC pair has an N1...N3 distance of ~2.8 Ã…; +# a small margin is allowed to tolerate energy-minimised / slightly distorted +# geometries. Overridden at runtime by the ``bp_cutoff`` value from the module's +# defaults.yaml. +BP_CUTOFF = 3.5 + +# Default distance cutoff (Ã…) used to define interface contacts between two +# interacting molecules. Overridden at runtime by the ``int_cutoff`` value from +# the module's defaults.yaml. +INT_CUTOFF = 5.0 + + +def wc_atom(resname: str) -> str: + """Return the Watson-Crick hydrogen-bonding ring nitrogen for a base. + + Purines (DA, DG) pair through their N1, pyrimidines (DC, DT) through their + N3. The distance between these two atoms is used to identify base pairs. + + Parameters + ---------- + resname : str + DNA residue name. + + Returns + ------- + str + Atom name of the Watson-Crick ring nitrogen (``N1`` or ``N3``). + """ + return "N1" if resname in PURINES else "N3" + + +def get_atoms_to_keep(ori_resname: str, target_resname: str) -> Dict[str, str]: + """Return the atoms to preserve when mutating one DNA base into another. + + Thin wrapper around :func:`haddock.libs.libscan.get_atoms_to_keep` bound to + this module's deoxyribose-phosphate backbone and DNA ring-type + classification. + """ + return _get_atoms_to_keep( + ori_resname, + target_resname, + backbone_atoms=BACKBONE_ATOMS, + purines=PURINES, + pyrimidines=PYRIMIDINES, + ) + + +# Default set of target bases tested for each selected interface nucleotide. +DEFAULT_SCAN_BASES = list(DNA_RESIDUES) + + +def validate_scan_bases(scan_bases: List[str]) -> List[str]: + """Validate and normalise the list of target DNA bases. + + Only the canonical two-letter DNA residue names (``DA``, ``DC``, ``DG``, + ``DT``) are accepted, in a case-insensitive manner. The one-letter names + (``A``, ``C``, ``G``, ``U``) denote RNA bases in HADDOCK and are therefore + rejected by this DNA-specific module. + + Thin wrapper around :func:`haddock.libs.libscan.validate_scan_bases`. + """ + return _validate_scan_bases( + scan_bases, + DNA_RESIDUES, + base_kind="DNA", + allowed_note=( + f"two-letter DNA residue names {', '.join(DNA_RESIDUES)} " + "(one-letter names such as A, C, G, U denote RNA bases and are " + "not supported)." + ), + ) + + +def is_cross_type(ori_resname: str, target_resname: str) -> bool: + """Return True if a mutation changes the base ring type. + + A cross-type mutation converts a purine into a pyrimidine or vice versa. + + Parameters + ---------- + ori_resname : str + Original (wild-type) residue name. + target_resname : str + Target base residue name. + """ + return (ori_resname in PURINES) != (target_resname in PURINES) + + +def _mutate_residues(pdb_f, mutations: Dict[Tuple[str, int], str]) -> Path: + """Apply one or more nucleotide mutations to a PDB file. + + For each targeted nucleotide the deoxyribose-phosphate backbone is always + kept; when the original and target bases share a ring type the common base + ring atoms are kept as well to preserve the base orientation (see + ``get_atoms_to_keep``). For cross-type mutations (purine <-> pyrimidine) the + three glycosidic-region anchor atoms are kept and renamed to their + counterpart in the target ring (pyrimidine N1/C2/C6 <-> purine N9/C4/C8). The + remaining base atoms are dropped and rebuilt by CNS during scoring. + + Parameters + ---------- + pdb_f : Path + Path to the pdb file. + mutations : dict + Mapping of ``(chain, resid)`` -> target residue name. The output file + name and mutation identifier follow the insertion order of this dict. + + Returns + ------- + mut_pdb_fname : Path + Path to the mutated pdb file. + """ + # Map (chain, resid) -> discovered original name, target base name and the + # atoms-to-keep map (computed once per residue, when its original name is + # first seen). + targets = { + key: {"mut": mut, "ori": "", "keep": None} for key, mut in mutations.items() + } + mut_pdb_l = [] + with open(pdb_f, "r") as fh: + for line in fh: + if line.startswith("ATOM"): + chain = line[21] + resid = int(line[22:26]) + atom_name = _norm_atom_name(line[12:16].strip()) + key = (chain, resid) + if key in targets: + entry = targets[key] + if not entry["ori"]: + entry["ori"] = line[17:20].strip() + entry["keep"] = get_atoms_to_keep(entry["ori"], entry["mut"]) + atoms_to_keep = entry["keep"] + if atom_name in atoms_to_keep: + # DNA residue names are shorter than 3 characters, so + # they must be right-justified to keep the PDB columns + # (18-20) aligned. + resname_field = entry["mut"].rjust(3) + line = line[:17] + resname_field + line[20:] + # rename the atom if it maps to a different name in the + # target ring system (cross-type anchor atoms) + new_atom_name = atoms_to_keep[atom_name] + if new_atom_name != atom_name: + new_field = line[12:16].replace(atom_name, new_atom_name, 1) + line = line[:12] + new_field + line[16:] + mut_pdb_l.append(line) + else: + mut_pdb_l.append(line) + + # Build the mutation identifier, one "" token per + # mutated nucleotide, in the order the mutations were provided. + id_tokens = [] + for (chain, resid), entry in targets.items(): + ori = entry["ori"] + try: + id_tokens.append(f"{RES_CODES[ori]}{resid}{RES_CODES[entry['mut']]}") + except KeyError: + raise KeyError(f"Could not mutate {ori} into {entry['mut']}.") + mut_id = "-".join(id_tokens) + first_chain = next(iter(targets))[0] + mut_pdb_fname = Path(pdb_f.name.replace(".pdb", f"-{first_chain}_{mut_id}.pdb")) + with open(mut_pdb_fname, "w") as fh: + fh.write("".join(mut_pdb_l)) + return mut_pdb_fname + + +def mutate( + pdb_f, + target_chain, + target_resid, + mut_resname, + partner_chain, + partner_resid, + partner_mut_resname, +): + """ + Perform a Watson-Crick double mutation of a base pair in a PDB file. + + Both the selected nucleotide and its base-pairing partner are mutated at + once so that a valid Watson-Crick pair is preserved. See + ``_mutate_residues`` for the details of how atoms are kept and renamed. + + Parameters + ---------- + pdb_f : str + Path to the pdb file. + target_chain : str + Chain of the primary nucleotide to be mutated. + target_resid : int + Residue number of the primary nucleotide to be mutated. + mut_resname : str + Residue name of the target base for the primary nucleotide + (e.g. ``DA``, ``DC``, ``DG``, ``DT``). + partner_chain : str + Chain of the base-pairing partner nucleotide. + partner_resid : int + Residue number of the base-pairing partner nucleotide. + partner_mut_resname : str + Residue name of the target base for the partner nucleotide (the + Watson-Crick complement of ``mut_resname``). + + Returns + ------- + mut_pdb_fname : Path + Path to the mutated pdb file. + """ + return _mutate_residues( + pdb_f, + { + (target_chain, target_resid): mut_resname, + (partner_chain, partner_resid): partner_mut_resname, + }, + ) + + +def find_base_pairs( + coords: Dict[Tuple[str, int, str, str], Any], + cutoff: float = BP_CUTOFF, +) -> Dict[Tuple[str, int], Tuple[str, int, str]]: + """Detect Watson-Crick base pairs from atomic coordinates. + + For every DNA nucleotide, the Watson-Crick hydrogen-bonding ring nitrogen + (N1 for purines, N3 for pyrimidines) is located and the closest nucleotide + of the complementary ring type (purine <-> pyrimidine) whose corresponding + nitrogen lies within ``cutoff`` is taken as its base-pairing partner. Base + pairing is not restricted to a single chain: partners on the same strand + (e.g. hairpin duplexes) or on a different chain are both detected. + + Parameters + ---------- + coords : dict + Coordinate dictionary as returned by + :func:`haddock.libs.libalign.load_coords` with ``add_resname=True``; + keys are ``(chain, resid, atom_name, resname)`` tuples. + cutoff : float, optional + Maximum N1...N3 distance (Ã…) for two bases to be considered paired. + + Returns + ------- + dict + Mapping ``(chain, resid) -> (partner_chain, partner_resid, + partner_resname)`` for every nucleotide for which a partner was found. + """ + # Gather the Watson-Crick nitrogen coordinate of every DNA nucleotide. + nucleotides: Dict[Tuple[str, int], Tuple[str, Any]] = {} + for (chain, resid, atom, resname), xyz in coords.items(): + if resname not in DNA_RESIDUES: + continue + if atom == wc_atom(resname): + nucleotides[(chain, resid)] = (resname, np.asarray(xyz)) + + # For every nucleotide keep the closest complementary-ring-type partner seen + # within the cutoff. Each unordered pair is visited once and its distance + # updates the running best of *both* nucleotides, halving the distance + # computations compared to a full pairwise sweep. + keys = list(nucleotides.keys()) + best_dist: Dict[Tuple[str, int], float] = {key: cutoff for key in keys} + best_key: Dict[Tuple[str, int], Optional[Tuple[str, int]]] = { + key: None for key in keys + } + for i, key_i in enumerate(keys): + resname_i, xyz_i = nucleotides[key_i] + for key_j in keys[i + 1 :]: + resname_j, xyz_j = nucleotides[key_j] + # Partners must be of complementary ring type (purine <-> pyrimidine) + if (resname_i in PURINES) == (resname_j in PURINES): + continue + dist = float(np.linalg.norm(xyz_i - xyz_j)) + if dist < best_dist[key_i]: + best_dist[key_i] = dist + best_key[key_i] = key_j + if dist < best_dist[key_j]: + best_dist[key_j] = dist + best_key[key_j] = key_i + + pairs: Dict[Tuple[str, int], Tuple[str, int, str]] = {} + for key, partner in best_key.items(): + if partner is not None: + pairs[key] = (partner[0], partner[1], nucleotides[partner][0]) + return pairs + + +class ClusterOutputer(_ClusterOutputer): + """Manage the generation of dnascan outputs for cluster-based analysis.""" + + module_name = "dnascan" + default_scan_residue = "DNA base pair" + sort_columns = ["chain", "resid", "target_resname"] + zscore_reference = "mutations" + + def _identity_columns(self): + return [ + "chain", + "resid", + "resname", + "target_resname", + "partner_chain", + "partner_resid", + "partner_resname", + "partner_target_resname", + "full_resname", + ] + + def _extra_header_lines(self): + return ["each row is a Watson-Crick double mutation (base pair)"] + + def _identity_row(self, ident, clt_res_dt): + # Build a compact human-readable label for the base-pair mutation + full_resname = ( + f"{clt_res_dt['chain']}{clt_res_dt['resid']}" + f"{clt_res_dt['ori_resname']}>{clt_res_dt['target_resname']}" + f"/{clt_res_dt['partner_chain']}{clt_res_dt['partner_resid']}" + f"{clt_res_dt['partner_ori_resname']}>" + f"{clt_res_dt['partner_target_resname']}" + ) + return [ + clt_res_dt["chain"], + clt_res_dt["resid"], + clt_res_dt["ori_resname"], + clt_res_dt["target_resname"], + clt_res_dt["partner_chain"], + clt_res_dt["partner_resid"], + clt_res_dt["partner_ori_resname"], + clt_res_dt["partner_target_resname"], + full_resname, + ] + + +class AddDeltaBFactor(_AddDeltaBFactor): + """Add dnascan delta score in the b-factor column of a PDB. + + The delta score of a base-pair mutation is attributed to both nucleotides + of the pair so that both light up when colouring by b-factor. + """ + + module_name = "dnascan" + + def _residue_keys(self, mut_result): + return [ + (mut_result.chain, mut_result.resid), + (mut_result.partner_chain, mut_result.partner_resid), + ] + + +_DNA_SCAN_COLUMNS = [ + "chain", + "res", + "ori_resname", + "end_resname", + "partner_chain", + "partner_res", + "partner_ori_resname", + "partner_end_resname", + "score", + "vdw", + "elec", + "desolv", + "bsa", + "delta_score", + "delta_vdw", + "delta_elec", + "delta_desolv", + "delta_bsa", +] + + +def _dna_scan_row(result): + """Build a per-model TSV row for a base-pair (double) mutation result.""" + m_score, m_vdw, m_elec, m_des, m_bsa = result.mutant_scores + d_score, d_vdw, d_elec, d_des, d_bsa = result.delta_scores + return [ + result.chain, + result.resid, + result.ori_resname, + result.target_resname, + result.partner_chain, + result.partner_resid, + result.partner_ori_resname, + result.partner_target_resname, + m_score, + m_vdw, + m_elec, + m_des, + m_bsa, + d_score, + d_vdw, + d_elec, + d_des, + d_bsa, + ] + + +def _dna_cluster_metadata(result): + """Static per-mutation metadata stored for each base-pair identifier.""" + return { + "chain": result.chain, + "resid": result.resid, + "ori_resname": result.ori_resname, + "target_resname": result.target_resname, + "partner_chain": result.partner_chain, + "partner_resid": result.partner_resid, + "partner_ori_resname": result.partner_ori_resname, + "partner_target_resname": result.partner_target_resname, + } + + +def group_scan_by_cluster(models, results_by_model): + """Group dnascan data per cluster, keyed by base-pair mutation.""" + return _group_scan_by_cluster( + models, + results_by_model, + ident_builder=lambda r: ( + f"{r.chain}-{r.resid}-{r.ori_resname}-{r.target_resname}" + ), + metadata_builder=_dna_cluster_metadata, + ) + + +def write_scan_out(results, model_id): + """Save dnascan base-pair mutation results for one model to a tsv file.""" + _write_scan_out( + results, + model_id, + module_name="dnascan", + sort_columns=["chain", "res", "end_resname"], + row_builder=_dna_scan_row, + columns=_DNA_SCAN_COLUMNS, + zscore_reference="mutations", + extra_header_lines=["each row is a Watson-Crick double mutation (base pair)"], + ) + + +class InterfaceScanner(_BaseInterfaceScanner): + """Scan interface of a model to get target base pairs and create + corresponding double-mutation jobs. + """ + + def __init__( + self, + model: Union[str, Path, Any], + scan_bases: Optional[List[str]] = None, + params: Optional[Dict[str, Any]] = None, + ) -> None: + """ + Initialize InterfaceScanner for a single model. + + Parameters + ---------- + model : str, Path, or model object + HADDOCK ``PDBFile`` model object or a path to a PDB file. + scan_bases : list of str, optional + Target bases tested for each interface nucleotide + (default: DA, DC, DG, DT) + params : dict, optional + Additional parameters for interface detection + (list on top of dnascan/__init__.py) + """ + super().__init__(model, params) + self.scan_bases = list(scan_bases) if scan_bases else list(DEFAULT_SCAN_BASES) + + def _compute_native_baselines( + self, + ) -> Tuple[ + Tuple[float, float, float, float, float], + Tuple[float, float, float, float, float], + ]: + """Return the one-pass and two-pass wild-type score baselines. + + The one-pass baseline is the score of the wild-type model (matching the + single CNS call used for same-ring-type mutants). The two-pass baseline + scores the CNS energy-minimised wild type a second time, so that it + matches the two sequential CNS steps used to score cross-ring-type + base-pair mutations. Both are computed once per model. + + Returns + ------- + tuple + ``(native_scores, native_scores_2step)``. + """ + sc_dir_1 = f"haddock3-score-{self.model_id}-{os.getpid()}-nat1" + sc_dir_2 = f"haddock3-score-{self.model_id}-{os.getpid()}-nat2" + min_wt = Path(f"{Path(self.model_path).stem}_hs.pdb") + try: + # one CNS pass: score the WT and write its energy-minimised structure + native_scores = calc_score( + self.model_path, + run_dir=sc_dir_1, + outputpdb=True, + ligand_param_fname=self.ligand_param_fname, + ligand_top_fname=self.ligand_top_fname, + ) + # two CNS passes: score the energy-minimised WT a second time + if min_wt.exists(): + native_scores_2step = calc_score( + min_wt, + run_dir=sc_dir_2, + outputpdb=False, + ligand_param_fname=self.ligand_param_fname, + ligand_top_fname=self.ligand_top_fname, + ) + else: + # the minimised structure was not produced (e.g. calc_score is + # mocked in tests); fall back to the one-pass baseline + native_scores_2step = native_scores + finally: + if min_wt.exists(): + os.remove(min_wt) + for d in (sc_dir_1, sc_dir_2): + if os.path.exists(d): + shutil.rmtree(d) + return native_scores, native_scores_2step + + @staticmethod + def _iter_mutations(interface, resname_dict, base_pairs, scan_bases): + """Yield each valid Watson-Crick base-pair (double) mutation to perform. + + Flattens the interface (chain -> residues) and the requested target + bases into a single stream of fully-resolved mutation tuples. Skips + non-DNA residues, nucleotides without a Watson-Crick partner, no-op + mutations (target base equal to the original) and deduplicates each base + pair so it is scanned once (not once per nucleotide). + + Parameters + ---------- + interface : dict + Mapping of chain id to the list of interface residue numbers. + resname_dict : dict + Mapping of ``"{chain}-{resid}"`` to the residue name. + base_pairs : dict + Mapping of ``(chain, resid)`` to its Watson-Crick partner tuple + ``(partner_chain, partner_resid, partner_ori_resname)``. + scan_bases : list of str + Target bases to scan each nucleotide into. + + Yields + ------ + tuple + ``(chain, resid, ori_resname, target_resname, partner_chain, + partner_resid, partner_ori_resname, partner_target_resname)`` for + each base-pair mutation. + """ + # Keep track of base pairs already scheduled so that a pair is not + # scanned twice (once from each of its two nucleotides). + scheduled_pairs = set() + for chain, residues in interface.items(): + for res in residues: + ori_resname = resname_dict[f"{chain}-{res}"] + # Only scan DNA nucleotides, skip protein/other residues + if ori_resname not in DNA_RESIDUES: + continue + # DNA mutations must be done as base pairs: locate partner + partner = base_pairs.get((chain, res)) + if partner is None: + log.warning( + f"No Watson-Crick partner found for {chain}:{res} " + f"{ori_resname}; skipping it (dnascan only mutates " + "base pairs)." + ) + continue + partner_chain, partner_resid, partner_ori = partner + # Deduplicate on the unordered base pair + pair_key = frozenset({(chain, res), (partner_chain, partner_resid)}) + if pair_key in scheduled_pairs: + continue + scheduled_pairs.add(pair_key) + for end_resname in scan_bases: + # Skip no-op mutation (e.g. DA -> DA) + if ori_resname == end_resname: + continue + # Partner is mutated to the Watson-Crick complement to keep + # a valid base pair + partner_target = COMPLEMENT[end_resname] + yield ( + chain, + res, + ori_resname, + end_resname, + partner_chain, + partner_resid, + partner_ori, + partner_target, + ) + + def run(self): + """ + Get interface base pairs and create the double-mutation jobs. + + The jobs are returned (not executed): the caller hands them to a haddock + Engine so that all mutations are scheduled together. + + Returns + ------- + List[ModelBasePairMutation] + The base-pair mutation jobs to perform for this model. + """ + try: + # Calculate the wild-type score baselines. Two baselines are used + # so that each mutant is compared against a wild type that has gone + # through the same number of CNS minimisation passes: + # * native_scores : one CNS pass (same-ring-type mutants) + # * native_scores_2step : two CNS passes (cross-ring-type mutants, + # which are themselves scored in two sequential CNS steps) + native_scores, native_scores_2step = self._compute_native_baselines() + + # Load coordinates + atoms = get_atoms(self.model_path) + coords, _chain_ranges = load_coords( + self.model_path, + atoms, + add_resname=True, + ) + + # Detect Watson-Crick base pairs across the whole model + bp_cutoff = self.params.get("bp_cutoff", BP_CUTOFF) + base_pairs = find_base_pairs(coords, cutoff=bp_cutoff) + + # Determine target nucleotides: get interface, then apply user filters + cutoff = self.params.get("int_cutoff", INT_CUTOFF) + interface = CAPRI.identify_interface(self.model_path, cutoff=cutoff) + interface = filter_interface( + interface, self.filter_resdic, self.params.get("chains", []) + ) + + # residue type lookup used to verify residue type down the line + resname_dict = build_resname_dict(coords) + + # Create mutations + output_mutants = self.params.get("output_mutants", False) + for mut in self._iter_mutations( + interface, resname_dict, base_pairs, self.scan_bases + ): + ( + chain, + res, + ori_resname, + end_resname, + partner_chain, + partner_resid, + partner_ori, + partner_target, + ) = mut + job = ModelBasePairMutation( + model_path=self.model_path, + model_id=self.model_id, + chain=chain, + resid=res, + ori_resname=ori_resname, + target_resname=end_resname, + partner_chain=partner_chain, + partner_resid=partner_resid, + partner_ori_resname=partner_ori, + partner_target_resname=partner_target, + native_scores=native_scores, + native_scores_2step=native_scores_2step, + output_mutants=output_mutants, + ligand_param_fname=self.ligand_param_fname, + ligand_top_fname=self.ligand_top_fname, + ) + self.point_mutations_jobs.append(job) + + return self.point_mutations_jobs + + except Exception as e: + log.error(f"Failed to scan model {self.model_id}: {e}") + raise + + +class ModelBasePairMutation: + """Executes a single Watson-Crick base-pair (double) mutation.""" + + def __init__( + self, + model_path: Path, + model_id: str, + chain: str, + resid: int, + ori_resname: str, + target_resname: str, + partner_chain: str, + partner_resid: int, + partner_ori_resname: str, + partner_target_resname: str, + native_scores: Tuple[float, float, float, float, float], + native_scores_2step: Optional[Tuple[float, float, float, float, float]] = None, + output_mutants: bool = False, + ligand_param_fname: Union[Path, str] = "", + ligand_top_fname: Union[Path, str] = "", + ) -> None: + """ + Initialize a single base-pair mutation job. + + Parameters + ---------- + model_path : Path + Path to the PDB file + model_id : str + Identifier for the model + chain : str + Chain identifier of the primary nucleotide + resid : int + Residue number of the primary nucleotide + ori_resname : str + Original residue name of the primary nucleotide + target_resname : str + Target base name for the primary nucleotide + partner_chain : str + Chain identifier of the base-pairing partner + partner_resid : int + Residue number of the base-pairing partner + partner_ori_resname : str + Original residue name of the base-pairing partner + partner_target_resname : str + Target base name for the base-pairing partner (WC complement) + native_scores : tuple + Wild-type model scores from a single CNS pass (score, vdw, elec, + desolv, bsa); used as the baseline for same-ring-type mutants. + native_scores_2step : tuple, optional + Wild-type model scores from two CNS passes; used as the baseline for + cross-ring-type mutants (which are scored in two sequential CNS + steps). Defaults to ``native_scores`` when not provided. + output_mutants : bool + Whether to keep mutant PDB files + ligand_param_fname : Union[Path, str] + Path to additional parameter file used by CNS + ligand_top_fname : Union[Path, str] + Path to additional topology file used by CNS + """ + self.model_path = Path(model_path) + self.model_id = model_id + self.chain = chain + self.resid = resid + self.ori_resname = ori_resname + self.target_resname = target_resname + self.partner_chain = partner_chain + self.partner_resid = partner_resid + self.partner_ori_resname = partner_ori_resname + self.partner_target_resname = partner_target_resname + self.native_scores = native_scores + self.native_scores_2step = ( + native_scores_2step if native_scores_2step is not None else native_scores + ) + self.output_mutants = output_mutants + self.ligand_param_fname = ligand_param_fname + self.ligand_top_fname = ligand_top_fname + + def _canonical_mutant_name(self) -> Path: + """Return the canonical output name for the fully mutated base pair.""" + mut_id = ( + f"{RES_CODES[self.ori_resname]}{self.resid}" + f"{RES_CODES[self.target_resname]}" + f"-{RES_CODES[self.partner_ori_resname]}{self.partner_resid}" + f"{RES_CODES[self.partner_target_resname]}" + ) + return Path(self.model_path.name.replace(".pdb", f"-{self.chain}_{mut_id}.pdb")) + + def _score_same_type(self, mutation_id): + """Score a same-ring-type base-pair mutation with a single CNS call. + + Both nucleotides keep their ring type (purine <-> purine and + pyrimidine <-> pyrimidine), so the two mutations can be applied + simultaneously and rebuilt by CNS in one pass. + """ + sc_dir = f"haddock3-score-{mutation_id}" + os.makedirs(sc_dir, exist_ok=True) + + # Perform the double (base-pair) mutation on the pdb file + mut_pdb = mutate( + self.model_path, + self.chain, + self.resid, + self.target_resname, + self.partner_chain, + self.partner_resid, + self.partner_target_resname, + ) + + mutant_scores = calc_score( + mut_pdb, + run_dir=sc_dir, + outputpdb=self.output_mutants, + ligand_param_fname=self.ligand_param_fname, + ligand_top_fname=self.ligand_top_fname, + ) + + # Handle output files + em_mut_pdb = Path(f"{mut_pdb.stem}_hs.pdb") + if not self.output_mutants: + for f in (mut_pdb, em_mut_pdb): + if os.path.exists(f): + os.remove(f) + elif os.path.exists(em_mut_pdb): + # keep the energy-minimized mutant under the canonical name + shutil.move(em_mut_pdb, mut_pdb) + if os.path.exists(sc_dir): + shutil.rmtree(sc_dir) + + return mutant_scores + + def _score_cross_type(self, mutation_id): + """Score a cross-ring-type base-pair mutation with two CNS calls. + + A cross-type base-pair mutation swaps the ring types of the two + nucleotides (one purine -> pyrimidine and its partner + pyrimidine -> purine). Building a whole purine ring from its three + anchor atoms in the same pass as the partner mutation is unreliable, so + the mutation is performed in two sequential, CNS-regularised steps: + + 1. the purine -> pyrimidine mutation is applied and energy-minimised by + CNS, and + 2. the pyrimidine -> purine mutation is then applied to that minimised + intermediate and scored by CNS. + + The scores and energies of the *second* (final) CNS call are returned + and used for the plots. + """ + # Identify the purine->pyrimidine nucleotide (step 1) and the + # pyrimidine->purine one (step 2). + if self.ori_resname in PURINES: + step1_key, step1_target = (self.chain, self.resid), self.target_resname + step2_key, step2_target = ( + (self.partner_chain, self.partner_resid), + self.partner_target_resname, + ) + else: + step1_key, step1_target = ( + (self.partner_chain, self.partner_resid), + self.partner_target_resname, + ) + step2_key, step2_target = (self.chain, self.resid), self.target_resname + + sc_dir_1 = f"haddock3-score-{mutation_id}-1" + sc_dir_2 = f"haddock3-score-{mutation_id}-2" + os.makedirs(sc_dir_1, exist_ok=True) + os.makedirs(sc_dir_2, exist_ok=True) + + intermediates = [] + try: + # Step 1: purine -> pyrimidine, energy-minimised by CNS + step1_pdb = _mutate_residues(self.model_path, {step1_key: step1_target}) + intermediates.append(step1_pdb) + calc_score( + step1_pdb, + run_dir=sc_dir_1, + outputpdb=True, + ligand_param_fname=self.ligand_param_fname, + ligand_top_fname=self.ligand_top_fname, + ) + step1_min = Path(f"{step1_pdb.stem}_hs.pdb") + intermediates.append(step1_min) + if not step1_min.exists(): + raise FileNotFoundError( + f"CNS did not produce the energy-minimized intermediate " + f"{step1_min} for the first mutation step." + ) + + # Step 2: pyrimidine -> purine on the minimised intermediate + step2_pdb = _mutate_residues(step1_min, {step2_key: step2_target}) + intermediates.append(step2_pdb) + mutant_scores = calc_score( + step2_pdb, + run_dir=sc_dir_2, + outputpdb=self.output_mutants, + ligand_param_fname=self.ligand_param_fname, + ligand_top_fname=self.ligand_top_fname, + ) + step2_min = Path(f"{step2_pdb.stem}_hs.pdb") + + # Handle output files + if self.output_mutants and step2_min.exists(): + # keep the final energy-minimized mutant under the canonical name + shutil.move(step2_min, self._canonical_mutant_name()) + elif step2_min.exists(): + intermediates.append(step2_min) + finally: + for f in intermediates: + if os.path.exists(f): + os.remove(f) + for d in (sc_dir_1, sc_dir_2): + if os.path.exists(d): + shutil.rmtree(d) + + return mutant_scores + + def run(self): + """Execute the base-pair (double) mutation.""" + mutation_id = ( + f"{self.model_id}_{self.chain}{self.resid}{self.target_resname}" + f"_{self.partner_chain}{self.partner_resid}{self.partner_target_resname}" + ) + + try: + # Cross-type base-pair mutations (a purine and a pyrimidine swapping + # ring types) are performed in two sequential CNS-regularised steps; + # same-type mutations can be done in a single CNS call. Each mutant + # is compared against the wild-type baseline that went through the + # same number of CNS minimisation passes. + if is_cross_type(self.ori_resname, self.target_resname): + mutant_scores = self._score_cross_type(mutation_id) + baseline_scores = self.native_scores_2step + else: + mutant_scores = self._score_same_type(mutation_id) + baseline_scores = self.native_scores + + # Calculate deltas (native - mutant) + n_score, n_vdw, n_elec, n_des, n_bsa = baseline_scores + m_score, m_vdw, m_elec, m_des, m_bsa = mutant_scores + delta_scores = ( + n_score - m_score, + n_vdw - m_vdw, + n_elec - m_elec, + n_des - m_des, + n_bsa - m_bsa, + ) + + return MutationResult( + model_id=self.model_id, + chain=self.chain, + resid=self.resid, + ori_resname=self.ori_resname, + target_resname=self.target_resname, + partner_chain=self.partner_chain, + partner_resid=self.partner_resid, + partner_ori_resname=self.partner_ori_resname, + partner_target_resname=self.partner_target_resname, + mutant_scores=mutant_scores, + delta_scores=delta_scores, + success=True, + ) + + except Exception as e: + return MutationResult( + model_id=self.model_id, + chain=self.chain, + resid=self.resid, + ori_resname=self.ori_resname, + target_resname=self.target_resname, + partner_chain=self.partner_chain, + partner_resid=self.partner_resid, + partner_ori_resname=self.partner_ori_resname, + partner_target_resname=self.partner_target_resname, + mutant_scores=(0, 0, 0, 0, 0), + delta_scores=(0, 0, 0, 0, 0), + success=False, + error_msg=str(e), + ) diff --git a/src/haddock/modules/analysis/rnascan/__init__.py b/src/haddock/modules/analysis/rnascan/__init__.py index bfc692edcd..2ca49ebf27 100644 --- a/src/haddock/modules/analysis/rnascan/__init__.py +++ b/src/haddock/modules/analysis/rnascan/__init__.py @@ -69,7 +69,7 @@ from haddock.libs.libparallel import GenericTask from haddock.modules import BaseHaddockModule, get_engine from haddock.modules.analysis import get_analysis_exec_mode -from haddock.modules.analysis.rnascan.scan import ( +from haddock.modules.analysis.rnascan.rnascan import ( AddDeltaBFactor, ClusterOutputer, group_scan_by_cluster, diff --git a/src/haddock/modules/analysis/rnascan/rnascan.py b/src/haddock/modules/analysis/rnascan/rnascan.py new file mode 100644 index 0000000000..cf79549ec7 --- /dev/null +++ b/src/haddock/modules/analysis/rnascan/rnascan.py @@ -0,0 +1,366 @@ +"""rnascan module.""" + +import os +import shutil +from pathlib import Path +from typing import List, Dict + + +from haddock import log +from haddock.core.typing import Any, Optional, Union +from haddock.libs.libalign import get_atoms, load_coords +from haddock.libs.libscan import ( + MutationResult, # noqa: F401 re-exported for the module's public API + calc_score, + AddDeltaBFactor as _AddDeltaBFactor, + ClusterOutputer as _ClusterOutputer, + write_scan_out as _write_scan_out, + group_scan_by_cluster as _group_scan_by_cluster, + BaseInterfaceScanner as _BaseInterfaceScanner, + ModelPointMutation as _ModelPointMutation, + PURINE_BASE_ATOMS, # noqa: F401 re-exported for the module's public API + PYRIMIDINE_BASE_ATOMS, # noqa: F401 re-exported for the module's public API + norm_atom_name as _norm_atom_name, + get_atoms_to_keep as _get_atoms_to_keep, + validate_scan_bases as _validate_scan_bases, + filter_interface, + build_resname_dict, +) +from haddock.libs.libcapri import CAPRI + +# Heavy atoms of the ribose-phosphate backbone that are common to all RNA +# nucleotides. These atoms are always preserved when mutating a base. The +# ribose 2'-hydroxyl oxygen (O2') is part of the backbone for RNA. Both the +# modern (OP1/OP2) and legacy (O1P/O2P) phosphate oxygen namings are accepted +# so that models from either convention are handled correctly. +BACKBONE_ATOMS = [ + "P", + "OP1", + "OP2", + "O1P", + "O2P", + "O5'", + "C5'", + "C4'", + "O4'", + "C1'", + "C2'", + "O2'", + "C3'", + "O3'", +] + +# RNA residues that can be scanned/mutated by this module. +RNA_RESIDUES = ("A", "C", "G", "U") + +# Ring-type classification of the RNA bases. +PURINES = ("A", "G") +PYRIMIDINES = ("C", "U") + +# Base ring atoms and glycosidic anchors are chemistry-invariant and shared with +# dnascan; they live in libscan (PURINE_BASE_ATOMS / PYRIMIDINE_BASE_ATOMS are +# re-exported above so this module's public names are preserved). + + +def get_atoms_to_keep(ori_resname: str, target_resname: str) -> Dict[str, str]: + """Return the atoms to preserve when mutating one RNA base into another. + + Thin wrapper around :func:`haddock.libs.libscan.get_atoms_to_keep` bound to + this module's ribose-phosphate backbone and RNA ring-type classification. + """ + return _get_atoms_to_keep( + ori_resname, + target_resname, + backbone_atoms=BACKBONE_ATOMS, + purines=PURINES, + pyrimidines=PYRIMIDINES, + ) + + +# Default set of target bases tested for each selected interface nucleotide. +DEFAULT_SCAN_BASES = list(RNA_RESIDUES) + + +def validate_scan_bases(scan_bases: List[str]) -> List[str]: + """Validate and normalise the list of target RNA bases. + + Only the canonical RNA residue names (``A``, ``C``, ``G``, ``U``) are + accepted, in a case-insensitive manner. Two-letter names prefixed with + ``D`` (``DA``, ``DC``, ``DG``, ``DT``) denote DNA residues in HADDOCK and + are therefore rejected by this RNA-specific module. + + Thin wrapper around :func:`haddock.libs.libscan.validate_scan_bases`. + """ + return _validate_scan_bases( + scan_bases, + RNA_RESIDUES, + base_kind="RNA", + allowed_note=( + f"RNA residue names {', '.join(RNA_RESIDUES)} (two-letter names " + "such as DA, DC, DG, DT denote DNA and are not supported)." + ), + ) + + +def mutate(pdb_f, target_chain, target_resid, mut_resname): + """ + Mutate an RNA base in a PDB file into a different base. + + The ribose-phosphate backbone is always kept for the mutated nucleotide. + When the original and target bases share a ring type (both purines or both + pyrimidines) the common base ring atoms are kept as well to preserve the + base orientation (see ``get_atoms_to_keep``). For cross-type mutations + (purine <-> pyrimidine) the three glycosidic-region anchor atoms are kept and + renamed to their counterpart in the target ring (pyrimidine N1/C2/C6 <-> + purine N9/C4/C8). The remaining base atoms are dropped and rebuilt by CNS + during scoring. + + Parameters + ---------- + pdb_f : str + Path to the pdb file. + + target_chain : str + Chain of the nucleotide to be mutated. + + target_resid : int + Residue number of the nucleotide to be mutated. + + mut_resname : str + Residue name of the target base (e.g. ``A``, ``C``, ``G``, ``U``). + + Returns + ------- + mut_pdb_fname : str + Path to the mutated pdb file. + """ + mut_pdb_l = [] + resname = "" + atoms_to_keep: Dict[str, str] = {} + # RNA residue names are shorter than 3 characters, so they must be + # right-justified to keep the PDB columns (18-20) aligned. + resname_field = mut_resname.rjust(3) + with open(pdb_f, "r") as fh: + for line in fh: + if line.startswith("ATOM"): + chain = line[21] + resid = int(line[22:26]) + atom_name = _norm_atom_name(line[12:16].strip()) + if target_chain == chain and target_resid == resid: + if not resname: + resname = line[17:20].strip() + # Determine which atoms to keep now that the original + # base is known (depends on the ori/target ring types). + atoms_to_keep = get_atoms_to_keep(resname, mut_resname) + if atom_name in atoms_to_keep: + # mutate the residue name + line = line[:17] + resname_field + line[20:] + # rename the atom if it maps to a different name in the + # target ring system (cross-type anchor atoms) + new_atom_name = atoms_to_keep[atom_name] + if new_atom_name != atom_name: + new_field = line[12:16].replace(atom_name, new_atom_name, 1) + line = line[:12] + new_field + line[16:] + mut_pdb_l.append(line) + else: + mut_pdb_l.append(line) + # RNA residue names are already the one-letter codes used in identifiers. + if resname not in RNA_RESIDUES or mut_resname not in RNA_RESIDUES: + raise KeyError(f"Could not mutate {resname} into {mut_resname}.") + mut_id = f"{resname}{target_resid}{mut_resname}" + mut_pdb_fname = Path(pdb_f.name.replace(".pdb", f"-{target_chain}_{mut_id}.pdb")) + with open(mut_pdb_fname, "w") as fh: + fh.write("".join(mut_pdb_l)) + return mut_pdb_fname + + +class ClusterOutputer(_ClusterOutputer): + """Manage the generation of rnascan outputs for cluster-based analysis.""" + + module_name = "rnascan" + default_scan_residue = "RNA base" + sort_columns = ["chain", "resid", "target_resname"] + zscore_reference = "mutations" + + def _identity_columns(self): + return ["chain", "resid", "resname", "target_resname", "full_resname"] + + def _identity_row(self, ident, clt_res_dt): + # ident is "---" + chain, resid, resname, target_resname = ident.split("-") + return [chain, int(resid), resname, target_resname, ident] + + +class AddDeltaBFactor(_AddDeltaBFactor): + """Add rnascan delta score in the b-factor column of a PDB.""" + + module_name = "rnascan" + + +def group_scan_by_cluster(models, results_by_model): + """Group rnascan data per cluster, keyed by mutation (base included).""" + return _group_scan_by_cluster( + models, + results_by_model, + ident_builder=lambda r: ( + f"{r.chain}-{r.resid}-{r.ori_resname}-{r.target_resname}" + ), + ) + + +def write_scan_out(results, model_id): + """Save rnascan mutation results for one model to a tsv file.""" + _write_scan_out( + results, + model_id, + module_name="rnascan", + sort_columns=["chain", "res", "end_resname"], + zscore_reference="mutations", + ) + + +class InterfaceScanner(_BaseInterfaceScanner): + """Scan interface of a model to get target nucleotides and create + corresponding mutation jobs. + """ + + def __init__( + self, + model: Union[str, Path, Any], + scan_bases: Optional[List[str]] = None, + params: Optional[Dict[str, Any]] = None, + ) -> None: + """ + Initialize InterfaceScanner for a single model. + + Parameters + ---------- + model : str, Path, or model object + HADDOCK ``PDBFile`` model object or a path to a PDB file. + scan_bases : list of str, optional + Target bases tested for each interface nucleotide + (default: A, C, G, T) + params : dict, optional + Additional parameters for interface detection + (list on top of rnascan/__init__.py) + """ + super().__init__(model, params) + self.scan_bases = list(scan_bases) if scan_bases else list(DEFAULT_SCAN_BASES) + + @staticmethod + def _iter_mutations(interface, resname_dict, scan_bases): + """Yield each valid single-base mutation to perform. + + Flattens the interface (chain -> residues) and the requested target + bases into a single stream of ``(chain, resid, ori_resname, + target_resname)`` tuples, skipping non-RNA residues and no-op mutations + (target base equal to the original, e.g. ``A -> A``). + + Parameters + ---------- + interface : dict + Mapping of chain id to the list of interface residue numbers. + resname_dict : dict + Mapping of ``"{chain}-{resid}"`` to the residue name. + scan_bases : list of str + Target bases to scan each nucleotide into. + + Yields + ------ + tuple + ``(chain, resid, ori_resname, target_resname)`` for each mutation. + """ + for chain, residues in interface.items(): + for res in residues: + ori_resname = resname_dict[f"{chain}-{res}"] + # Only scan RNA nucleotides, skip protein/other residues + if ori_resname not in RNA_RESIDUES: + continue + for end_resname in scan_bases: + # Skip no-op mutation (e.g. A -> A) + if ori_resname == end_resname: + continue + yield chain, res, ori_resname, end_resname + + def run(self): + """ + Get interface nucleotides and create the mutation jobs for this model. + + The jobs are returned (not executed): the caller hands them to a haddock + Engine so that all mutations are scheduled together. + + Returns + ------- + List[ModelPointMutation] + The mutation jobs to perform for this model. + """ + try: + # Calculate native scores + sc_dir = f"haddock3-score-{self.model_id}-{os.getpid()}" + try: + native_scores = calc_score( + self.model_path, + run_dir=sc_dir, + outputpdb=False, + ligand_param_fname=self.ligand_param_fname, + ligand_top_fname=self.ligand_top_fname, + ) + finally: + if os.path.exists(sc_dir): + shutil.rmtree(sc_dir) + + # Load coordinates + atoms = get_atoms(self.model_path) + coords, _chain_ranges = load_coords( + self.model_path, + atoms, + add_resname=True, + ) + + # Determine target nucleotides: get interface, then apply user filters + cutoff = self.params.get("int_cutoff", 5.0) + interface = CAPRI.identify_interface(self.model_path, cutoff=cutoff) + interface = filter_interface( + interface, self.filter_resdic, self.params.get("chains", []) + ) + + # residue type lookup used to verify residue type down the line + resname_dict = build_resname_dict(coords) + + # Create mutations + output_mutants = self.params.get("output_mutants", False) + for chain, res, ori_resname, end_resname in self._iter_mutations( + interface, resname_dict, self.scan_bases + ): + job = ModelPointMutation( + model_path=self.model_path, + model_id=self.model_id, + chain=chain, + resid=res, + ori_resname=ori_resname, + target_resname=end_resname, + native_scores=native_scores, + output_mutants=output_mutants, + ligand_param_fname=self.ligand_param_fname, + ligand_top_fname=self.ligand_top_fname, + ) + self.point_mutations_jobs.append(job) + + return self.point_mutations_jobs + + except Exception as e: + log.error(f"Failed to scan model {self.model_id}: {e}") + raise + + +class ModelPointMutation(_ModelPointMutation): + """Execute a single rnascan (RNA base) point mutation. + + Shares its scoring flow with :class:`haddock.libs.libscan.ModelPointMutation` + and only forwards this module's ``mutate``/``calc_score`` (which stay + patchable in the module namespace). + """ + + def run(self): + """Execute the point mutation.""" + return self._run(mutate, calc_score) diff --git a/src/haddock/modules/analysis/rnascan/scan.py b/src/haddock/modules/analysis/rnascan/scan.py deleted file mode 100644 index a34dfceeb9..0000000000 --- a/src/haddock/modules/analysis/rnascan/scan.py +++ /dev/null @@ -1,590 +0,0 @@ -"""rnascan module.""" - -import os -import shutil -from pathlib import Path -from typing import List, Tuple, Dict - - -from haddock import log -from haddock.core.exceptions import ConfigurationError -from haddock.core.typing import Any, Optional, Union -from haddock.libs.libalign import get_atoms, load_coords -from haddock.libs.libontology import PDBFile -from haddock.libs.libscan import ( - MutationResult, - calc_score, - AddDeltaBFactor as _AddDeltaBFactor, - ClusterOutputer as _ClusterOutputer, - write_scan_out as _write_scan_out, - group_scan_by_cluster as _group_scan_by_cluster, -) -from haddock.libs.libcapri import CAPRI - -# Heavy atoms of the ribose-phosphate backbone that are common to all RNA -# nucleotides. These atoms are always preserved when mutating a base. The -# ribose 2'-hydroxyl oxygen (O2') is part of the backbone for RNA. Both the -# modern (OP1/OP2) and legacy (O1P/O2P) phosphate oxygen namings are accepted -# so that models from either convention are handled correctly. -BACKBONE_ATOMS = [ - "P", - "OP1", - "OP2", - "O1P", - "O2P", - "O5'", - "C5'", - "C4'", - "O4'", - "C1'", - "C2'", - "O2'", - "C3'", - "O3'", -] - -# Base ring atoms shared by both purines (A and G). Preserving them when -# mutating one purine into the other keeps the base plane and glycosidic -# orientation, so CNS only has to rebuild the differing substituents. -PURINE_BASE_ATOMS = ["N9", "C8", "N7", "C5", "C4", "N3", "C2", "N1", "C6"] - -# Base ring atoms shared by both pyrimidines (C and U). Same rationale as for -# the purines above. -PYRIMIDINE_BASE_ATOMS = ["N1", "C2", "O2", "N3", "C4", "C5", "C6"] - -# RNA residues that can be scanned/mutated by this module. -RNA_RESIDUES = ("A", "C", "G", "U") - -# Ring-type classification of the RNA bases. -PURINES = ("A", "G") -PYRIMIDINES = ("C", "U") - -# Glycosidic-region anchor atoms kept (and renamed) on cross-type mutations -# (purine <-> pyrimidine). Because the purine and pyrimidine ring systems do -# not share atom names, these three atoms are preserved and renamed to their -# counterpart in the target ring so that CNS rebuilds the new base with the -# correct glycosidic orientation (and syn/anti conformation). The -# correspondence is: -# pyrimidine N1 <-> purine N9 (glycosidic nitrogen) -# pyrimidine C2 <-> purine C4 -# pyrimidine C6 <-> purine C8 -PYRIMIDINE_TO_PURINE_ANCHORS = {"N1": "N9", "C2": "C4", "C6": "C8"} -PURINE_TO_PYRIMIDINE_ANCHORS = {"N9": "N1", "C4": "C2", "C8": "C6"} - - -def get_atoms_to_keep(ori_resname: str, target_resname: str) -> Dict[str, str]: - """Return the atoms to preserve when mutating one base into another. - - The result maps each atom name to keep (as found in the original residue) - to the atom name it must be written with in the mutated residue. Backbone - atoms and same-ring-type base atoms keep their name (mapped to themselves). - - The ribose-phosphate backbone is always kept. In addition, when the - original and target bases are of the same ring type (both purines or both - pyrimidines), the base ring atoms common to that type are kept as well, so - that the base orientation is preserved and CNS only rebuilds the differing - substituents. For cross-type mutations (purine <-> pyrimidine) the three - glycosidic-region anchor atoms are kept and renamed to their counterpart in - the target ring system (pyrimidine N1/C2/C6 <-> purine N9/C4/C8), so that the - base orientation is preserved and CNS rebuilds the rest of the new base. - - Parameters - ---------- - ori_resname : str - Original (wild-type) residue name. - target_resname : str - Target base residue name. - - Returns - ------- - dict of str -> str - Mapping of original atom name -> atom name to write for the mutated - nucleotide. - """ - # Backbone is always kept, with unchanged atom names. - atoms_to_keep: Dict[str, str] = {atom: atom for atom in BACKBONE_ATOMS} - if ori_resname in PURINES and target_resname in PURINES: - atoms_to_keep.update({atom: atom for atom in PURINE_BASE_ATOMS}) - elif ori_resname in PYRIMIDINES and target_resname in PYRIMIDINES: - atoms_to_keep.update({atom: atom for atom in PYRIMIDINE_BASE_ATOMS}) - elif ori_resname in PYRIMIDINES and target_resname in PURINES: - atoms_to_keep.update(PYRIMIDINE_TO_PURINE_ANCHORS) - elif ori_resname in PURINES and target_resname in PYRIMIDINES: - atoms_to_keep.update(PURINE_TO_PYRIMIDINE_ANCHORS) - return atoms_to_keep - - -# Default set of target bases tested for each selected interface nucleotide. -DEFAULT_SCAN_BASES = ["A", "C", "G", "U"] - - -def validate_scan_bases(scan_bases: List[str]) -> List[str]: - """Validate and normalise the list of target RNA bases. - - Only the canonical RNA residue names (``A``, ``C``, ``G``, ``U``) are - accepted, in a case-insensitive manner. Two-letter names prefixed with - ``D`` (``DA``, ``DC``, ``DG``, ``DT``) denote DNA residues in HADDOCK and - are therefore rejected by this RNA-specific module. - - Parameters - ---------- - scan_bases : list of str - Target bases requested by the user. - - Returns - ------- - list of str - Normalised, de-duplicated list of canonical RNA residue names, in the - order first seen. - - Raises - ------ - ConfigurationError - If ``scan_bases`` is empty or contains an unrecognised base. - """ - if not scan_bases: - raise ConfigurationError( - "'scan_bases' must contain at least one RNA base " - f"among {', '.join(RNA_RESIDUES)}." - ) - normalised: List[str] = [] - for base in scan_bases: - canonical = str(base).strip().upper() - if canonical not in RNA_RESIDUES: - raise ConfigurationError( - f"Invalid 'scan_bases' entry {base!r}. Allowed values are the " - f"RNA residue names {', '.join(RNA_RESIDUES)} (two-letter names " - "such as DA, DC, DG, DT denote DNA and are not supported)." - ) - if canonical not in normalised: - normalised.append(canonical) - return normalised - - -def _norm_atom_name(atom_name: str) -> str: - """Normalise atom names so that primes are always written as `'`.""" - return atom_name.replace("*", "'") - - -def mutate(pdb_f, target_chain, target_resid, mut_resname): - """ - Mutate an RNA base in a PDB file into a different base. - - The ribose-phosphate backbone is always kept for the mutated nucleotide. - When the original and target bases share a ring type (both purines or both - pyrimidines) the common base ring atoms are kept as well to preserve the - base orientation (see ``get_atoms_to_keep``). For cross-type mutations - (purine <-> pyrimidine) the three glycosidic-region anchor atoms are kept and - renamed to their counterpart in the target ring (pyrimidine N1/C2/C6 <-> - purine N9/C4/C8). The remaining base atoms are dropped and rebuilt by CNS - during scoring. - - Parameters - ---------- - pdb_f : str - Path to the pdb file. - - target_chain : str - Chain of the nucleotide to be mutated. - - target_resid : int - Residue number of the nucleotide to be mutated. - - mut_resname : str - Residue name of the target base (e.g. ``A``, ``C``, ``G``, ``U``). - - Returns - ------- - mut_pdb_fname : str - Path to the mutated pdb file. - """ - mut_pdb_l = [] - resname = "" - atoms_to_keep: List[str] = [] - # RNA residue names are shorter than 3 characters, so they must be - # right-justified to keep the PDB columns (18-20) aligned. - resname_field = mut_resname.rjust(3) - with open(pdb_f, "r") as fh: - for line in fh.readlines(): - if line.startswith("ATOM"): - chain = line[21] - resid = int(line[22:26]) - atom_name = _norm_atom_name(line[12:16].strip()) - if target_chain == chain and target_resid == resid: - if not resname: - resname = line[17:20].strip() - # Determine which atoms to keep now that the original - # base is known (depends on the ori/target ring types). - atoms_to_keep = get_atoms_to_keep(resname, mut_resname) - if atom_name in atoms_to_keep: - # mutate the residue name - line = line[:17] + resname_field + line[20:] - # rename the atom if it maps to a different name in the - # target ring system (cross-type anchor atoms) - new_atom_name = atoms_to_keep[atom_name] - if new_atom_name != atom_name: - new_field = line[12:16].replace(atom_name, new_atom_name, 1) - line = line[:12] + new_field + line[16:] - mut_pdb_l.append(line) - else: - mut_pdb_l.append(line) - # RNA residue names are already the one-letter codes used in identifiers. - if resname not in RNA_RESIDUES or mut_resname not in RNA_RESIDUES: - raise KeyError(f"Could not mutate {resname} into {mut_resname}.") - mut_id = f"{resname}{target_resid}{mut_resname}" - mut_pdb_fname = Path(pdb_f.name.replace(".pdb", f"-{target_chain}_{mut_id}.pdb")) - with open(mut_pdb_fname, "w") as fh: - fh.write("".join(mut_pdb_l)) - return mut_pdb_fname - - -class ClusterOutputer(_ClusterOutputer): - """Manage the generation of rnascan outputs for cluster-based analysis.""" - - module_name = "rnascan" - default_scan_residue = "RNA base" - sort_columns = ["chain", "resid", "target_resname"] - zscore_reference = "mutations" - - def _identity_columns(self): - return ["chain", "resid", "resname", "target_resname", "full_resname"] - - def _identity_row(self, ident, clt_res_dt): - # ident is "---" - chain, resid, resname, target_resname = ident.split("-") - return [chain, int(resid), resname, target_resname, ident] - - -class AddDeltaBFactor(_AddDeltaBFactor): - """Add rnascan delta score in the b-factor column of a PDB.""" - - module_name = "rnascan" - - -def group_scan_by_cluster(models, results_by_model): - """Group rnascan data per cluster, keyed by mutation (base included).""" - return _group_scan_by_cluster( - models, - results_by_model, - ident_builder=lambda r: ( - f"{r.chain}-{r.resid}-{r.ori_resname}-{r.target_resname}" - ), - ) - - -def write_scan_out(results, model_id): - """Save rnascan mutation results for one model to a tsv file.""" - _write_scan_out( - results, - model_id, - module_name="rnascan", - sort_columns=["chain", "res", "end_resname"], - zscore_reference="mutations", - ) - - -class InterfaceScanner: - """Scan interface of a model to get target nucleotides and create - corresponding mutation jobs. - """ - - def __init__( - self, - model: Union[str, Path, Any], - scan_bases: Optional[List[str]] = None, - params: Optional[Dict[str, Any]] = None, - ) -> None: - """ - Initialize InterfaceScanner for a single model. - - Parameters - ---------- - model : str, Path, or model object - HADDOCK ``PDBFile`` model object or a path to a PDB file. - scan_bases : list of str, optional - Target bases tested for each interface nucleotide - (default: A, C, G, T) - params : dict, optional - Additional parameters for interface detection - (list on top of rnascan/__init__.py) - """ - self.model = model - self.scan_bases = list(scan_bases) if scan_bases else list(DEFAULT_SCAN_BASES) - self.params = params or {} - self.point_mutations_jobs = [] - self.ligand_param_fname = self.params.get("ligand_param_fname", "") - self.ligand_top_fname = self.params.get("ligand_top_fname", "") - self.filter_resdic = { - key[-1]: value - for key, value in self.params.items() - if key.startswith("resdic") - } - if isinstance(model, PDBFile): - self.model_path = model.rel_path - self.model_id = model.file_name.removesuffix(".pdb") - else: - # model given as a plain path - self.model_path = Path(model) - self.model_id = self.model_path.stem - - @staticmethod - def _iter_mutations(interface, resname_dict, scan_bases): - """Yield each valid single-base mutation to perform. - - Flattens the interface (chain -> residues) and the requested target - bases into a single stream of ``(chain, resid, ori_resname, - target_resname)`` tuples, skipping non-RNA residues and no-op mutations - (target base equal to the original, e.g. ``A -> A``). - - Parameters - ---------- - interface : dict - Mapping of chain id to the list of interface residue numbers. - resname_dict : dict - Mapping of ``"{chain}-{resid}"`` to the residue name. - scan_bases : list of str - Target bases to scan each nucleotide into. - - Yields - ------ - tuple - ``(chain, resid, ori_resname, target_resname)`` for each mutation. - """ - for chain, residues in interface.items(): - for res in residues: - ori_resname = resname_dict[f"{chain}-{res}"] - # Only scan RNA nucleotides, skip protein/other residues - if ori_resname not in RNA_RESIDUES: - continue - for end_resname in scan_bases: - # Skip no-op mutation (e.g. A -> A) - if ori_resname == end_resname: - continue - yield chain, res, ori_resname, end_resname - - def run(self): - """ - Get interface nucleotides and create the mutation jobs for this model. - - The jobs are returned (not executed): the caller hands them to a haddock - Engine so that all mutations are scheduled together. - - Returns - ------- - List[ModelPointMutation] - The mutation jobs to perform for this model. - """ - try: - # Calculate native scores - sc_dir = f"haddock3-score-{self.model_id}-{os.getpid()}" - try: - native_scores = calc_score( - self.model_path, - run_dir=sc_dir, - outputpdb=False, - ligand_param_fname=self.ligand_param_fname, - ligand_top_fname=self.ligand_top_fname, - ) - finally: - if os.path.exists(sc_dir): - shutil.rmtree(sc_dir) - - # Load coordinates - atoms = get_atoms(self.model_path) - coords, _chain_ranges = load_coords( - self.model_path, - atoms, - add_resname=True, - ) - - # Determine target nucleotides: get interface, then apply user filters - # Get all interface residues - cutoff = self.params.get("int_cutoff", 5.0) - interface = CAPRI.identify_interface(self.model_path, cutoff=cutoff) - - # get user_chains for the check down the line - user_chains = self.params.get("chains", []) - - # if user defined target residues, check they are in the interface - if self.filter_resdic != {"_": []}: - filtered_interface = {} - for chain in self.filter_resdic: - if chain in interface: - # Search for the intersection of user queried residues and interface residues - user_res_valid = list( - set(self.filter_resdic[chain]).intersection( - set(interface[chain]) - ) - ) - # If at least one residue must be analyzed, add it to residues to be scanned - if user_res_valid: - filtered_interface[chain] = user_res_valid - interface = filtered_interface - - # if (user defined target chains) & (no user target residues) - do use user chains - elif user_chains: - interface = { - chain: res - for chain, res in interface.items() - if chain in user_chains - } - - # get all atoms of the model to verify residue type down the line - resname_dict = {} - for chain, resid, _atom, resname in coords.keys(): - key = f"{chain}-{resid}" - if key not in resname_dict: - resname_dict[key] = resname - - # Create mutations - output_mutants = self.params.get("output_mutants", False) - for chain, res, ori_resname, end_resname in self._iter_mutations( - interface, resname_dict, self.scan_bases - ): - job = ModelPointMutation( - model_path=self.model_path, - model_id=self.model_id, - chain=chain, - resid=res, - ori_resname=ori_resname, - target_resname=end_resname, - native_scores=native_scores, - output_mutants=output_mutants, - ligand_param_fname=self.ligand_param_fname, - ligand_top_fname=self.ligand_top_fname, - ) - self.point_mutations_jobs.append(job) - - return self.point_mutations_jobs - - except Exception as e: - log.error(f"Failed to scan model {self.model_id}: {e}") - raise - - -class ModelPointMutation: - """Executes a single base point mutation.""" - - def __init__( - self, - model_path: Path, - model_id: str, - chain: str, - resid: int, - ori_resname: str, - target_resname: str, - native_scores: Tuple[float, float, float, float, float], - output_mutants: bool = False, - ligand_param_fname: Union[Path, str] = "", - ligand_top_fname: Union[Path, str] = "", - ) -> None: - """ - Initialize a single point mutation job. - - Parameters - ---------- - model_path : Path - Path to the PDB file - model_id : str - Identifier for the model - chain : str - Chain identifier - resid : int - Residue number - ori_resname : str - Original residue name - target_resname : str - Target base name for mutation - native_scores : tuple - Native model scores (score, vdw, elec, desolv, bsa) - output_mutants : bool - Whether to keep mutant PDB files - ligand_param_fname : Union[Path, str] - Path to additional parameter file used by CNS - ligand_top_fname : Union[Path, str] - Path to additional topology file used by CNS - """ - self.model_path = Path(model_path) - self.model_id = model_id - self.chain = chain - self.resid = resid - self.ori_resname = ori_resname - self.target_resname = target_resname - self.native_scores = native_scores - self.output_mutants = output_mutants - self.ligand_param_fname = ligand_param_fname - self.ligand_top_fname = ligand_top_fname - - def run(self): - """Execute the point mutation.""" - mutation_id = f"{self.model_id}_{self.chain}{self.resid}{self.target_resname}" - - try: - # Setup working directory - sc_dir = f"haddock3-score-{mutation_id}" - os.makedirs(sc_dir, exist_ok=True) - - # Perform point mutation on pdb file - mut_pdb = mutate( - self.model_path, self.chain, self.resid, self.target_resname - ) - - # Calculate mutant scores - mutant_scores = calc_score( - mut_pdb, - run_dir=sc_dir, - outputpdb=self.output_mutants, - ligand_param_fname=self.ligand_param_fname, - ligand_top_fname=self.ligand_top_fname, - ) - - # Calculate deltas (native - mutant) - n_score, n_vdw, n_elec, n_des, n_bsa = self.native_scores - m_score, m_vdw, m_elec, m_des, m_bsa = mutant_scores - delta_scores = ( - n_score - m_score, - n_vdw - m_vdw, - n_elec - m_elec, - n_des - m_des, - n_bsa - m_bsa, - ) - - # Handle output files - em_mut_pdb = Path(f"{mut_pdb.stem}_hs.pdb") - if not self.output_mutants: - # if output_mutants = False, then remove both files - if os.path.exists(mut_pdb): - os.remove(mut_pdb) - if em_mut_pdb.exists(): - os.remove(em_mut_pdb) - else: - # otherwise keep energy-minimized pdb - if os.path.exists(em_mut_pdb): - shutil.move(em_mut_pdb, mut_pdb) - # clean up scoring dir - if os.path.exists(sc_dir): - shutil.rmtree(sc_dir) - - return MutationResult( - model_id=self.model_id, - chain=self.chain, - resid=self.resid, - ori_resname=self.ori_resname, - target_resname=self.target_resname, - mutant_scores=mutant_scores, - delta_scores=delta_scores, - success=True, - ) - - except Exception as e: - return MutationResult( - model_id=self.model_id, - chain=self.chain, - resid=self.resid, - ori_resname=self.ori_resname, - target_resname=self.target_resname, - mutant_scores=(0, 0, 0, 0, 0), - delta_scores=(0, 0, 0, 0, 0), - success=False, - error_msg=str(e), - ) diff --git a/tests/test_module_alascan.py b/tests/test_module_alascan.py index 2580dbc785..fbd6899207 100644 --- a/tests/test_module_alascan.py +++ b/tests/test_module_alascan.py @@ -12,7 +12,7 @@ from haddock.libs.libscan import add_zscores from haddock.modules.analysis.alascan import DEFAULT_CONFIG from haddock.modules.analysis.alascan import HaddockModule as AlascanModule -from haddock.modules.analysis.alascan.scan import ( +from haddock.modules.analysis.alascan.alascan import ( AddDeltaBFactor, calc_score, ClusterOutputer, @@ -373,7 +373,7 @@ def test_interface_scanner_run(mocker, interface_scanner): """Test InterfaceScanner.run() (in haddock mode, so returns mutation jobs).""" # mock stand-alone functions mocker.patch( - "haddock.modules.analysis.alascan.scan.calc_score", + "haddock.modules.analysis.alascan.alascan.calc_score", return_value=(-106.7, -29.6, -316.5, -13.8, 1494.7), ) mocker.patch( @@ -408,7 +408,7 @@ def test_interface_scanner_run_empty_interface(mocker, interface_scanner): "haddock.libs.libcapri.CAPRI.identify_interface", return_value={} ) # no interface mocker.patch( - "haddock.modules.analysis.alascan.scan.calc_score", + "haddock.modules.analysis.alascan.alascan.calc_score", return_value=(-106.7, -29.6, -316.5, -13.8, 1494.7), ) mocker.patch("haddock.libs.libalign.get_atoms") @@ -425,7 +425,7 @@ def test_interface_scanner_chain_filtering(mocker, params, complex_pdb): return_value={"A": [19, 20], "B": [30, 31]}, ) mocker.patch( - "haddock.modules.analysis.alascan.scan.calc_score", + "haddock.modules.analysis.alascan.alascan.calc_score", return_value=(-106.7, -29.6, -316.5, -13.8, 1494.7), ) mocker.patch("haddock.libs.libalign.get_atoms") @@ -456,7 +456,7 @@ def test_interface_scanner_residue_filtering(mocker, complex_pdb, params): return_value={"A": [19, 20, 21], "B": [30, 31, 32]}, ) mocker.patch( - "haddock.modules.analysis.alascan.scan.calc_score", + "haddock.modules.analysis.alascan.alascan.calc_score", return_value=(-113.941, -43.353, -303.753, -9.838, 1579.730), ) mocker.patch("haddock.libs.libalign.get_atoms") @@ -488,7 +488,7 @@ def test_interface_scanner_residue_filtering_not_in_interface( return_value={"A": [19, 20], "B": [30, 31]}, ) mocker.patch( - "haddock.modules.analysis.alascan.scan.calc_score", + "haddock.modules.analysis.alascan.alascan.calc_score", return_value=(-113.941, -43.353, -303.753, -9.838, 1579.730), ) mocker.patch("haddock.libs.libalign.get_atoms") @@ -506,7 +506,7 @@ def test_interface_scanner_skip_same_residue_mutation(mocker, complex_pdb, param return_value={"A": [19, 20]}, # here 19 is TRP, so should be not mutated ) mocker.patch( - "haddock.modules.analysis.alascan.scan.calc_score", + "haddock.modules.analysis.alascan.alascan.calc_score", return_value=(-113.941, -43.353, -303.753, -9.838, 1579.730), ) scanner = InterfaceScanner(model=complex_pdb, mutation_res="THR", params=params) @@ -546,12 +546,12 @@ def test_model_point_mutation_run(mocker, mutation_job, monkeypatch): with tempfile.TemporaryDirectory() as tmpdir: monkeypatch.chdir(tmpdir) mock_mutate = mocker.patch( - "haddock.modules.analysis.alascan.scan.mutate", + "haddock.modules.analysis.alascan.alascan.mutate", return_value=Path("mutant.pdb"), ) mutant_scores = (-101.3, -28.5, -344.0, -14.2, 1484.2) mock_calc_score = mocker.patch( - "haddock.modules.analysis.alascan.scan.calc_score", + "haddock.modules.analysis.alascan.alascan.calc_score", return_value=mutant_scores, ) # mock file operations @@ -587,10 +587,10 @@ def test_model_point_mutation_run_with_output_mutants_false( mutant_pdb.touch() em_mutant_pdb.touch() mocker.patch( - "haddock.modules.analysis.alascan.scan.mutate", return_value=mutant_pdb + "haddock.modules.analysis.alascan.alascan.mutate", return_value=mutant_pdb ) mocker.patch( - "haddock.modules.analysis.alascan.scan.calc_score", + "haddock.modules.analysis.alascan.alascan.calc_score", return_value=(-101.3, -28.5, -344.0, -14.2, 1484.2), ) # Mock file removal operations @@ -622,10 +622,10 @@ def test_model_point_mutation_run_with_output_mutants_true(mocker, monkeypatch): mutant_pdb.touch() em_mutant_pdb.touch() mocker.patch( - "haddock.modules.analysis.alascan.scan.mutate", return_value=mutant_pdb + "haddock.modules.analysis.alascan.alascan.mutate", return_value=mutant_pdb ) mocker.patch( - "haddock.modules.analysis.alascan.scan.calc_score", + "haddock.modules.analysis.alascan.alascan.calc_score", return_value=(-101.3, -28.5, -344.0, -14.2, 1484.2), ) # mock file operations @@ -663,11 +663,11 @@ def test_model_point_mutation_cleanup(mocker, mutation_job, monkeypatch): with tempfile.TemporaryDirectory() as tmpdir: monkeypatch.chdir(tmpdir) mocker.patch( - "haddock.modules.analysis.alascan.scan.mutate", + "haddock.modules.analysis.alascan.alascan.mutate", return_value=Path("mutant.pdb"), ) mocker.patch( - "haddock.modules.analysis.alascan.scan.calc_score", + "haddock.modules.analysis.alascan.alascan.calc_score", return_value=(-101.3, -28.5, -344.0, -14.2, 1484.2), ) mocker.patch("os.path.exists", return_value=True) diff --git a/tests/test_module_dnascan.py b/tests/test_module_dnascan.py new file mode 100644 index 0000000000..d69c8a02a3 --- /dev/null +++ b/tests/test_module_dnascan.py @@ -0,0 +1,769 @@ +"""Test the dnascan module.""" + +import os +import tempfile +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +from haddock.core.exceptions import ConfigurationError +from haddock.libs.libontology import PDBFile +from haddock.libs.libscan import add_zscores +from haddock.modules.analysis.dnascan import DEFAULT_CONFIG +from haddock.modules.analysis.dnascan import HaddockModule as DnascanModule +from haddock.modules.analysis.dnascan.dnascan import ( + AddDeltaBFactor, + calc_score, + ClusterOutputer, + COMPLEMENT, + find_base_pairs, + get_atoms_to_keep, + group_scan_by_cluster, + is_cross_type, + mutate, + validate_scan_bases, + wc_atom, + write_scan_out, + MutationResult, + InterfaceScanner, + ModelBasePairMutation, + BACKBONE_ATOMS, + PURINE_BASE_ATOMS, + PYRIMIDINE_BASE_ATOMS, + DEFAULT_SCAN_BASES, +) + +from . import golden_data + + +# The protein-DNA complex protdna_complex_1 has a protein chain A and a +# double-stranded DNA chain B. A few relevant base pairs (chain B): +# 2=DG paired with 37=DC ; 7=DA paired with 32=DT ; 5=DC paired with 34=DG + + +# fixtures +@pytest.fixture(name="dna_input_list") +def fixture_dna_input_list(): + """Prot-DNA input (two entries pointing at the same complex).""" + return [ + PDBFile(Path(golden_data, "protdna_complex_1.pdb"), path=golden_data), + PDBFile(Path(golden_data, "protdna_complex_1.pdb"), path=golden_data), + ] + + +@pytest.fixture(name="complex_pdb") +def fixture_complex_pdb(): + """Return example prot-DNA complex pdb.""" + return Path(golden_data, "protdna_complex_1.pdb") + + +@pytest.fixture(name="dna_model_list") +def fixture_dna_model_list(complex_pdb): + """Prot-DNA input.""" + return [ + PDBFile(file_name=complex_pdb, path=str(golden_data)), + ] + + +@pytest.fixture(name="params") +def fixture_params(): + """Parameter fixture to be used in the tests""" + return { + "int_cutoff": 5.0, + "bp_cutoff": 3.5, + "plot": False, + "scan_bases": ["DA", "DC", "DG", "DT"], + # resdic_ with the trailing underscore mimics the default (no user + # residue filter), so that the whole interface is scanned. + "resdic_": [], + "chains": [], + "output_mutants": False, + } + + +@pytest.fixture(name="dnascan") +def fixture_dnascan(monkeypatch): + """Return dnascan module.""" + with tempfile.TemporaryDirectory() as tmpdir: + monkeypatch.chdir(tmpdir) + yield DnascanModule( + order=1, + path=Path("."), + initial_params=DEFAULT_CONFIG, + ) + + +@pytest.fixture +def example_df_scan_clt(): + """Return example dnascan clt DataFrame.""" + example_clt_data = [ + ["B", 2, "DG", "DA", "B-2-DG-DA", -2.0, -1.0, -0.4, -2.3, -0.5, -7.2, 1.0], + ["B", 2, "DG", "DT", "B-2-DG-DT", -0.0, 1.0, -0.4, 0.8, 0.5, -7.2, 1.0], + ] + columns = [ + "chain", + "resid", + "resname", + "target_resname", + "full_resname", + "score", + "delta_score", + "delta_vdw", + "delta_elec", + "delta_desolv", + "delta_bsa", + "frac_pres", + ] + + example_df_scan_clt = pd.DataFrame(example_clt_data, columns=columns) + yield example_df_scan_clt + + +@pytest.fixture(name="interface_scanner") +def fixture_interface_scanner(dna_model_list, params): + """Interface scanner in default HADDOCK mode.""" + return InterfaceScanner( + model=dna_model_list[0], + scan_bases=["DA", "DC", "DG", "DT"], + params=params, + ) + + +@pytest.fixture(name="successful_mutation_result") +def fixture_successful_mutation_result(): + """Successful base-pair mutation result for B2 DG->DA / B37 DC->DT.""" + return MutationResult( + model_id="protdna_complex_1", + chain="B", + resid=2, + ori_resname="DG", + target_resname="DA", + partner_chain="B", + partner_resid=37, + partner_ori_resname="DC", + partner_target_resname="DT", + mutant_scores=(-108.540, -43.234, -275.271, -10.252, 1589.260), + delta_scores=(5.401, 0.119, 28.482, -0.414, 10.470), + success=True, + ) + + +@pytest.fixture(name="successful_mutation_result_2") +def fixture_successful_mutation_result_2(): + """Successful base-pair mutation result for B2 DG->DC / B37 DC->DG.""" + return MutationResult( + model_id="protdna_complex_1", + chain="B", + resid=2, + ori_resname="DG", + target_resname="DC", + partner_chain="B", + partner_resid=37, + partner_ori_resname="DC", + partner_target_resname="DG", + mutant_scores=(-18.540, -41.234, -270.271, -11.252, 589.260), + delta_scores=(4.401, 0.109, 27.482, -0.3, 0.470), + success=True, + ) + + +@pytest.fixture(name="results_by_model") +def fixture_results_by_model(successful_mutation_result, successful_mutation_result_2): + return { + "protdna_complex_1": [ + successful_mutation_result, + successful_mutation_result_2, + ], + } + + +# tests start here +def test_init(dnascan): + dnascan.__init__( + order=42, + path=Path("0_anything"), + initial_params=DEFAULT_CONFIG, + ) + assert dnascan.path == Path("0_anything") + assert dnascan._origignal_config_file == DEFAULT_CONFIG + assert isinstance(dnascan.params, dict) + assert len(dnascan.params) != 0 + # scan_bases default must contain all four DNA bases + assert set(dnascan.params["scan_bases"]) == {"DA", "DC", "DG", "DT"} + + +def test_confirm_installation(dnascan): + assert dnascan.confirm_installation() is None + + +"""Test validate_scan_bases guard""" + + +def test_validate_scan_bases_canonical(): + """Canonical DNA names are accepted and returned unchanged.""" + assert validate_scan_bases(["DA", "DC", "DG", "DT"]) == ["DA", "DC", "DG", "DT"] + + +def test_validate_scan_bases_case_insensitive_and_dedup(): + """Lower-case/mixed entries are normalised and duplicates removed.""" + assert validate_scan_bases(["dg", "DA", "DG"]) == ["DG", "DA"] + + +def test_validate_scan_bases_rejects_one_letter_rna_names(): + """One-letter names (RNA bases) must be rejected.""" + with pytest.raises(ConfigurationError): + validate_scan_bases(["A", "C", "G", "T"]) + with pytest.raises(ConfigurationError): + validate_scan_bases(["DA", "U"]) + + +def test_validate_scan_bases_rejects_unknown(): + """Unknown bases must be rejected.""" + with pytest.raises(ConfigurationError): + validate_scan_bases(["DA", "X"]) + + +def test_validate_scan_bases_rejects_empty(): + """An empty list must be rejected.""" + with pytest.raises(ConfigurationError): + validate_scan_bases([]) + + +"""Test base-pair detection""" + + +def test_wc_atom(): + """Purines pair through N1, pyrimidines through N3.""" + assert wc_atom("DA") == "N1" + assert wc_atom("DG") == "N1" + assert wc_atom("DC") == "N3" + assert wc_atom("DT") == "N3" + + +def test_find_base_pairs_real_duplex(complex_pdb): + """Watson-Crick base pairs are detected in a real DNA duplex.""" + from haddock.libs.libalign import get_atoms, load_coords + + atoms = get_atoms(complex_pdb) + coords, _ = load_coords(complex_pdb, atoms, add_resname=True) + pairs = find_base_pairs(coords) + # canonical pairs of the duplex + assert pairs[("B", 2)] == ("B", 37, "DC") # DG:DC + assert pairs[("B", 7)] == ("B", 32, "DT") # DA:DT + # pairing must be symmetric + assert pairs[("B", 37)] == ("B", 2, "DG") + + +def test_find_base_pairs_only_complementary_ring_types(): + """A purine can only be paired with a pyrimidine.""" + coords = { + # two purines close together must NOT be paired to each other + ("B", 1, "N1", "DA"): np.array([0.0, 0.0, 0.0]), + ("B", 2, "N1", "DG"): np.array([0.0, 0.0, 2.0]), + } + assert find_base_pairs(coords) == {} + + +"""Test InterfaceScanner""" + + +def test_interface_scanner_init_default_bases(): + """Default scan_bases should be the four DNA bases.""" + scanner = InterfaceScanner(model=Path("test.pdb")) + assert scanner.scan_bases == DEFAULT_SCAN_BASES + assert scanner.params == {} + assert scanner.filter_resdic == {} + + +def test_interface_scanner_double_mutations(mocker, interface_scanner): + """Each interface base pair yields double-mutation jobs with a WC partner.""" + mocker.patch( + "haddock.modules.analysis.dnascan.dnascan.calc_score", + return_value=(-106.7, -29.6, -316.5, -13.8, 1494.7), + ) + # A protein residue (A44) plus two DNA nucleotides that pair with each + # other (B7:B32) and one DNA nucleotide whose partner (B37) is not itself + # at the interface. + mocker.patch( + "haddock.libs.libcapri.CAPRI.identify_interface", + return_value={"A": [44], "B": [2, 7, 32]}, + ) + mutation_jobs = interface_scanner.run() + + # all jobs are base-pair (double) mutations + assert all(isinstance(job, ModelBasePairMutation) for job in mutation_jobs) + # the protein residue must be ignored: only DNA chain B is mutated + assert {job.chain for job in mutation_jobs} == {"B"} + # WT base must never appear as a target and partner must be its complement + for job in mutation_jobs: + assert job.ori_resname != job.target_resname + assert job.partner_target_resname == COMPLEMENT[job.target_resname] + # B7 and B32 form a single base pair: it must be scanned only once, from + # the first-encountered nucleotide (B7). B32 must never be a primary. + assert 32 not in {job.resid for job in mutation_jobs} + # base pair B2:B37 -> 3 jobs (DG to DA/DC/DT); base pair B7:B32 -> 3 jobs + assert len(mutation_jobs) == 6 + # B2 (DG) mutated to A, C, T (not G); partner B37 always mutated + b2_targets = {j.target_resname for j in mutation_jobs if j.resid == 2} + assert b2_targets == {"DA", "DC", "DT"} + assert all(j.partner_resid == 37 for j in mutation_jobs if j.resid == 2) + + +def test_interface_scanner_skips_unpaired(mocker, dna_model_list, params): + """DNA nucleotides without a base-pairing partner are skipped.""" + scanner = InterfaceScanner( + model=dna_model_list[0], + params=params, + ) + mocker.patch( + "haddock.modules.analysis.dnascan.dnascan.calc_score", + return_value=(-106.7, -29.6, -316.5, -13.8, 1494.7), + ) + mocker.patch("haddock.modules.analysis.dnascan.dnascan.get_atoms") + # a single isolated nucleotide with no complementary partner + mocker.patch( + "haddock.modules.analysis.dnascan.dnascan.load_coords", + return_value=( + {("B", 5, "N1", "DG"): np.array([0.0, 0.0, 0.0])}, + {}, + ), + ) + mocker.patch( + "haddock.libs.libcapri.CAPRI.identify_interface", + return_value={"B": [5]}, + ) + mutation_jobs = scanner.run() + assert mutation_jobs == [] + + +def test_interface_scanner_restrict_scan_bases(mocker, dna_model_list, params): + """Restricting scan_bases limits the number of mutations.""" + scanner = InterfaceScanner( + model=dna_model_list[0], + scan_bases=["DA"], + params=params, + ) + mocker.patch( + "haddock.modules.analysis.dnascan.dnascan.calc_score", + return_value=(-106.7, -29.6, -316.5, -13.8, 1494.7), + ) + mocker.patch( + "haddock.libs.libcapri.CAPRI.identify_interface", + return_value={"B": [2]}, + ) + mutation_jobs = scanner.run() + # B2 is DG -> only the DG->DA mutation remains (partner DC->DT) + assert len(mutation_jobs) == 1 + assert mutation_jobs[0].resid == 2 + assert mutation_jobs[0].target_resname == "DA" + assert mutation_jobs[0].partner_target_resname == "DT" + + +"""Test stand-alone functions""" + + +def test_get_atoms_to_keep_same_ring_type(): + """Same-type mutations keep the shared base ring atoms.""" + # purine -> purine + keep = get_atoms_to_keep("DG", "DA") + assert set(BACKBONE_ATOMS).issubset(keep) + assert set(PURINE_BASE_ATOMS).issubset(keep) + assert not set(PYRIMIDINE_BASE_ATOMS).issubset(keep) + # pyrimidine -> pyrimidine + keep = get_atoms_to_keep("DT", "DC") + assert set(PYRIMIDINE_BASE_ATOMS).issubset(keep) + # N9/C8 (purine-only) must not be kept for a pyrimidine mutation + assert "N9" not in keep + assert "C8" not in keep + + +def test_get_atoms_to_keep_cross_ring_type(): + """Cross-type mutations keep the backbone plus renamed anchor atoms.""" + # purine -> pyrimidine: keep glycosidic anchors, renamed N9->N1, C4->C2, C8->C6 + keep = get_atoms_to_keep("DG", "DC") + assert set(BACKBONE_ATOMS).issubset(keep) + assert keep["N9"] == "N1" + assert keep["C4"] == "C2" + assert keep["C8"] == "C6" + assert set(keep) - set(BACKBONE_ATOMS) == {"N9", "C4", "C8"} + # pyrimidine -> purine: keep glycosidic anchors, renamed N1->N9, C2->C4, C6->C8 + keep = get_atoms_to_keep("DT", "DA") + assert set(BACKBONE_ATOMS).issubset(keep) + assert keep["N1"] == "N9" + assert keep["C2"] == "C4" + assert keep["C6"] == "C8" + assert set(keep) - set(BACKBONE_ATOMS) == {"N1", "C2", "C6"} + + +def test_backbone_excludes_ribose_hydroxyl(): + """The DNA (deoxyribose) backbone must NOT contain the 2'-hydroxyl O2'.""" + assert "O2'" not in BACKBONE_ATOMS + assert "C2'" in BACKBONE_ATOMS + + +def test_mutate_double_cross_type(dna_model_list, monkeypatch): + """A base-pair mutation mutates both nucleotides at once.""" + with tempfile.TemporaryDirectory() as tmpdir: + mut_fname = Path(golden_data, dna_model_list[0].file_name) + monkeypatch.chdir(path=tmpdir) + # B2 is DG (purine); mutate to DC (pyrimidine, cross-type). Partner B37 + # is DC and must be mutated to the complement of DC, i.e. DG. + mut_pdb_fname = mutate(mut_fname, "B", 2, "DC", "B", 37, "DG") + + assert mut_pdb_fname == Path("protdna_complex_1-B_G2C-C37G.pdb") + assert os.path.exists(mut_pdb_fname) + + primary_atoms, partner_atoms = [], [] + with open(mut_pdb_fname) as fh: + for line in fh: + if not line.startswith("ATOM") or line[21] != "B": + continue + resid = int(line[22:26]) + atom = line[12:16].strip().replace("*", "'") + if resid == 2: + assert line[17:20] == " DC" # DG -> DC + primary_atoms.append(atom) + elif resid == 37: + assert line[17:20] == " DG" # DC -> DG + partner_atoms.append(atom) + # cross-type primary DG -> DC (purine -> pyrimidine): the glycosidic + # anchors are kept and renamed N9 -> N1, C4 -> C2, C8 -> C6 + assert "N9" not in primary_atoms + assert "C8" not in primary_atoms + assert set(a for a in primary_atoms if a not in BACKBONE_ATOMS) == { + "N1", + "C2", + "C6", + } + assert "C1'" in primary_atoms + # cross-type partner DC -> DG (pyrimidine -> purine): anchors renamed + # N1 -> N9, C2 -> C4, C6 -> C8 + assert set(a for a in partner_atoms if a not in BACKBONE_ATOMS) == { + "N9", + "C4", + "C8", + } + # DNA backbone has no 2'-hydroxyl + assert "O2'" not in primary_atoms + + +def test_mutate_double_same_type_keeps_ring(dna_model_list, monkeypatch): + """A purine->purine base-pair mutation keeps the shared ring atoms.""" + with tempfile.TemporaryDirectory() as tmpdir: + mut_fname = Path(golden_data, dna_model_list[0].file_name) + monkeypatch.chdir(path=tmpdir) + # B2 DG -> DA (purine->purine) ; partner B37 DC -> DT (pyrimidine) + mut_pdb_fname = mutate(mut_fname, "B", 2, "DA", "B", 37, "DT") + + primary_atoms = [] + with open(mut_pdb_fname) as fh: + for line in fh: + if ( + line.startswith("ATOM") + and line[21] == "B" + and int(line[22:26]) == 2 + ): + assert line[17:20] == " DA" + primary_atoms.append(line[12:16].strip().replace("*", "'")) + # shared purine ring atoms preserved to keep base orientation + for atom in PURINE_BASE_ATOMS: + assert atom in primary_atoms + # guanine-specific atoms O6/N2 dropped (rebuilt as N6 for A) + assert "O6" not in primary_atoms + assert "N2" not in primary_atoms + + +def test_mutate_raises_on_bad_base(dna_model_list, monkeypatch): + """An unmappable target base raises a KeyError.""" + with tempfile.TemporaryDirectory() as tmpdir: + mut_fname = Path(golden_data, dna_model_list[0].file_name) + monkeypatch.chdir(path=tmpdir) + with pytest.raises(KeyError): + mutate(mut_fname, "B", 2, "HOH", "B", 37, "DT") + + +"""Test the base-pair (double) mutation execution""" + + +def test_is_cross_type(): + """A cross-type mutation changes the base ring type.""" + assert is_cross_type("DG", "DC") is True # purine -> pyrimidine + assert is_cross_type("DC", "DG") is True # pyrimidine -> purine + assert is_cross_type("DG", "DA") is False # purine -> purine + assert is_cross_type("DC", "DT") is False # pyrimidine -> pyrimidine + + +# One-pass and two-pass wild-type baselines used by the run tests. They are +# deliberately different so that the tests can verify which baseline is used. +_NATIVE_1STEP = (-100.0, -10.0, -250.0, -5.0, 1000.0) +_NATIVE_2STEP = (-105.0, -11.0, -255.0, -6.0, 1050.0) + + +def _make_bp_job(ori, target, p_ori, p_target, output_mutants=False): + """Helper to build a ModelBasePairMutation with fixed native scores.""" + return ModelBasePairMutation( + model_path=Path("model.pdb"), + model_id="m", + chain="B", + resid=2, + ori_resname=ori, + target_resname=target, + partner_chain="B", + partner_resid=37, + partner_ori_resname=p_ori, + partner_target_resname=p_target, + native_scores=_NATIVE_1STEP, + native_scores_2step=_NATIVE_2STEP, + output_mutants=output_mutants, + ) + + +def test_bp_mutation_same_type_single_cns_call(mocker, monkeypatch): + """A same-ring-type base-pair mutation uses a single CNS call.""" + with tempfile.TemporaryDirectory() as tmpdir: + monkeypatch.chdir(tmpdir) + + def fake_mutate(pdb_f, tc, tr, tt, pc, pr, pt): + p = Path("mut.pdb") + p.write_text("ATOM\n") + return p + + mocker.patch( + "haddock.modules.analysis.dnascan.dnascan.mutate", side_effect=fake_mutate + ) + mock_calc = mocker.patch( + "haddock.modules.analysis.dnascan.dnascan.calc_score", + return_value=(-90.0, -9.0, -200.0, -4.0, 900.0), + ) + # DG->DA (purine->purine), partner DC->DT (pyrimidine->pyrimidine) + result = _make_bp_job("DG", "DA", "DC", "DT").run() + assert result.success + assert mock_calc.call_count == 1 + assert result.mutant_scores == (-90.0, -9.0, -200.0, -4.0, 900.0) + # same-type mutants use the one-pass wild-type baseline + assert result.delta_scores[0] == pytest.approx(_NATIVE_1STEP[0] - (-90.0)) + + +def test_bp_mutation_cross_type_two_cns_calls(mocker, monkeypatch): + """A cross-type base-pair mutation uses two sequential CNS calls.""" + with tempfile.TemporaryDirectory() as tmpdir: + monkeypatch.chdir(tmpdir) + + applied = [] + + def fake_mutate_residues(pdb_f, mutations): + applied.append(dict(mutations)) + (chain, resid), target = next(iter(mutations.items())) + p = Path(f"step_{chain}{resid}{target}.pdb") + p.write_text("ATOM\n") + return p + + mocker.patch( + "haddock.modules.analysis.dnascan.dnascan._mutate_residues", + side_effect=fake_mutate_residues, + ) + + step_scores = iter( + [ + (-50.0, -5.0, -100.0, -2.0, 500.0), # step 1 (discarded) + (-90.0, -9.0, -200.0, -4.0, 900.0), # step 2 (kept) + ] + ) + + def fake_calc(pdb_f, run_dir=None, outputpdb=False, **kwargs): + if outputpdb: + Path(f"{Path(pdb_f).stem}_hs.pdb").write_text("ATOM\n") + return next(step_scores) + + mock_calc = mocker.patch( + "haddock.modules.analysis.dnascan.dnascan.calc_score", + side_effect=fake_calc, + ) + # DG->DC (purine->pyrimidine), partner DC->DG (pyrimidine->purine) + result = _make_bp_job("DG", "DC", "DC", "DG").run() + + assert result.success + assert mock_calc.call_count == 2 + # the scores/energies of the second (final) CNS call are kept + assert result.mutant_scores == (-90.0, -9.0, -200.0, -4.0, 900.0) + # cross-type mutants are compared to the two-pass wild-type baseline + assert result.delta_scores[0] == pytest.approx(_NATIVE_2STEP[0] - (-90.0)) + # step 1 mutates the purine->pyrimidine nucleotide (primary DG->DC) + assert applied[0] == {("B", 2): "DC"} + # step 2 mutates the pyrimidine->purine nucleotide (partner DC->DG) + assert applied[1] == {("B", 37): "DG"} + # the first CNS call regularises the intermediate (writes minimized pdb) + assert mock_calc.call_args_list[0].kwargs["outputpdb"] is True + + +def test_bp_mutation_cross_type_step_order_partner_purine(mocker, monkeypatch): + """When the partner is the purine->pyrimidine one, it is mutated first.""" + with tempfile.TemporaryDirectory() as tmpdir: + monkeypatch.chdir(tmpdir) + + applied = [] + + def fake_mutate_residues(pdb_f, mutations): + applied.append(dict(mutations)) + (chain, resid), target = next(iter(mutations.items())) + p = Path(f"step_{chain}{resid}{target}.pdb") + p.write_text("ATOM\n") + return p + + mocker.patch( + "haddock.modules.analysis.dnascan.dnascan._mutate_residues", + side_effect=fake_mutate_residues, + ) + + def fake_calc(pdb_f, run_dir=None, outputpdb=False, **kwargs): + if outputpdb: + Path(f"{Path(pdb_f).stem}_hs.pdb").write_text("ATOM\n") + return (-90.0, -9.0, -200.0, -4.0, 900.0) + + mocker.patch( + "haddock.modules.analysis.dnascan.dnascan.calc_score", side_effect=fake_calc + ) + # primary DC->DG (pyrimidine->purine), partner DG->DC (purine->pyrimidine) + result = _make_bp_job("DC", "DG", "DG", "DC").run() + + assert result.success + # step 1 is the purine->pyrimidine one, which here is the partner (B37) + assert applied[0] == {("B", 37): "DC"} + # step 2 is the pyrimidine->purine primary (B2) + assert applied[1] == {("B", 2): "DG"} + + +def test_compute_native_baselines_two_passes( + mocker, dna_model_list, params, monkeypatch +): + """The wild-type baseline is scored with one and with two CNS passes.""" + with tempfile.TemporaryDirectory() as tmpdir: + monkeypatch.chdir(tmpdir) + scanner = InterfaceScanner(model=dna_model_list[0], params=params) + + scores = iter([(-100.0, -10.0, -250.0, -5.0, 1000.0), _NATIVE_2STEP]) + + def fake_calc(pdb_f, run_dir=None, outputpdb=False, **kwargs): + if outputpdb: + Path(f"{Path(pdb_f).stem}_hs.pdb").write_text("ATOM\n") + return next(scores) + + mock_calc = mocker.patch( + "haddock.modules.analysis.dnascan.dnascan.calc_score", side_effect=fake_calc + ) + native, native_2step = scanner._compute_native_baselines() + + assert mock_calc.call_count == 2 + assert native == (-100.0, -10.0, -250.0, -5.0, 1000.0) + assert native_2step == _NATIVE_2STEP + # the first pass writes the energy-minimized WT, the second re-scores it + assert mock_calc.call_args_list[0].kwargs["outputpdb"] is True + assert mock_calc.call_args_list[1].kwargs["outputpdb"] is False + # the intermediate minimized wild-type PDB is cleaned up + assert not Path(f"{Path(scanner.model_path).stem}_hs.pdb").exists() + + +def test_write_delta_score_to_pdb(dna_model_list, results_by_model, monkeypatch): + """Both nucleotides of a base pair receive the (mean) delta score.""" + with tempfile.TemporaryDirectory() as tmpdir: + mut_fname = Path(golden_data, dna_model_list[0].file_name) + monkeypatch.chdir(path=tmpdir) + mut_pdb_fname = mutate(mut_fname, "B", 2, "DA", "B", 37, "DT") + dna_model_list[0].file_name = mut_pdb_fname + dna_model_list[0].path = tmpdir + add_delta_to_bfactor_obj = AddDeltaBFactor( + dna_model_list[0], + tmpdir, + results_by_model["protdna_complex_1"], + ) + add_delta_to_bfactor_obj.reorder_results() + expected = np.mean([5.401, 4.401]) + # both members of the base pair (B2 and its partner B37) get the score + assert np.isclose(add_delta_to_bfactor_obj.model_results["B-2"], expected) + assert np.isclose(add_delta_to_bfactor_obj.model_results["B-37"], expected) + + +def test_add_zscores(example_df_scan_clt): + """Test the add_zscores function.""" + obs_df_scan_clt = add_zscores(example_df_scan_clt) + assert np.isclose(obs_df_scan_clt["z_score"].values[0], -1.0) + assert np.isclose(obs_df_scan_clt["z_score"].values[1], 1.0) + + +def test_calc_score(mocker): + """Test the calc_score function.""" + mocker.patch( + "haddock.libs.libscan.get_score_string", + return_value=[ + "> starting calculations...", + "> HADDOCK-score = (1.0 * vdw) + (0.2 * elec) + (1.0 * desolv) + (0.0 * air) + (0.0 * bsa)", + "> HADDOCK-score (emscoring) = -106.7376", + "> vdw=-29.5808,elec=-316.542,desolv=-13.8484,air=0.0,bsa=1494.73", + ], + ) + scores = calc_score(Path(golden_data, "protdna_complex_1.pdb"), run_dir="tmp") + assert scores == (-106.7376, -29.5808, -316.542, -13.8484, 1494.73) + + +def test_dnascan_group_scan_by_cluster(dna_input_list, results_by_model, monkeypatch): + """Group scan data per cluster, keyed by mutation, with partner metadata.""" + with tempfile.TemporaryDirectory() as tmpdir: + monkeypatch.chdir(tmpdir) + clt_scan, clt_pops = group_scan_by_cluster(dna_input_list, results_by_model) + assert "unclustered" in clt_scan.keys() + assert clt_pops["unclustered"] == 2 + # The two mutations of base pair B2 must be reported separately + idents = set(clt_scan["unclustered"].keys()) + assert "B-2-DG-DA" in idents + assert "B-2-DG-DC" in idents + # partner metadata is carried along + entry = clt_scan["unclustered"]["B-2-DG-DA"] + assert entry["partner_chain"] == "B" + assert entry["partner_resid"] == 37 + assert entry["partner_target_resname"] == "DT" + + +def test_dnascan_cluster_full_outputs(dna_input_list, results_by_model, monkeypatch): + """Test ClusterOutputer writes tsv and plot.""" + with tempfile.TemporaryDirectory() as tmpdir: + monkeypatch.chdir(tmpdir) + clt_scan, clt_pops = group_scan_by_cluster(dna_input_list, results_by_model) + ClusterOutputer( + clt_scan["unclustered"], + "unclustered", + clt_pops["unclustered"], + scan_residue="DNA base pair", + generate_plot=True, + offline=False, + ).run() + tsv = Path("scan_clt_unclustered.tsv") + assert tsv.exists() + assert Path("scan_clt_unclustered.html").exists() + # partner columns must be present in the cluster tsv + df = pd.read_csv(tsv, sep="\t", comment="#") + assert "partner_target_resname" in df.columns + + +def test_write_scan_out_with_mutation_results( + successful_mutation_result, successful_mutation_result_2, monkeypatch +): + """Test write_scan_out with base-pair (double) mutation results.""" + with tempfile.TemporaryDirectory() as tmpdir: + monkeypatch.chdir(tmpdir) + results = [successful_mutation_result, successful_mutation_result_2] + write_scan_out(results, "protdna_complex_1") + output_file = Path("scan_protdna_complex_1.tsv") + assert output_file.exists() + content = output_file.read_text() + assert "# `dnascan` results for protdna_complex_1" in content + assert "delta_score" in content + assert "z_score" in content + df = pd.read_csv(output_file, sep="\t", comment="#") + assert len(df) == 2 + assert df["ori_resname"].tolist() == ["DG", "DG"] + assert set(df["end_resname"].tolist()) == {"DA", "DC"} + # partner (double mutation) columns are reported + assert df["partner_res"].tolist() == [37, 37] + assert set(df["partner_end_resname"].tolist()) == {"DT", "DG"} diff --git a/tests/test_module_rnascan.py b/tests/test_module_rnascan.py index a3a1e2c0c1..3ca25cb576 100644 --- a/tests/test_module_rnascan.py +++ b/tests/test_module_rnascan.py @@ -13,7 +13,7 @@ from haddock.libs.libscan import add_zscores from haddock.modules.analysis.rnascan import DEFAULT_CONFIG from haddock.modules.analysis.rnascan import HaddockModule as RnascanModule -from haddock.modules.analysis.rnascan.scan import ( +from haddock.modules.analysis.rnascan.rnascan import ( AddDeltaBFactor, calc_score, ClusterOutputer, @@ -233,7 +233,7 @@ def test_interface_scanner_init_default_bases(): def test_interface_scanner_run_four_mutations(mocker, interface_scanner): """Each interface nucleotide yields one job per non-WT base.""" mocker.patch( - "haddock.modules.analysis.rnascan.scan.calc_score", + "haddock.modules.analysis.rnascan.rnascan.calc_score", return_value=(-106.7, -29.6, -316.5, -13.8, 1494.7), ) mocker.patch( @@ -269,7 +269,7 @@ def test_interface_scanner_skips_non_rna(mocker, rna_model_list, params): params={**params, "resdic_B": [3], "resdic_A": [40]}, ) mocker.patch( - "haddock.modules.analysis.rnascan.scan.calc_score", + "haddock.modules.analysis.rnascan.rnascan.calc_score", return_value=(-106.7, -29.6, -316.5, -13.8, 1494.7), ) mocker.patch( @@ -301,7 +301,7 @@ def test_interface_scanner_restrict_scan_bases(mocker, rna_model_list, params): params=params, ) mocker.patch( - "haddock.modules.analysis.rnascan.scan.calc_score", + "haddock.modules.analysis.rnascan.rnascan.calc_score", return_value=(-106.7, -29.6, -316.5, -13.8, 1494.7), ) mocker.patch(