Skip to content

RMSD and Rotational Constants Modules #230

Open
crjacinto wants to merge 19 commits into
faccts:mainfrom
crjacinto:feature-rmsd
Open

RMSD and Rotational Constants Modules #230
crjacinto wants to merge 19 commits into
faccts:mainfrom
crjacinto:feature-rmsd

Conversation

@crjacinto

@crjacinto crjacinto commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Closes Issues

Closes #220 #219

Description

  • Class methods to calculate the RMSD between two structures and the rotational spectrum (rotor type, moments of inertia and rotational constants) of a molecule.

Release Notes

(Project adheres to Keep a Changelog; Every entry should start in upper-case and end with (#<pr-id>); Remove sections that don't apply)

Added

  • Added constants.py containing some universal constants
  • Added rotconst.py containing auxiliary dictionaries for Rotational Constants Calculation

Changed

  • Modified structure.py with RMSD and RotConst as Class methods
  • Modified elements.py adding atomic masses for moment of inertia calculations

Removed

  • Removed redundant rmsd.py module

@crjacinto crjacinto requested a review from a team as a code owner April 17, 2026 12:13
@crjacinto crjacinto changed the title RMSD and Rotationl Constants Modules WIP: RMSD and Rotationl Constants Modules Apr 17, 2026
@crjacinto crjacinto changed the title WIP: RMSD and Rotationl Constants Modules WIP: RMSD and Rotational Constants Modules Apr 17, 2026
@crjacinto crjacinto changed the title WIP: RMSD and Rotational Constants Modules RMSD and Rotational Constants Modules Apr 17, 2026
@timmyte timmyte self-requested a review April 20, 2026 13:02

@timmyte timmyte left a comment

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.

Solid first PR.
But the tools are bit to decoupled from OPI. You should use and add to the data structures that we already have in OPI.

Comment thread src/opi/utils/rmsd.py Outdated
Comment thread src/opi/utils/rmsd.py Outdated
Comment thread src/opi/utils/rmsd.py Outdated
Comment thread src/opi/utils/rmsd.py Outdated
Comment thread src/opi/utils/rmsd.py Outdated
Comment thread src/opi/utils/rotconst.py Outdated
Comment thread src/opi/utils/rmsd.py Outdated
Comment thread src/opi/utils/rotconst.py Outdated
Comment thread src/opi/utils/rotconst.py Outdated
Comment thread src/opi/utils/rotconst.py Outdated
@timmyte

timmyte commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

As discussed, I would design the RMSD function as follows:

class Structure:
    def rmsd(self, other: Structure, /, only_atoms: Sequence[int] = (), *, ignore_hs: bool = False) -> float:
        return _rmsd(self.coordinates, other.coordinates, only_atoms=only_atoms, ignore_hs=ignore_hs)

    def _rmsd(coords1: NDArray[float], coords2: NDArray[float], /, only_atoms: Sequence[int] = (), *, ignore_hs: bool = False)
        # We ignore `ignore_hs` if `only_atoms` is populated. This should also be documented
        if coords1.shape != coords2.shape:
            raise ValueError

    def rmsd_kabsch(self, other: Structure, /, only_atoms: Sequence[int] = (), *, ignore_hs: bool = False) -> float:
        # 1) Translate to center of geometry (centroid)
        # 2) find optimal rotation

        # `only_atoms` and `ignore_hs` don't need to be passed here anymore as `coords1` and `coords2_rotated` should only contain the coordinates of the relevant atoms.
        return _rmsd(coords1, coords2_rotated)

@timmyte timmyte assigned timmyte and crjacinto and unassigned timmyte Apr 28, 2026
@crjacinto crjacinto requested a review from timmyte May 6, 2026 14:12

@timmyte timmyte left a comment

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.

Very nice improvement of the code. We are getting there.
I really appreciate how well you documented the code.

Comment thread src/opi/input/structures/structure.py Outdated
Comment thread src/opi/input/structures/structure.py Outdated
Comment thread src/opi/input/structures/structure.py Outdated
Comment thread src/opi/input/structures/structure.py Outdated
Comment thread src/opi/input/structures/structure.py Outdated
Comment thread src/opi/utils/rotconst.py Outdated
Comment thread src/opi/input/structures/structure.py Outdated
Comment thread src/opi/input/structures/structure.py
Only ``Atom`` instances contribute; ``GhostAtom``, ``PointCharge``,
and ``EmbeddingPotential`` atoms are silently ignored.

Mass priority

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 block should come after Parameters. This applies to every docstring that has this.

First explain your terminology, then you can explain the rules.

@crjacinto crjacinto May 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Change made, thanks for the explanation

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.

Did you forget to push the change?

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.

Please move the block after Parameters.

Comment thread src/opi/input/structures/structure.py Outdated
@crjacinto crjacinto requested a review from timmyte May 19, 2026 18:24

@timmyte timmyte left a comment

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.

Thanks for tackling my remarks. There are still some unresolved issues though.

Please also see the few additional points I raised.

Comment thread src/opi/input/structures/structure.py Outdated
Comment thread src/opi/input/structures/structure.py Outdated
Comment thread src/opi/input/structures/structure.py Outdated
Comment thread src/opi/input/structures/structure.py
Coordinates in the same order as the input list.
"""
atom_list = atoms if atoms is not None else self.atoms
return np.array([a.coordinates.coordinates for a in atom_list], dtype=np.float64)

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.

We should refrain from setting dtype explicitly everywhere, as this introduces an extra chore, which might be overlooked by developers.

Or is there a particular reason, why we need to set this explicitly?

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.

dtype is still set explicitly. Please remove this, if there is no particular reason to do so. Otherwise, add comment mentioning the reason.

Comment thread src/opi/input/structures/structure.py
Only ``Atom`` instances contribute; ``GhostAtom``, ``PointCharge``,
and ``EmbeddingPotential`` atoms are silently ignored.

Mass priority

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.

Did you forget to push the change?

Comment thread tests/unit/test_rmsd.py Outdated
@crjacinto crjacinto requested a review from timmyte May 22, 2026 14:17

@timmyte timmyte left a comment

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.

Looks good. Please consider my last remark before merging.
I also would like to get @haneug opinion on the API with all the different weight parameters

Comment thread src/opi/input/structures/structure.py
Comment thread src/opi/input/structures/structure.py Outdated
Comment thread src/opi/input/structures/structure.py Outdated
Comment thread src/opi/utils/rotconst.py Outdated
@timmyte timmyte requested a review from haneug June 2, 2026 14:08

@timmyte timmyte left a comment

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.

Very good, especially the exhaustive amount of tests. BRAVO!

Comment thread src/opi/input/structures/structure.py
Comment thread src/opi/input/structures/structure.py
Comment thread src/opi/input/structures/structure.py Outdated
Comment thread src/opi/input/structures/structure.py Outdated
Comment thread src/opi/input/structures/structure.py Outdated
Comment thread tests/unit/test_rmsd.py
"""All atoms in water are real."""
assert len(water.real_atoms) == 3

def test_return_type(self, water):

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.

Please add docstring, briefly describing the purpose of the test.

Comment thread tests/unit/test_rmsd.py
Comment thread tests/unit/test_rmsd.py
class TestGetCoordinatesAtCentroid:
def test_centroid_is_zero(self, water):
coords = water.get_coordinates_at_centroid()
np.testing.assert_allclose(coords.mean(axis=0), [0.0, 0.0, 0.0], atol=1e-12)

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.

Please document why so picked this value for atol?
Please do this for all tests below.
It's not obvious why its 1e-12 here and 1e-6 below.

Comment thread tests/unit/test_rmsd.py
Comment thread tests/unit/test_rotconst.py Outdated

@timmyte timmyte left a comment

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.

Thanks for quickly tackling my remarks. But please make sure you addressed all of them.

Coordinates in the same order as the input list.
"""
atom_list = atoms if atoms is not None else self.atoms
return np.array([a.coordinates.coordinates for a in atom_list], dtype=np.float64)

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.

dtype is still set explicitly. Please remove this, if there is no particular reason to do so. Otherwise, add comment mentioning the reason.

Only ``Atom`` instances contribute; ``GhostAtom``, ``PointCharge``,
and ``EmbeddingPotential`` atoms are silently ignored.

Mass priority

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.

Please move the block after Parameters.

@@ -1475,13 +1494,14 @@ def calc_rotational_constants(

Mass priority

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.

Same here: Please make that the Parameters block comes first and then the Mass priority

Comment thread src/opi/utils/rotconst.py
effectively zero (degenerate / linear axis).
"""
if inertia is None or inertia < 1e-6:
if inertia is None or inertia < 1e-3:

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.

Why did this number change now!?
Please document why exactly 10e-3.

Comment thread tests/unit/test_rmsd.py
def test_rotated_zero_rmsd(self, water, water_rotated):
"""Kabsch should align the rotation and give ~0 RMSD."""
centered = water.centered_structure()
print(centered.get_coordinates())

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.

Why the print statement here?

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.

RMSD between Structure Objects

3 participants