Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
2977c01
Refactor RMSD analyses into MDAnalysis AnaysisBase classes
hannahbaumann Feb 20, 2026
4a960a0
Combine RMSD analyses
hannahbaumann Feb 23, 2026
afa829a
Merge branch 'main' into rmsd_refactor_analysisbase
hannahbaumann Feb 23, 2026
19ddaba
First attempt at implementing a symmetry corrected RMSD
hannahbaumann Feb 27, 2026
df017da
Add reference and superposition option
hannahbaumann Mar 2, 2026
d824ab6
Apply suggestion from @hannahbaumann
hannahbaumann Mar 2, 2026
71a11d3
Merge branch 'main' into rmsd_refactor_analysisbase
hannahbaumann Mar 3, 2026
5cabcf4
Merge branch 'main' into rmsd_refactor_analysisbase
hannahbaumann Apr 13, 2026
e16b2e3
Some changes
hannahbaumann Apr 13, 2026
616bf42
Fix mda 2D analysis example
hannahbaumann Apr 13, 2026
90ed39e
Add RMSD test using mda test data
hannahbaumann Apr 13, 2026
7d6e49f
Some fixes
hannahbaumann May 19, 2026
8820fa5
Merge branch 'rmsd_refactor_analysisbase' into spyrmsd
hannahbaumann May 19, 2026
60478a9
Some updates
hannahbaumann May 19, 2026
0b50d46
Merge branch 'spyrmsd' of https://github.com/OpenFreeEnergy/openfe_an…
hannahbaumann May 19, 2026
4bf598f
Some more updates
hannahbaumann May 19, 2026
3adcddf
Merge branch 'rmsd_refactor_analysisbase' into spyrmsd
hannahbaumann May 19, 2026
c73260b
Add tests symmetry RMSD
hannahbaumann May 19, 2026
80b984d
Update src/openfe_analysis/rmsd.py
hannahbaumann May 22, 2026
db61dc4
Address review comments
hannahbaumann May 22, 2026
f22d96f
Fix docs build
hannahbaumann May 22, 2026
4ccb734
Merge branch 'rmsd_refactor_analysisbase' into spyrmsd
hannahbaumann May 22, 2026
fdf4531
Add MDAnalysisTests as test dependency
hannahbaumann May 22, 2026
2b4f9f5
Merge branch 'rmsd_refactor_analysisbase' into spyrmsd
hannahbaumann May 22, 2026
a1022a2
Add utils for state atom extraction and fixing elements in a state
hannahbaumann May 22, 2026
aae4531
small update
hannahbaumann May 22, 2026
e9947a6
Merge branch 'main' into rmsd_refactor_analysisbase
hannahbaumann May 22, 2026
4e8be20
Merge branch 'rmsd_refactor_analysisbase' into spyrmsd
hannahbaumann May 22, 2026
3c001a1
Add news entry
hannahbaumann May 26, 2026
adf2b37
Merge branch 'rmsd_refactor_analysisbase' into spyrmsd
hannahbaumann May 26, 2026
5e55ecb
Add spyrmsd
hannahbaumann May 26, 2026
e4c5417
add more tests for classes
hannahbaumann May 26, 2026
1035634
Merge branch 'rmsd_refactor_analysisbase' into spyrmsd
hannahbaumann May 26, 2026
d04bef5
Add utils function to correct elements for hybrid topology analysis
hannahbaumann May 26, 2026
b4600d8
Remove element fix function
hannahbaumann May 26, 2026
c5a84b7
Merge branch 'spyrmsd' of https://github.com/OpenFreeEnergy/openfe_an…
hannahbaumann May 26, 2026
20e138f
Merge branch 'spyrmsd' into correct_elements_stateb
hannahbaumann May 26, 2026
bf02611
Restructure tests
hannahbaumann May 26, 2026
c96fd8e
Update test symm corr
hannahbaumann May 26, 2026
984bcbb
Update tests
hannahbaumann May 26, 2026
42c3017
Update src/openfe_analysis/utils/universe_utils.py
hannahbaumann May 26, 2026
da0e65c
Merge branch 'rmsd_refactor_analysisbase' into spyrmsd
hannahbaumann May 26, 2026
26f04f5
Small fix
hannahbaumann May 26, 2026
eafa23e
Add error for missing bonds
hannahbaumann May 26, 2026
6bb4274
add missing imports
hannahbaumann May 26, 2026
fca32c7
More imports
hannahbaumann May 26, 2026
433a2ac
Merge branch 'spyrmsd' into correct_elements_stateb
hannahbaumann May 26, 2026
236d1a8
Apply suggestion from @hannahbaumann
hannahbaumann May 26, 2026
19e24f5
Merge branch 'main' into spyrmsd
hannahbaumann May 27, 2026
ca9f3d6
fix failing tests
hannahbaumann May 27, 2026
aa61816
Merge branch 'spyrmsd' into correct_elements_stateb
hannahbaumann May 27, 2026
5988460
Merge branch 'main' into spyrmsd
hannahbaumann May 27, 2026
c134726
update fixture to make test more stable
hannahbaumann May 27, 2026
ed313ee
Merge branch 'spyrmsd' of https://github.com/OpenFreeEnergy/openfe_an…
hannahbaumann May 27, 2026
9c91222
merge conflicts
hannahbaumann May 27, 2026
c6b3b03
Merge branch 'main' into spyrmsd
hannahbaumann May 27, 2026
b08671a
fix tests
hannahbaumann May 27, 2026
9ddee0f
Merge branch 'spyrmsd' of https://github.com/OpenFreeEnergy/openfe_an…
hannahbaumann May 27, 2026
82e22d0
Merge branch 'spyrmsd' into correct_elements_stateb
hannahbaumann May 27, 2026
bfd64c2
Merge branch 'main' into correct_elements_stateb
hannahbaumann May 28, 2026
065500f
small fix
hannahbaumann May 28, 2026
0be05e0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/openfe_analysis/rmsd.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,11 @@ def gather_rms_data(
output["protein_2D_RMSD"].append(prot_rmsd2d.results.rmsd2d)

if ligand:
# For now, leave it at the normal RMSD
lig_rmsd = RMSDAnalysis(ligand, mass_weighted=True).run(step=skip)
# state_lig = select_state_atoms(u, end_state="A").select_atoms("resname UNK")
# guess_ligand_bonds(state_lig, delete_existing=True)
# lig_rmsd = SymmetryCorrectedLigandRMSD(state_lig).run(step=skip)
output["ligand_RMSD"].append(lig_rmsd.results.rmsd)

lig_com_drift = LigandCOMDrift(ligand).run(step=skip)
Expand Down
107 changes: 107 additions & 0 deletions src/openfe_analysis/tests/utils/test_universe_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import MDAnalysis as mda
import numpy as np
import pytest
from rdkit import Chem

from openfe_analysis.utils import apply_transformations
from openfe_analysis.utils.universe_utils import (
correct_elements,
create_universe_single_state,
guess_ligand_bonds,
select_state_atoms,
Expand Down Expand Up @@ -77,3 +81,106 @@ def test_select_state_atoms_shared_atoms(universe):
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


def test_correct_elements_fixes_element():
"""correct_elements should update element where rdmol differs."""

# Build a minimal universe with a C atom
u = mda.Universe.empty(2, n_residues=1, trajectory=True)
u.add_TopologyAttr("elements", ["C", "C"]) # second atom is wrong
u.add_TopologyAttr("names", ["C1", "C2"])
u.add_TopologyAttr("resnames", ["UNK"])
u.add_TopologyAttr("resids", [1])
u.load_new(
np.array([[[0.0, 0.0, 0.0], [1.5, 0.0, 0.0]]]),
order="fac",
)
ag = u.select_atoms("all")

mol = Chem.RWMol()
mol.AddAtom(Chem.Atom(6)) # C
mol.AddAtom(Chem.Atom(7)) # N
rdmol = mol.GetMol()

with pytest.warns(UserWarning, match="No atom_mapping provided"):
correct_elements(ag, rdmol)

assert ag[0].element == "C"
assert ag[1].element == "N"
assert ag[1].name == "N"


def test_correct_elements_no_change_when_correct():
"""correct_elements should not modify atoms that already have correct elements."""

u = mda.Universe.empty(2, n_residues=1, trajectory=True)
u.add_TopologyAttr("elements", ["C", "N"])
u.add_TopologyAttr("names", ["C1", "N1"])
u.add_TopologyAttr("resnames", ["UNK"])
u.add_TopologyAttr("resids", [1])
u.load_new(
np.array([[[0.0, 0.0, 0.0], [1.5, 0.0, 0.0]]]),
order="fac",
)
ag = u.select_atoms("all")

mol = Chem.RWMol()
mol.AddAtom(Chem.Atom(6)) # C
mol.AddAtom(Chem.Atom(7)) # N
rdmol = mol.GetMol()

with pytest.warns(UserWarning, match="No atom_mapping provided"):
correct_elements(ag, rdmol)

assert ag[0].element == "C"
assert ag[0].name == "C1" # name unchanged
assert ag[1].element == "N"
assert ag[1].name == "N1" # name unchanged


def test_correct_elements_with_atom_mapping():
"""correct_elements with atom_mapping should use mapping"""

u = mda.Universe.empty(2, n_residues=1, trajectory=True)
u.add_TopologyAttr("elements", ["C", "C"]) # second atom is wrong
u.add_TopologyAttr("names", ["C1", "C2"])
u.add_TopologyAttr("resnames", ["UNK"])
u.add_TopologyAttr("resids", [1])
u.load_new(
np.array([[[0.0, 0.0, 0.0], [1.5, 0.0, 0.0]]]),
order="fac",
)
ag = u.select_atoms("all")

# rdmol has atoms in reverse order: N, C
mol = Chem.RWMol()
mol.AddAtom(Chem.Atom(7)) # N at rdmol index 0
mol.AddAtom(Chem.Atom(6)) # C at rdmol index 1
rdmol = mol.GetMol()

# explicitly map ag index 0 -> rdmol index 1 (C), ag index 1 -> rdmol index 0 (N)
correct_elements(ag, rdmol, atom_mapping={0: 1, 1: 0})

assert ag[0].element == "C" # mapped to rdmol index 1 (C)
assert ag[1].element == "N" # mapped to rdmol index 0 (N)
assert ag[1].name == "N"


def test_correct_elements_raises_size_error():
"""correct_elements should raise ValueError if atom counts don't match."""

u = mda.Universe.empty(2, n_residues=1, trajectory=True)
u.add_TopologyAttr("elements", ["C", "N"])
u.add_TopologyAttr("names", ["C1", "N1"])
u.add_TopologyAttr("resnames", ["UNK"])
u.add_TopologyAttr("resids", [1])
u.load_new(np.array([[[0.0, 0.0, 0.0], [1.5, 0.0, 0.0]]]), order="fac")
ag = u.select_atoms("all")

mol = Chem.RWMol()
mol.AddAtom(Chem.Atom(6)) # only 1 atom
rdmol = mol.GetMol()

with pytest.raises(ValueError, match="atomgroup has 2 atoms but rdmol has 1"):
correct_elements(ag, rdmol)
61 changes: 61 additions & 0 deletions src/openfe_analysis/utils/universe_utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from __future__ import annotations

import warnings
from pathlib import Path
from typing import Literal

import MDAnalysis as mda
import netCDF4 as nc
from MDAnalysis.guesser.tables import vdwradii as MDA_VDWRADII
from rdkit import Chem

from ..reader import FEReader

Expand Down Expand Up @@ -92,6 +94,65 @@ def guess_ligand_bonds(
atomgroup.guess_bonds(vdwradii)


def correct_elements(
atomgroup: mda.AtomGroup,
rdmol: Chem.Mol,
atom_mapping: dict[int, int] | None = None,
) -> None:
Comment on lines +97 to +101

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great, it might be worthing checking you can round trip a ligand, so for a real Htop output fix stateB which uses different elements and connectivity, then compare the input SMC with the fixed atomgroup and check that the atoms and bonds match exactly.

"""
Correct element and atom names in an AtomGroup in-place
using an RDKit molecule.

This is needed for hybrid topologies where mapped atoms that
undergo element changes carry state A's element types, even when
state B's ligand is selected.

Parameters
----------
atomgroup : mda.AtomGroup
Ligand atoms whose elements and names will be corrected.
rdmol : Chem.Mol
RDKit molecule with the correct element and atom name information.
atom_mapping : dict[int, int], optional
A mapping of ``{atomgroup_index: rdmol_index}`` defining the
correspondence between atoms in ``atomgroup`` and ``rdmol``. If
``None``, atoms are matched by position which gives wrong results if
the atom order was not the same.

Raises
------
ValueError
If the number of atoms in ``atomgroup`` and ``rdmol`` do not match.
"""
periodic_table = Chem.GetPeriodicTable()

if len(atomgroup) != rdmol.GetNumAtoms():
raise ValueError(
f"atomgroup has {len(atomgroup)} atoms but rdmol has {rdmol.GetNumAtoms()} atoms."
)

if atom_mapping is not None:
for ag_idx, rd_idx in atom_mapping.items():
mda_atom = atomgroup[ag_idx]
rd_atom = rdmol.GetAtomWithIdx(rd_idx)
element = periodic_table.GetElementSymbol(rd_atom.GetAtomicNum())
if mda_atom.element != element:
mda_atom.element = element
mda_atom.name = rd_atom.GetSymbol()
else:
warnings.warn(
"No atom_mapping provided to correct_elements. Assuming that "
"atom ordering is the same between atomgroup and rdmol. This may "
"give incorrect results if the atom ordering differs between the two.",
UserWarning,
)
for mda_atom, rd_atom in zip(atomgroup, rdmol.GetAtoms()):
element = periodic_table.GetElementSymbol(rd_atom.GetAtomicNum())
if mda_atom.element != element:
mda_atom.element = element
mda_atom.name = rd_atom.GetSymbol()


def create_universe_single_state(
top: Path | mda.core.topology.Topology, trj: nc.Dataset, state: int
) -> mda.Universe:
Expand Down
Loading