Skip to content

RMSD and Rotational Constants Modules #230

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

RMSD and Rotational Constants Modules #230
crjacinto wants to merge 17 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
Comment thread src/opi/input/structures/structure.py
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
Comment thread src/opi/input/structures/structure.py Outdated
Comment thread src/opi/input/structures/structure.py
Comment thread src/opi/input/structures/structure.py
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!

"""
Compute RMSD between this structure and *other* (no rotational alignment).

Both structures are translated to their centroid before comparison.

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.

I would be a bit more precise here:
The two structure objects themselves are not modified. Please mention this.

If the objects themselves were changed, there could be side-effects in any code coming after calling this method. This is important to avoid!

"""
Compute RMSD between this structure and *other* using the Kabsch algorithm.

Translates both structures to their centroid, then finds the optimal

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. Mention that the Structure object themselves are not modified.

# ------------------------------------------------------------------ #
def _resolve_masses(
self,
elem_masses: dict[str, float] | None = None,

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.

Suggested change
elem_masses: dict[str, float] | None = None,
elem_masses: dict[str | Element, float] | None = None,

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.

Also update the docstring below, please.


def calc_moments_of_inertia(
self,
elem_masses: dict[str, float] | None = None,

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.

Suggested change
elem_masses: dict[str, float] | None = None,
elem_masses: dict[str | Element, float] | None = None,

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.

Also update the docstring below, please.

`None` the moments are computed via
:meth:`calc_moments_of_inertia` using *mass_kwargs*.
**mass_kwargs
Forwarded to :meth:`calc_moments_of_inertia` when *moments* is

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.

Suggested change
Forwarded to :meth:`calc_moments_of_inertia` when *moments* is
Forwarded to :meth:`calc_moments_of_inertia()` when `moments` is

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

class TestGetCoordinates:
def test_shape(self, water):
coords = water.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.

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

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

class TestSetCoordinates:
def test_mutates_in_place(self, water):
"""set_coordinates should modify the structure in place."""

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.

Suggested change
"""set_coordinates should modify the structure in place."""
"""set_coordinates should modify the structure in-place."""

pm = water.calc_moments_of_inertia()
assert pm is not None
s = str(pm)
assert "Moments of inertia (amu·Å²):" in s

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.

You should know the extract string that comes out.
Why not compare directly against that?

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