Skip to content

replaced lib.transformations with transformations package #5062

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

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .github/actions/setup-deps/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ inputs:
default: 'pytest'
scipy:
default: 'scipy'
transformations:
default: 'transformations'
Comment on lines +48 to +49
Copy link
Member Author

Choose a reason for hiding this comment

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

I thought adding transformations here would install it as part of CI but I still see it as pypi installed https://github.com/MDAnalysis/mdanalysis/actions/runs/15578157334/job/43867072733?pr=5062#step:7:269

  transformations                   2025.1.1      pypi_0                   pypi    

What should I be doing?

Copy link
Member

Choose a reason for hiding this comment

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

I got time this weekend, will look into it!

Copy link
Member

Choose a reason for hiding this comment

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

Just to expand, I suspect that there is something deeper with the way in which conda recognises the package. We should check this before making the switch, otherwise we might end up with and rdkit pypi situation, except from a non-optional package.

Copy link
Member

Choose a reason for hiding this comment

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

One thing I noticed and maybe is relevant, is that the conda-forge package does not appear to be maintained: the latest packaged version is 2022.9.26.

Copy link
Member Author

Choose a reason for hiding this comment

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

sigh...

Do we want to put another orphan package on our shoulders (until we remove transformations in 3.0)?

Or do we want to go back and just vendor the 2025.x code?

Copy link
Member Author

Choose a reason for hiding this comment

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

It might be as easy as merging a bunch of PRs https://github.com/conda-forge/transformations-feedstock/pulls ... or not ;-)

Copy link
Member

Choose a reason for hiding this comment

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

I wouldn't say it is an orphan packaged, because PyPI is up to date and there has been a 2025 release. So I think the main issue is with the conda-forge feedstock, which we could take over?

Copy link
Member

Choose a reason for hiding this comment

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

I agree with @RMeli - the recipe seems rather simple so it might not be too much of a hassle for us to take it on.

Copy link
Member Author

Choose a reason for hiding this comment

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

FYI: when I tried mamba install transformations in my developer env, it tried downgrading numpy. So I am just confirming what you've been saying: the conda-forge version of transformations needs to be updated for us to be useful.


  Package              Version  Build            Channel           Size
─────────────────────────────────────────────────────────────────────────
  Install:
─────────────────────────────────────────────────────────────────────────

  + transformations  2022.9.26  py312hc7c0aa3_2  conda-forge       80kB

  Downgrade:
─────────────────────────────────────────────────────────────────────────

  - numpy                2.2.6  py312h72c5963_0  conda-forge     Cached
  + numpy               1.26.4  py312heda63a1_0  conda-forge     Cached

