-
Notifications
You must be signed in to change notification settings - Fork 2
Add BoreschRestraintAnalysis class #123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4227a6c
4c56f09
6086cd5
8e00c67
2187f23
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ../../examples/boresch_analysis.ipynb |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,3 +10,4 @@ Trajectory Analysis | |
| :maxdepth: 1 | ||
|
|
||
| structural_analysis | ||
| boresch_analysis | ||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import MDAnalysis as mda | ||
| import numpy as np | ||
| from MDAnalysis.analysis.base import AnalysisBase | ||
|
|
||
|
|
||
| class BoreschRestraintAnalysis(AnalysisBase): | ||
| """ | ||
| Boresch restraint geometry (bond, 2 angles, 3 dihedrals) time series. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| atomgroup : mda.AtomGroup | ||
| Exactly 6 atoms, ordered [H0, H1, H2, G0, G1, G2] (host atoms then | ||
| guest atoms), matching the atom ordering used by | ||
| ``openfe.protocols.restraint_utils.geometry.boresch``. | ||
|
|
||
| Raises | ||
| ------ | ||
| ValueError | ||
| If ``atomgroup`` does not contain exactly 6 atoms. | ||
|
|
||
| Notes | ||
| ----- | ||
| Bond length is reported in Angstrom; angles and dihedrals are reported | ||
| in radians, matching the units returned by MDAnalysis's | ||
| ``calc_bonds``/``calc_angles``/``calc_dihedrals``. Reported quantities | ||
| are, in order: the H0-G0 bond, the H1-H0-G0 and H0-G0-G1 angles, and the | ||
| H2-H1-H0-G0, H1-H0-G0-G1, and H0-G0-G1-G2 dihedrals. | ||
| """ | ||
|
|
||
| _analysis_algorithm_is_parallelizable = False | ||
|
|
||
| def __init__(self, atomgroup: mda.AtomGroup, **kwargs): | ||
| super().__init__(atomgroup.universe.trajectory, **kwargs) | ||
| if len(atomgroup) != 6: | ||
| raise ValueError( | ||
| f"atomgroup must contain exactly 6 atoms (3 host + 3 guest), got {len(atomgroup)}." | ||
| ) | ||
|
Comment on lines
+35
to
+38
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it make sense to check that there are no bonds between the H and G atoms or that they are in different residues?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [personal take] I could go either way, but I personally wouldn't bother (i.e. flexibility is always good). |
||
| self._ag = atomgroup | ||
|
|
||
| def _prepare(self) -> None: | ||
| self.results.bond = np.zeros(self.n_frames, dtype=np.float64) | ||
| self.results.angle1 = np.zeros(self.n_frames, dtype=np.float64) | ||
| self.results.angle2 = np.zeros(self.n_frames, dtype=np.float64) | ||
| self.results.dihedral1 = np.zeros(self.n_frames, dtype=np.float64) | ||
| self.results.dihedral2 = np.zeros(self.n_frames, dtype=np.float64) | ||
| self.results.dihedral3 = np.zeros(self.n_frames, dtype=np.float64) | ||
|
|
||
| def _single_frame(self) -> None: | ||
| atoms = self._ag.atoms | ||
| box = self._ag.dimensions | ||
|
|
||
| self.results.bond[self._frame_index] = mda.lib.distances.calc_bonds( | ||
| atoms[0].position, | ||
| atoms[3].position, | ||
| box=box, | ||
| ) | ||
|
|
||
| self.results.angle1[self._frame_index] = mda.lib.distances.calc_angles( | ||
| atoms[1].position, | ||
| atoms[0].position, | ||
| atoms[3].position, | ||
| box=box, | ||
| ) | ||
| self.results.angle2[self._frame_index] = mda.lib.distances.calc_angles( | ||
| atoms[0].position, | ||
| atoms[3].position, | ||
| atoms[4].position, | ||
| box=box, | ||
| ) | ||
|
|
||
| self.results.dihedral1[self._frame_index] = mda.lib.distances.calc_dihedrals( | ||
| atoms[2].position, | ||
| atoms[1].position, | ||
| atoms[0].position, | ||
| atoms[3].position, | ||
| box=box, | ||
| ) | ||
| self.results.dihedral2[self._frame_index] = mda.lib.distances.calc_dihedrals( | ||
| atoms[1].position, | ||
| atoms[0].position, | ||
| atoms[3].position, | ||
| atoms[4].position, | ||
| box=box, | ||
| ) | ||
| self.results.dihedral3[self._frame_index] = mda.lib.distances.calc_dihedrals( | ||
| atoms[0].position, | ||
| atoms[3].position, | ||
| atoms[4].position, | ||
| atoms[5].position, | ||
| box=box, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import MDAnalysis as mda | ||
| import pytest | ||
| from MDAnalysis.lib.distances import calc_angles, calc_bonds, calc_dihedrals | ||
| from MDAnalysisTests.datafiles import DCD, PSF | ||
| from numpy.testing import assert_allclose | ||
|
|
||
| from openfe_analysis.restraints import BoreschRestraintAnalysis | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def mda_universe(): | ||
| return mda.Universe(PSF, DCD) | ||
|
|
||
|
|
||
| class TestBoreschRestraintAnalysis: | ||
| def test_matches_direct_mda_calls(self, mda_universe): | ||
| atoms = mda_universe.atoms[[10, 20, 30, 40, 50, 60]] | ||
| result = BoreschRestraintAnalysis(atoms).run(step=25) | ||
|
|
||
| expected = { | ||
| "bond": [], | ||
| "angle1": [], | ||
| "angle2": [], | ||
| "dihedral1": [], | ||
| "dihedral2": [], | ||
| "dihedral3": [], | ||
| } | ||
| for _ in mda_universe.trajectory[::25]: | ||
| box = atoms.dimensions | ||
| expected["bond"].append(calc_bonds(atoms[0].position, atoms[3].position, box=box)) | ||
| expected["angle1"].append( | ||
| calc_angles(atoms[1].position, atoms[0].position, atoms[3].position, box=box) | ||
| ) | ||
| expected["angle2"].append( | ||
| calc_angles(atoms[0].position, atoms[3].position, atoms[4].position, box=box) | ||
| ) | ||
| expected["dihedral1"].append( | ||
| calc_dihedrals( | ||
| atoms[2].position, | ||
| atoms[1].position, | ||
| atoms[0].position, | ||
| atoms[3].position, | ||
| box=box, | ||
| ) | ||
| ) | ||
| expected["dihedral2"].append( | ||
| calc_dihedrals( | ||
| atoms[1].position, | ||
| atoms[0].position, | ||
| atoms[3].position, | ||
| atoms[4].position, | ||
| box=box, | ||
| ) | ||
| ) | ||
| expected["dihedral3"].append( | ||
| calc_dihedrals( | ||
| atoms[0].position, | ||
| atoms[3].position, | ||
| atoms[4].position, | ||
| atoms[5].position, | ||
| box=box, | ||
| ) | ||
| ) | ||
|
|
||
| assert_allclose(result.results.bond, expected["bond"]) | ||
| assert_allclose(result.results.angle1, expected["angle1"]) | ||
| assert_allclose(result.results.angle2, expected["angle2"]) | ||
| assert_allclose(result.results.dihedral1, expected["dihedral1"]) | ||
| assert_allclose(result.results.dihedral2, expected["dihedral2"]) | ||
| assert_allclose(result.results.dihedral3, expected["dihedral3"]) | ||
|
|
||
| @pytest.mark.parametrize("n_atoms", [5, 7]) | ||
| def test_raises_on_wrong_atom_count(self, mda_universe, n_atoms): | ||
| with pytest.raises(ValueError, match="exactly 6 atoms"): | ||
| BoreschRestraintAnalysis(mda_universe.atoms[:n_atoms]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something to think about doing (not here) is to make the final results united.