-
Notifications
You must be signed in to change notification settings - Fork 1
Add analysis for symmetry corrected RMSD #92
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
Merged
Merged
Changes from all commits
Commits
Show all changes
54 commits
Select commit
Hold shift + click to select a range
2977c01
Refactor RMSD analyses into MDAnalysis AnaysisBase classes
hannahbaumann 4a960a0
Combine RMSD analyses
hannahbaumann afa829a
Merge branch 'main' into rmsd_refactor_analysisbase
hannahbaumann 19ddaba
First attempt at implementing a symmetry corrected RMSD
hannahbaumann df017da
Add reference and superposition option
hannahbaumann d824ab6
Apply suggestion from @hannahbaumann
hannahbaumann 71a11d3
Merge branch 'main' into rmsd_refactor_analysisbase
hannahbaumann 5cabcf4
Merge branch 'main' into rmsd_refactor_analysisbase
hannahbaumann e16b2e3
Some changes
hannahbaumann 616bf42
Fix mda 2D analysis example
hannahbaumann 90ed39e
Add RMSD test using mda test data
hannahbaumann 7d6e49f
Some fixes
hannahbaumann 8820fa5
Merge branch 'rmsd_refactor_analysisbase' into spyrmsd
hannahbaumann 60478a9
Some updates
hannahbaumann 0b50d46
Merge branch 'spyrmsd' of https://github.com/OpenFreeEnergy/openfe_an…
hannahbaumann 4bf598f
Some more updates
hannahbaumann 3adcddf
Merge branch 'rmsd_refactor_analysisbase' into spyrmsd
hannahbaumann c73260b
Add tests symmetry RMSD
hannahbaumann 80b984d
Update src/openfe_analysis/rmsd.py
hannahbaumann db61dc4
Address review comments
hannahbaumann f22d96f
Fix docs build
hannahbaumann 4ccb734
Merge branch 'rmsd_refactor_analysisbase' into spyrmsd
hannahbaumann fdf4531
Add MDAnalysisTests as test dependency
hannahbaumann 2b4f9f5
Merge branch 'rmsd_refactor_analysisbase' into spyrmsd
hannahbaumann a1022a2
Add utils for state atom extraction and fixing elements in a state
hannahbaumann aae4531
small update
hannahbaumann e9947a6
Merge branch 'main' into rmsd_refactor_analysisbase
hannahbaumann 4e8be20
Merge branch 'rmsd_refactor_analysisbase' into spyrmsd
hannahbaumann 3c001a1
Add news entry
hannahbaumann adf2b37
Merge branch 'rmsd_refactor_analysisbase' into spyrmsd
hannahbaumann 5e55ecb
Add spyrmsd
hannahbaumann e4c5417
add more tests for classes
hannahbaumann 1035634
Merge branch 'rmsd_refactor_analysisbase' into spyrmsd
hannahbaumann b4600d8
Remove element fix function
hannahbaumann c5a84b7
Merge branch 'spyrmsd' of https://github.com/OpenFreeEnergy/openfe_an…
hannahbaumann bf02611
Restructure tests
hannahbaumann c96fd8e
Update test symm corr
hannahbaumann 984bcbb
Update tests
hannahbaumann 42c3017
Update src/openfe_analysis/utils/universe_utils.py
hannahbaumann da0e65c
Merge branch 'rmsd_refactor_analysisbase' into spyrmsd
hannahbaumann 26f04f5
Small fix
hannahbaumann eafa23e
Add error for missing bonds
hannahbaumann 19e24f5
Merge branch 'main' into spyrmsd
hannahbaumann ca9f3d6
fix failing tests
hannahbaumann 5988460
Merge branch 'main' into spyrmsd
hannahbaumann c134726
update fixture to make test more stable
hannahbaumann ed313ee
Merge branch 'spyrmsd' of https://github.com/OpenFreeEnergy/openfe_an…
hannahbaumann 9c91222
merge conflicts
hannahbaumann c6b3b03
Merge branch 'main' into spyrmsd
hannahbaumann b08671a
fix tests
hannahbaumann 9ddee0f
Merge branch 'spyrmsd' of https://github.com/OpenFreeEnergy/openfe_an…
hannahbaumann b6b05c0
Apply suggestion from @hannahbaumann
hannahbaumann e8d8a02
Add error if rdkit mol and atomgroup have different number of atoms
hannahbaumann 53d718b
Move atm number check
hannahbaumann File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,3 +7,5 @@ dependencies: | |
| - python=3.12 | ||
| - sphinx | ||
| - furo | ||
| - spyrmsd | ||
| - rdkit | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,3 +18,5 @@ dependencies: | |
| - pytest-cov | ||
| - pytest-xdist | ||
| - pytest-rerunfailures | ||
| - spyrmsd | ||
| - rdkit | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,9 +5,10 @@ | |
| import MDAnalysis as mda | ||
| import netCDF4 as nc | ||
| import numpy as np | ||
| import spyrmsd.rmsd as srmsd | ||
| from MDAnalysis.analysis import rms | ||
| from MDAnalysis.analysis.base import AnalysisBase | ||
| from MDAnalysis.transformations import unwrap | ||
| from rdkit import Chem | ||
|
|
||
| from .utils.apply_transformations import apply_alignment_transformations | ||
| from .utils.universe_utils import create_universe_single_state | ||
|
|
@@ -130,6 +131,85 @@ def _single_frame(self) -> None: | |
| ) | ||
|
|
||
|
|
||
| class SymmetryCorrectedLigandRMSD(AnalysisBase): | ||
| """ | ||
| Symmetry-corrected 1D RMSD time series for a ligand AtomGroup. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| atomgroup : mda.AtomGroup | ||
| Ligand atoms to compute RMSD for. If ``rdmol`` is not provided, | ||
| bonds must be guessed on the atomgroup before instantiating this | ||
|
jthorton marked this conversation as resolved.
|
||
| class; use :func:`guess_ligand_bonds` for this purpose. | ||
| rdmol : Chem.Mol, optional | ||
| RDKit molecule corresponding to ``atomgroup``. If provided, it is | ||
| used directly and ``guess_ligand_bonds`` does not need to be called. | ||
| If ``None``, the RDKit molecule is derived from ``atomgroup`` via | ||
| ``convert_to("RDKIT")``. | ||
|
jthorton marked this conversation as resolved.
|
||
|
|
||
| Raises | ||
| ------ | ||
| ValueError | ||
| If ``rdmol`` is ``None`` and no bonds are found on the atomgroup. | ||
| ValueError | ||
| If the number of atoms in ``atomgroup`` and ``rdmol`` do not match. | ||
| """ | ||
|
|
||
|
hannahbaumann marked this conversation as resolved.
|
||
| _analysis_algorithm_is_parallelizable = False | ||
|
|
||
| def __init__( | ||
| self, | ||
| atomgroup: mda.AtomGroup, | ||
| rdmol: Optional[Chem.Mol] = None, | ||
| **kwargs, | ||
| ): | ||
| super().__init__(atomgroup.universe.trajectory, **kwargs) | ||
| self._ag = atomgroup | ||
| if rdmol is None: | ||
| try: | ||
| has_bonds = len(atomgroup.bonds) > 0 | ||
| except mda.exceptions.NoDataError: | ||
| has_bonds = False | ||
| if not has_bonds: | ||
| raise ValueError( | ||
| "No bonds found on atomgroup. Call guess_ligand_bonds() " | ||
| "before instantiating SymmetryCorrectedLigandRMSD, or " | ||
| "pass an rdmol directly." | ||
| ) | ||
| else: | ||
| if len(atomgroup) != rdmol.GetNumAtoms(): | ||
| raise ValueError( | ||
| f"atomgroup has {len(atomgroup)} atoms but rdmol has " | ||
| f"{rdmol.GetNumAtoms()} atoms." | ||
| ) | ||
| self._mol = rdmol if rdmol is not None else atomgroup.convert_to("RDKIT") | ||
| self._aprops = np.array([atom.GetAtomicNum() for atom in self._mol.GetAtoms()]) | ||
|
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. It might be good to add a check here that ensures that the atomgroup has the same number of atoms as the rdmol. |
||
| self._am = Chem.rdmolops.GetAdjacencyMatrix(self._mol) | ||
|
|
||
| def _prepare(self): | ||
| self.results.rmsd = np.zeros(self.n_frames, dtype=np.float64) | ||
| # reference is taken from the first analyzed frame, not necessarily frame 0 | ||
| self._reference = self._ag.positions.copy() | ||
| self._isomorphisms: list | None = None | ||
|
|
||
| def _single_frame(self) -> None: | ||
| frame_rmsd, isomorphisms, _ = srmsd._rmsd_isomorphic_core( | ||
| coords1=self._ag.positions.copy(), | ||
| coords2=self._reference, | ||
| aprops1=self._aprops, | ||
| aprops2=self._aprops, | ||
| am1=self._am, | ||
| am2=self._am, | ||
| center=False, | ||
| minimize=False, | ||
| isomorphisms=self._isomorphisms, | ||
| ) | ||
| self.results.rmsd[self._frame_index] = frame_rmsd | ||
| # cache isomorphisms after first frame to avoid redundant graph matching | ||
| if self._isomorphisms is None: | ||
| self._isomorphisms = isomorphisms | ||
|
|
||
|
|
||
| class LigandCOMDrift(AnalysisBase): | ||
| """ | ||
| Ligand center-of-mass displacement from initial position. | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| import pytest | ||
|
|
||
| from openfe_analysis.utils import apply_transformations | ||
| from openfe_analysis.utils.universe_utils import ( | ||
| create_universe_single_state, | ||
| guess_ligand_bonds, | ||
| select_state_atoms, | ||
| ) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def universe(hybrid_system_skipped_pdb, simulation_skipped_nc): | ||
| universe = create_universe_single_state( | ||
| hybrid_system_skipped_pdb, simulation_skipped_nc, state=0 | ||
| ) | ||
| prot = universe.select_atoms("protein and name CA") | ||
| ligand = universe.select_atoms("resname UNK") | ||
| apply_transformations.apply_alignment_transformations(universe, prot, ligand) | ||
| yield universe | ||
| universe.trajectory.close() | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def ligand_ag(hybrid_system_skipped_pdb, simulation_skipped_nc): | ||
| universe = create_universe_single_state( | ||
| hybrid_system_skipped_pdb, simulation_skipped_nc, state=0 | ||
| ) | ||
| prot = universe.select_atoms("protein and name CA") | ||
| ligand = universe.select_atoms("resname UNK") | ||
| apply_transformations.apply_alignment_transformations(universe, prot, ligand) | ||
| ag = select_state_atoms(universe, end_state="A").select_atoms("resname UNK") | ||
| yield ag | ||
| universe.trajectory.close() | ||
|
|
||
|
|
||
| def test_guess_ligand_bonds_adds_bonds(ligand_ag): | ||
| """Bonds should be present on the atomgroup after guess_ligand_bonds.""" | ||
| original_count = len(ligand_ag.bonds) | ||
| # This also has stateB bond | ||
| assert original_count == 49 | ||
| guess_ligand_bonds(ligand_ag, delete_existing=True) | ||
| # Now only 48 stateA bonds | ||
| assert len(ligand_ag.bonds) == 48 | ||
|
|
||
|
|
||
| def test_guess_ligand_bonds_modifies_universe_inplace(ligand_ag): | ||
| """Bond topology should be reflected on the parent universe after guessing.""" | ||
| guess_ligand_bonds(ligand_ag) | ||
| universe_bonds = ligand_ag.universe.select_atoms("resname UNK").bonds | ||
| assert len(universe_bonds) > 0 | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "end_state, expected_bfactors", | ||
| [ | ||
| ("A", (0.25, 0.5)), | ||
| ("B", (0.75, 0.5)), | ||
| ], | ||
| ) | ||
| def test_select_state_atoms(universe, end_state, expected_bfactors): | ||
| """State selection should include state-unique and shared atoms.""" | ||
| state = select_state_atoms(universe, end_state=end_state) | ||
| assert len(state) > 0 | ||
| assert all(atom.bfactor in expected_bfactors for atom in state) | ||
|
|
||
|
|
||
| def test_select_state_atoms_invalid_state(universe): | ||
| """Invalid end_state should raise a ValueError.""" | ||
| with pytest.raises(ValueError, match="end_state must be 'A' or 'B'"): | ||
| select_state_atoms(universe, end_state="C") | ||
|
|
||
|
|
||
| def test_select_state_atoms_shared_atoms(universe): | ||
| """Shared atoms (bfactor 0.5) should appear in both state A and B selections.""" | ||
| state_a = select_state_atoms(universe, end_state="A") | ||
| state_b = select_state_atoms(universe, end_state="B") | ||
| shared_a = set(atom.ix for atom in state_a if atom.bfactor == 0.5) | ||
| shared_b = set(atom.ix for atom in state_b if atom.bfactor == 0.5) | ||
| assert shared_a == shared_b |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.