threadpoolctl:
default: 'threadpoolctl'
tqdm:
Expand Down
1 change: 1 addition & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ jobs:
pytest-timeout
pytest-xdist
scikit-learn
transformations
tqdm
threadpoolctl
filelock
Expand Down
1 change: 1 addition & 0 deletions maintainer/conda/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dependencies:
- scipy
- pip
- sphinx <7.0
- transformations
- tidynamics>=1.0.0
- tqdm>=4.43.0
- sphinxcontrib-bibtex
Expand Down
4 changes: 4 additions & 0 deletions package/CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,15 @@ Enhancements
MDAnalysisTest.util (PR #5038)

Changes
* Replaced old vendored lib.transformations with import of the
transformations package (#5061)
* Removed undocumented and unused attribute
`analysis.lineardensity.LinearDensity.totalmass` (PR #5007)
* Remove `default` channel from RTD conda env. (Issue # 5036, PR # 5037)

Deprecations
* lib.transformations.rotaxis() moved to lib.mdamath.rotaxis()
* lib.transformations to be removed in 3.0.0


03/11/25 IAlibay, ChiahsinChu, RMeli, tanishy7777, talagayev, tylerjereddy,
Expand Down
4 changes: 2 additions & 2 deletions package/MDAnalysis/analysis/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ class MyAnalysis(AnalysisBase):
def get_supported_backends(cls):
return ('serial', 'multiprocessing', 'dask',)


def _get_aggregator(self):
return ResultsGroup(lookup={'timeseries': ResultsGroup.ndarray_vstack})

Expand Down Expand Up @@ -352,7 +352,7 @@ def _define_run_frames(
Returns
-------
Union[slice, np.ndarray]
Appropriate slicer for the trajectory that would give correct iteraction
Appropriate slicer for the trajectory that would give correct iteration
order via trajectory[slicer]

Raises
Expand Down
11 changes: 6 additions & 5 deletions package/MDAnalysis/core/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@
import contextlib
import warnings

import transformations

from .. import (
_CONVERTERS,
_TOPOLOGY_ATTRS,
Expand All @@ -114,7 +116,6 @@
int_array_is_sorted,
)
from ..lib import distances
from ..lib import transformations
from ..lib import mdamath
from .accessors import Accessor, ConverterWrapper
from ..selections import get_writer as get_selection_writer_for
Expand Down Expand Up @@ -1518,7 +1519,7 @@ def transform(self, M):

See Also
--------
MDAnalysis.lib.transformations : module of all coordinate transforms
:mod:`transformations` : module of all coordinate transforms

Notes
-----
Expand Down Expand Up @@ -1551,7 +1552,7 @@ def translate(self, t):

See Also
--------
MDAnalysis.lib.transformations : module of all coordinate transforms
:mod:`transformations` : module of all coordinate transforms

Notes
-----
Expand Down Expand Up @@ -1601,7 +1602,7 @@ def rotate(self, R, point=(0, 0, 0)):
See Also
--------
rotateby : rotate around given axis and angle
MDAnalysis.lib.transformations : module of all coordinate transforms
:mod:`transformations` : module of all coordinate transforms

"""
R = np.asarray(R)
Expand Down Expand Up @@ -1651,7 +1652,7 @@ def rotateby(self, angle, axis, point=None):

See Also
--------
MDAnalysis.lib.transformations.rotation_matrix :
:mod:`transformations.rotation_matrix` :
calculate :math:`\mathsf{R}`

"""
Expand Down
40 changes: 20 additions & 20 deletions package/MDAnalysis/core/topologyattrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
unique_int_1d,
check_atomgroup_not_empty,
)
from ..lib import transformations, mdamath
from ..lib import mdamath
from ..exceptions import NoDataError, SelectionError
from .topologyobjects import TopologyGroup
from . import selection
Expand Down Expand Up @@ -2120,7 +2120,7 @@ def align_principal_axis(group, axis, vector):
"""
p = group.principal_axes()[axis]
angle = np.degrees(mdamath.angle(p, vector))
ax = transformations.rotaxis(p, vector)
ax = mdamath.rotaxis(p, vector)
# print "principal[%d] = %r" % (axis, p)
# print "axis = %r, angle = %f deg" % (ax, angle)
return group.rotateby(angle, ax)
Expand Down Expand Up @@ -2290,7 +2290,7 @@ def dipole_vector(
r"""Dipole vector of the group.

.. math::
\boldsymbol{\mu} = \sum_{i=1}^{N} q_{i} ( \mathbf{r}_{i} -
\boldsymbol{\mu} = \sum_{i=1}^{N} q_{i} ( \mathbf{r}_{i} -
\mathbf{r}_{COM} )

Computes the dipole vector of :class:`Atoms<Atom>` in the group.
Expand All @@ -2303,10 +2303,10 @@ def dipole_vector(
a charged group the dipole moment can be later adjusted with:

.. math::
\boldsymbol{\mu}_{COC} = \boldsymbol{\mu}_{COM} +
\boldsymbol{\mu}_{COC} = \boldsymbol{\mu}_{COM} +
q_{ag}\mathbf{r}_{COM} - q_{ag}\boldsymbol{r}_{COC}

Where :math:`\mathbf{r}_{COM}` is the center of mass and
Where :math:`\mathbf{r}_{COM}` is the center of mass and
:math:`\mathbf{r}_{COC}` is the center of charge.

Parameters
Expand All @@ -2330,7 +2330,7 @@ def dipole_vector(
:class:`Atoms<Atom>` *belonging to the group* will be taken into
account.
center : {'mass', 'charge'}, optional
Choose whether the dipole vector is calculated at the center of
Choose whether the dipole vector is calculated at the center of
"mass" or the center of "charge", default is "mass".

Returns
Expand Down Expand Up @@ -2421,8 +2421,8 @@ def dipole_moment(group, **kwargs):
fragment can be obtained by setting the `compound` parameter
accordingly.

Note that when there is a net charge, the magnitude of the dipole
moment is dependent on the `center` chosen.
Note that when there is a net charge, the magnitude of the dipole
moment is dependent on the `center` chosen.
See :meth:`~dipole_vector`.

Parameters
Expand All @@ -2446,7 +2446,7 @@ def dipole_moment(group, **kwargs):
:class:`Atoms<Atom>` *belonging to the group* will be taken into
account.
center : {'mass', 'charge'}, optional
Choose whether the dipole vector is calculated at the center of
Choose whether the dipole vector is calculated at the center of
"mass" or the center of "charge", default is "mass".

Returns
Expand Down Expand Up @@ -2492,23 +2492,23 @@ def quadrupole_tensor(
tensor of the group:

.. math::
\mathsf{Q} = \sum_{i=1}^{N} q_{i} ( \mathbf{r}_{i} -
\mathsf{Q} = \sum_{i=1}^{N} q_{i} ( \mathbf{r}_{i} -
\mathbf{r}_{COM} ) \otimes ( \mathbf{r}_{i} - \mathbf{r}_{COM} )

The traceless quadrupole tensor, :math:`\hat{\mathsf{Q}}`, is then
taken from:

.. math::
\hat{\mathsf{Q}} = \frac{3}{2} \mathsf{Q} - \frac{1}{2}
\hat{\mathsf{Q}} = \frac{3}{2} \mathsf{Q} - \frac{1}{2}
tr(\mathsf{Q})

Computes the quadrupole tensor of :class:`Atoms<Atom>` in the group.
Tensor per :class:`Residue`, :class:`Segment`, molecule, or
fragment can be obtained by setting the `compound` parameter
accordingly.

Note that when there is an unsymmetrical plane in the molecule or
group, the magnitude of the quadrupole tensor is dependent on the
Note that when there is an unsymmetrical plane in the molecule or
group, the magnitude of the quadrupole tensor is dependent on the
``center`` (e.g., :math:`\mathbf{r}_{COM}`) chosen and cannot be translated.

Parameters
Expand All @@ -2532,15 +2532,15 @@ def quadrupole_tensor(
:class:`Atoms<Atom>` *belonging to the group* will be taken into
account.
center : {'mass', 'charge'}, optional
Choose whether the dipole vector is calculated at the center of
Choose whether the dipole vector is calculated at the center of
"mass" or the center of "charge", default is "mass".

Returns
-------
numpy.ndarray
Quadrupole tensor(s) of (compounds of) the group in :math:`eÅ^2`.
If `compound` was set to ``'group'``, the output will be a single
tensor of shape ``(3,3)``. Otherwise, the output will be a 1d array
tensor of shape ``(3,3)``. Otherwise, the output will be a 1d array
of shape ``(n,3,3)`` where ``n`` is the number of compounds.


Expand Down Expand Up @@ -2623,20 +2623,20 @@ def __quadrupole_tensor(recenteredpos, charges):

def quadrupole_moment(group, **kwargs):
r"""Quadrupole moment of the group according to :footcite:p:`Gray1984`.

.. math::
Q = \sqrt{\frac{2}{3}{\hat{\mathsf{Q}}}:{\hat{\mathsf{Q}}}}

where the quadrupole moment is calculated from the tensor double
where the quadrupole moment is calculated from the tensor double
contraction of the traceless quadropole tensor :math:`\hat{\mathsf{Q}}`

Computes the quadrupole moment of :class:`Atoms<Atom>` in the group.
Quadrupole per :class:`Residue`, :class:`Segment`, molecule, or
fragment can be obtained by setting the `compound` parameter
accordingly.

Note that when there is an unsymmetrical plane in the molecule or
group, the magnitude of the quadrupole moment is dependant on the
Note that when there is an unsymmetrical plane in the molecule or
group, the magnitude of the quadrupole moment is dependant on the
``center`` chosen and cannot be translated.

Parameters
Expand All @@ -2660,7 +2660,7 @@ def quadrupole_moment(group, **kwargs):
:class:`Atoms<Atom>` *belonging to the group* will be taken into
account.
center : {'mass', 'charge'}, optional
Choose whether the dipole vector is calculated at the center of
Choose whether the dipole vector is calculated at the center of
"mass" or the center of "charge", default is "mass".

Returns
Expand Down
3 changes: 2 additions & 1 deletion package/MDAnalysis/lib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@
"nsgrid",
]


from . import log
from . import transformations
from . import util
from . import transformations
from . import mdamath
from . import distances # distances relies on mdamath
from . import NeighborSearch
Expand Down
28 changes: 28 additions & 0 deletions package/MDAnalysis/lib/mdamath.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
.. autofunction:: triclinic_box
.. autofunction:: triclinic_vectors
.. autofunction:: box_volume
.. autofunction:: rotaxis


Connectivity
Expand Down Expand Up @@ -455,3 +456,30 @@ def box_volume(dimensions: npt.ArrayLike) -> float:
tri_vecs = triclinic_vectors(dim, dtype=np.float64)
volume = tri_vecs[0, 0] * tri_vecs[1, 1] * tri_vecs[2, 2]
return volume


def rotaxis(a, b):
"""Return the rotation axis to rotate vector a into b.

Parameters
----------
a, b : array_like
two vectors

Returns
-------
c : np.ndarray
vector to rotate a into b


Note
----
If a == b this will always return [1, 0, 0]

.. versionchanged:: 2.10.0
Moved from (removed) lib.transformations to lib.mdamath
"""
if np.allclose(a, b):
return np.array([1, 0, 0])
c = np.cross(a, b)
return c / np.linalg.norm(c)
2 changes: 0 additions & 2 deletions package/MDAnalysis/lib/src/transformations/AUTHOR

This file was deleted.

21 changes: 0 additions & 21 deletions package/MDAnalysis/lib/src/transformations/LICENCE

This file was deleted.

Empty file.
Loading
Loading