|
| 1 | +# This code is part of OpenFE and is licensed under the MIT license. |
| 2 | +# For details, see https://github.com/OpenFreeEnergy/openfe |
| 3 | +""" |
| 4 | +Restraint Geometry classes |
| 5 | +
|
| 6 | +TODO |
| 7 | +---- |
| 8 | +* Add relevant duecredit entries. |
| 9 | +""" |
| 10 | +from typing import Optional |
| 11 | + |
| 12 | +import MDAnalysis as mda |
| 13 | +from gufe.vendor.openff.models.types import FloatQuantity |
| 14 | +from MDAnalysis.lib.distances import calc_angles, calc_bonds, calc_dihedrals |
| 15 | +from openfe.protocols.restraint_utils.geometry.base import HostGuestRestraintGeometry |
| 16 | +from openff.units import Quantity, unit |
| 17 | +from rdkit import Chem |
| 18 | + |
| 19 | +from .guest import find_guest_atom_candidates |
| 20 | +from .host import find_host_anchor, find_host_atom_candidates |
| 21 | + |
| 22 | + |
| 23 | +class BoreschRestraintGeometry(HostGuestRestraintGeometry): |
| 24 | + """ |
| 25 | + A class that defines the restraint geometry for a Boresch restraint. |
| 26 | +
|
| 27 | + The restraint is defined by the following: |
| 28 | +
|
| 29 | + H2 G2 |
| 30 | + - - |
| 31 | + - - |
| 32 | + H1 - - H0 -- G0 - - G1 |
| 33 | +
|
| 34 | + Where HX represents the X index of ``host_atoms`` and GX |
| 35 | + the X index of ``guest_atoms``. |
| 36 | + """ |
| 37 | + |
| 38 | + r_aA0: FloatQuantity["nanometer"] |
| 39 | + """ |
| 40 | + The equilibrium distance between H0 and G0. |
| 41 | + """ |
| 42 | + theta_A0: FloatQuantity["radians"] |
| 43 | + """ |
| 44 | + The equilibrium angle value between H1, H0, and G0. |
| 45 | + """ |
| 46 | + theta_B0: FloatQuantity["radians"] |
| 47 | + """ |
| 48 | + The equilibrium angle value between H0, G0, and G1. |
| 49 | + """ |
| 50 | + phi_A0: FloatQuantity["radians"] |
| 51 | + """ |
| 52 | + The equilibrium dihedral value between H2, H1, H0, and G0. |
| 53 | + """ |
| 54 | + phi_B0: FloatQuantity["radians"] |
| 55 | + |
| 56 | + """ |
| 57 | + The equilibrium dihedral value between H1, H0, G0, and G1. |
| 58 | + """ |
| 59 | + phi_C0: FloatQuantity["radians"] |
| 60 | + |
| 61 | + """ |
| 62 | + The equilibrium dihedral value between H0, G0, G1, and G2. |
| 63 | + """ |
| 64 | + |
| 65 | + |
| 66 | +def _get_restraint_distances( |
| 67 | + atomgroup: mda.AtomGroup, |
| 68 | +) -> tuple[Quantity, Quantity, Quantity, Quantity, Quantity, Quantity]: |
| 69 | + """ |
| 70 | + Get the bond, angle, and dihedral distances for an input atomgroup |
| 71 | + defining the six atoms for a Boresch-like restraint. |
| 72 | +
|
| 73 | + The atoms must be in the order of H0, H1, H2, G0, G1, G2. |
| 74 | +
|
| 75 | + Parameters |
| 76 | + ---------- |
| 77 | + atomgroup : mda.AtomGroup |
| 78 | + An AtomGroup defining the restrained atoms in order. |
| 79 | +
|
| 80 | + Returns |
| 81 | + ------- |
| 82 | + bond : openff.units.Quantity |
| 83 | + The H0-G0 bond value. |
| 84 | + angle1 : openff.units.Quantity |
| 85 | + The H1-H0-G0 angle value. |
| 86 | + angle2 : openff.units.Quantity |
| 87 | + The H0-G0-G1 angle value. |
| 88 | + dihed1 : openff.units.Quantity |
| 89 | + The H2-H1-H0-G0 dihedral value. |
| 90 | + dihed2 : openff.units.Quantity |
| 91 | + The H1-H0-G0-G1 dihedral value. |
| 92 | + dihed3 : openff.units.Quantity |
| 93 | + The H0-G0-G1-G2 dihedral value. |
| 94 | + """ |
| 95 | + bond = ( |
| 96 | + calc_bonds( |
| 97 | + atomgroup.atoms[0].position, |
| 98 | + atomgroup.atoms[3].position, |
| 99 | + box=atomgroup.dimensions, |
| 100 | + ) |
| 101 | + * unit.angstroms |
| 102 | + ) |
| 103 | + |
| 104 | + angles = [] |
| 105 | + for idx_set in [[1, 0, 3], [0, 3, 4]]: |
| 106 | + angle = calc_angles( |
| 107 | + atomgroup.atoms[idx_set[0]].position, |
| 108 | + atomgroup.atoms[idx_set[1]].position, |
| 109 | + atomgroup.atoms[idx_set[2]].position, |
| 110 | + box=atomgroup.dimensions, |
| 111 | + ) |
| 112 | + angles.append(angle * unit.radians) |
| 113 | + |
| 114 | + dihedrals = [] |
| 115 | + for idx_set in [[2, 1, 0, 3], [1, 0, 3, 4], [0, 3, 4, 5]]: |
| 116 | + dihed = calc_dihedrals( |
| 117 | + atomgroup.atoms[idx_set[0]].position, |
| 118 | + atomgroup.atoms[idx_set[1]].position, |
| 119 | + atomgroup.atoms[idx_set[2]].position, |
| 120 | + atomgroup.atoms[idx_set[3]].position, |
| 121 | + box=atomgroup.dimensions, |
| 122 | + ) |
| 123 | + dihedrals.append(dihed * unit.radians) |
| 124 | + |
| 125 | + return bond, angles[0], angles[1], dihedrals[0], dihedrals[1], dihedrals[2] |
| 126 | + |
| 127 | + |
| 128 | +def find_boresch_restraint( |
| 129 | + universe: mda.Universe, |
| 130 | + guest_rdmol: Chem.Mol, |
| 131 | + guest_idxs: list[int], |
| 132 | + host_idxs: list[int], |
| 133 | + guest_restraint_atoms_idxs: Optional[list[int]] = None, |
| 134 | + host_restraint_atoms_idxs: Optional[list[int]] = None, |
| 135 | + host_selection: str = "all", |
| 136 | + dssp_filter: bool = False, |
| 137 | + rmsf_cutoff: Quantity = 0.1 * unit.nanometer, |
| 138 | + host_min_distance: Quantity = 1 * unit.nanometer, |
| 139 | + host_max_distance: Quantity = 3 * unit.nanometer, |
| 140 | + angle_force_constant: Quantity = ( |
| 141 | + 83.68 * unit.kilojoule_per_mole / unit.radians**2 |
| 142 | + ), |
| 143 | + temperature: Quantity = 298.15 * unit.kelvin, |
| 144 | +) -> BoreschRestraintGeometry: |
| 145 | + """ |
| 146 | + Find suitable Boresch-style restraints between a host and guest entity |
| 147 | + based on the approach of Baumann et al. [1] with some modifications. |
| 148 | +
|
| 149 | + Parameters |
| 150 | + ---------- |
| 151 | + universe : mda.Universe |
| 152 | + An MDAnalysis Universe defining the system and its coordinates. |
| 153 | + guest_rdmol : Chem.Mol |
| 154 | + An RDKit Mol for the guest molecule. |
| 155 | + guest_idxs : list[int] |
| 156 | + Indices in the topology for the guest molecule. |
| 157 | + host_idxs : list[int] |
| 158 | + Indices in the topology for the host molecule. |
| 159 | + guest_restraint_atoms_idxs : Optional[list[int]] |
| 160 | + User selected indices of the guest molecule itself (i.e. indexed |
| 161 | + starting a 0 for the guest molecule). This overrides the |
| 162 | + restraint search and a restraint using these indices will |
| 163 | + be returned. Must be defined alongside ``host_restraint_atoms_idxs``. |
| 164 | + host_restraint_atoms_idxs : Optional[list[int]] |
| 165 | + User selected indices of the host molecule itself (i.e. indexed |
| 166 | + starting a 0 for the hosts molecule). This overrides the |
| 167 | + restraint search and a restraint using these indices will |
| 168 | + be returned. Must be defined alongside ``guest_restraint_atoms_idxs``. |
| 169 | + host_selection : str |
| 170 | + An MDAnalysis selection string to sub-select the host atoms. |
| 171 | + dssp_filter : bool |
| 172 | + Whether or not to filter the host atoms by their secondary structure. |
| 173 | + rmsf_cutoff : openff.units.Quantity |
| 174 | + The cutoff value for atom root mean square fluctuation. Atoms with RMSF |
| 175 | + values above this cutoff will be disregarded. |
| 176 | + Must be in units compatible with nanometer. |
| 177 | + host_min_distance : openff.units.Quantity |
| 178 | + The minimum distance between any host atom and the guest G0 atom. |
| 179 | + Must be in units compatible with nanometer. |
| 180 | + host_max_distance : openff.units.Quantity |
| 181 | + The maximum distance between any host atom and the guest G0 atom. |
| 182 | + Must be in units compatible with nanometer. |
| 183 | + angle_force_constant : openff.units.Quantity |
| 184 | + The force constant for the G1-G0-H0 and G0-H0-H1 angles. Must be |
| 185 | + in units compatible with kilojoule / mole / radians ** 2. |
| 186 | + temperature : openff.units.Quantity |
| 187 | + The system temperature in units compatible with Kelvin. |
| 188 | +
|
| 189 | + Returns |
| 190 | + ------- |
| 191 | + BoreschRestraintGeometry |
| 192 | + An object defining the parameters of the Boresch-like restraint. |
| 193 | +
|
| 194 | + References |
| 195 | + ---------- |
| 196 | + [1] Baumann, Hannah M., et al. "Broadening the scope of binding free energy |
| 197 | + calculations using a Separated Topologies approach." (2023). |
| 198 | + """ |
| 199 | + if (guest_restraint_atoms_idxs is not None) and (host_restraint_atoms_idxs is not None): # fmt: skip |
| 200 | + # In this case assume the picked atoms were intentional / |
| 201 | + # representative of the input and go with it |
| 202 | + guest_ag = universe.atoms[guest_idxs] |
| 203 | + guest_atoms = [at.ix for at in guest_ag.atoms[guest_restraint_atoms_idxs]] |
| 204 | + host_ag = universe.atoms[host_idxs] |
| 205 | + host_atoms = [at.ix for at in host_ag.atoms[host_restraint_atoms_idxs]] |
| 206 | + |
| 207 | + # Set the equilibrium values as those of the final frame |
| 208 | + universe.trajectory[-1] |
| 209 | + atomgroup = universe.atoms[host_atoms + guest_atoms] |
| 210 | + bond, ang1, ang2, dih1, dih2, dih3 = _get_restraint_distances(atomgroup) |
| 211 | + |
| 212 | + # TODO: add checks to warn if this is a badly picked |
| 213 | + # set of atoms. |
| 214 | + return BoreschRestraintGeometry( |
| 215 | + host_atoms=host_atoms, |
| 216 | + guest_atoms=guest_atoms, |
| 217 | + r_aA0=bond, |
| 218 | + theta_A0=ang1, |
| 219 | + theta_B0=ang2, |
| 220 | + phi_A0=dih1, |
| 221 | + phi_B0=dih2, |
| 222 | + phi_C0=dih3, |
| 223 | + ) |
| 224 | + |
| 225 | + if (guest_restraint_atoms_idxs is not None) ^ (host_restraint_atoms_idxs is not None): # fmt: skip |
| 226 | + # This is not an intended outcome, crash out here |
| 227 | + errmsg = ( |
| 228 | + "both ``guest_restraints_atoms_idxs`` and " |
| 229 | + "``host_restraint_atoms_idxs`` " |
| 230 | + "must be set or both must be None. " |
| 231 | + f"Got {guest_restraint_atoms_idxs} and {host_restraint_atoms_idxs}" |
| 232 | + ) |
| 233 | + raise ValueError(errmsg) |
| 234 | + |
| 235 | + # 1. Fetch the guest anchors |
| 236 | + guest_anchors = find_guest_atom_candidates( |
| 237 | + universe=universe, |
| 238 | + rdmol=guest_rdmol, |
| 239 | + guest_idxs=guest_idxs, |
| 240 | + rmsf_cutoff=rmsf_cutoff, |
| 241 | + ) |
| 242 | + |
| 243 | + if len(guest_anchors) == 0: |
| 244 | + errmsg = "No suitable ligand atoms found for the restraint." |
| 245 | + raise ValueError(errmsg) |
| 246 | + |
| 247 | + # 2. We then loop through the guest anchors to find suitable host atoms |
| 248 | + for guest_anchor in guest_anchors: |
| 249 | + # We next fetch the host atom pool |
| 250 | + # Note: return is a set, so need to convert it later on |
| 251 | + host_pool = find_host_atom_candidates( |
| 252 | + universe=universe, |
| 253 | + host_idxs=host_idxs, |
| 254 | + l1_idx=guest_anchor[0], |
| 255 | + host_selection=host_selection, |
| 256 | + dssp_filter=dssp_filter, |
| 257 | + rmsf_cutoff=rmsf_cutoff, |
| 258 | + min_distance=host_min_distance, |
| 259 | + max_distance=host_max_distance, |
| 260 | + ) |
| 261 | + |
| 262 | + host_anchor = find_host_anchor( |
| 263 | + guest_atoms=universe.atoms[list(guest_anchor)], |
| 264 | + host_atom_pool=universe.atoms[list(host_pool)], |
| 265 | + minimum_distance=0.5 * unit.nanometer, |
| 266 | + angle_force_constant=angle_force_constant, |
| 267 | + temperature=temperature, |
| 268 | + ) |
| 269 | + # continue if it's empty, otherwise stop |
| 270 | + if host_anchor is not None: |
| 271 | + break |
| 272 | + |
| 273 | + if host_anchor is None: |
| 274 | + errmsg = "No suitable host atoms could be found" |
| 275 | + raise ValueError(errmsg) |
| 276 | + |
| 277 | + # Set the equilibrium values as those of the final frame |
| 278 | + universe.trajectory[-1] |
| 279 | + atomgroup = universe.atoms[list(host_anchor) + list(guest_anchor)] |
| 280 | + bond, ang1, ang2, dih1, dih2, dih3 = _get_restraint_distances(atomgroup) |
| 281 | + |
| 282 | + return BoreschRestraintGeometry( |
| 283 | + host_atoms=list(host_anchor), |
| 284 | + guest_atoms=list(guest_anchor), |
| 285 | + r_aA0=bond, |
| 286 | + theta_A0=ang1, |
| 287 | + theta_B0=ang2, |
| 288 | + phi_A0=dih1, |
| 289 | + phi_B0=dih2, |
| 290 | + phi_C0=dih3, |
| 291 | + ) |
0 commit comments