Skip to content

Fix position of added OXT atom#347

Merged
peastman merged 2 commits intoopenmm:masterfrom
Aqemia:fix/oxt-geometry
Mar 10, 2026
Merged

Fix position of added OXT atom#347
peastman merged 2 commits intoopenmm:masterfrom
Aqemia:fix/oxt-geometry

Conversation

@aqemia-thomas-holder
Copy link
Contributor

@aqemia-thomas-holder aqemia-thomas-holder commented Mar 6, 2026

Problem description: The computation of the OXT atom position is wrong, it's flipped to the wrong side. Energy minimization often fixes the position, but not always. A good example is 4JSV.

Summary of changes:

  • Fixed inverted sign: +2*v-2*v
  • Added pipeline test

Example before and after:

pdbfixer pdbfixer/tests/data/4JSV.pdb --output=oxtfix-4JSV.pdb --add-atoms=heavy
image

- Fixed inverted sign (`+2*v` -> `-2*v`)
- Extract to testable function
Copy link
Member

@peastman peastman left a comment

Choose a reason for hiding this comment

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

Thanks! I really appreciate the fix. I have a few suggestions for making the code clearer.

direction /= unit.norm(direction)
return direction

def _findOXTPosition(atomPositions: dict[str, mm.Vec3]) -> mm.Vec3:
Copy link
Member

Choose a reason for hiding this comment

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

Why split this out into a separate method? That makes it harder to tell what's changed. From the version history it will look like this is new code, when in fact you just changed a single character.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved to a separate method for:

  • Writing a unit test
  • Have the option to use it in other places (currently not the case)

Copy link
Member

Choose a reason for hiding this comment

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

It will never be used anywhere else. Adding an OXT atom is a very specific calculation that only happens in one place.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will change it to your proposed solution.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed as requested

Comment on lines +6 to +13
ATOM_POSITIONS_ALA = {
"N": Vec3(-0.966, 0.493, 1.500),
"CA": Vec3(0.257, 0.418, 0.692),
"C": Vec3(-0.094, 0.017, -0.716),
"O": Vec3(-1.056, -0.682, -0.923),
"CB": Vec3(1.204, -0.620, 1.296),
"OXT": Vec3(0.586, 0.394, -1.639),
}
Copy link
Member

Choose a reason for hiding this comment

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

Where did these positions come from? How was the reference OXT position calculated?

What about using the following test instead, which makes it clearer what's really being tested?

def test_findOXTPosition():
    """Test that OXT is added at the correct position."""
    fixer = pdbfixer.PDBFixer(filename=(Path(__file__).parent / "data" / "1BHL.pdb").as_posix())

    # Record the original position of OXT, then delete it.

    originalPos = [pos for pos, atom in zip(fixer.positions, fixer.topology.atoms()) if atom.name == 'OXT']
    modeller = app.Modeller(fixer.topology, fixer.positions)
    modeller.delete([atom for atom in modeller.topology.atoms() if atom.name == 'OXT'])
    fixer.topology = modeller.topology
    fixer.positions = modeller.positions

    # Have PDBFixer add it back and make sure it is sufficiently close to the original position.

    fixer.missingResidues = {}
    fixer.findMissingAtoms()
    fixer.addMissingAtoms()
    newPos = [pos for pos, atom in zip(fixer.positions, fixer.topology.atoms()) if atom.name == 'OXT']
    assert unit.norm(newPos[0]-originalPos[0]) < 0.01*unit.nanometer

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Where did these positions come from?

Except OXT, these positions came from https://files.rcsb.org/ligands/download/ALA.cif

How was the reference OXT position calculated?

OXT position calculated with the fixed code

What about using the following test instead

This does energy minimization, correct? I think a test without energy minimization would be more appropriate.

Copy link
Member

Choose a reason for hiding this comment

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

It's better to test the whole pipeline. Otherwise we aren't really testing that it works properly. The test would pass even if the code called the new function incorrectly, or never called it at all!

The important thing is that the test should fail with the current code, but pass with the fixed code. That's what gives you confidence that you're really testing what you think you are.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed as requested

@aqemia-thomas-holder aqemia-thomas-holder marked this pull request as draft March 10, 2026 08:33
- Revert extraction to function
- Remove function unit test
- Add pipeline test
@aqemia-thomas-holder aqemia-thomas-holder marked this pull request as ready for review March 10, 2026 08:50
@peastman peastman merged commit 5e658a4 into openmm:master Mar 10, 2026
3 checks passed
@peastman
Copy link
Member

Looks good, thanks!

@aqemia-thomas-holder aqemia-thomas-holder deleted the fix/oxt-geometry branch March 11, 2026 08:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants