Skip to content

Rotation transformation#1937

Merged
jbarnoud merged 13 commits intoMDAnalysis:developfrom
davidercruz:rotation-transform
Jul 7, 2018
Merged

Rotation transformation#1937
jbarnoud merged 13 commits intoMDAnalysis:developfrom
davidercruz:rotation-transform

Conversation

@davidercruz
Copy link
Contributor

@davidercruz davidercruz commented Jun 13, 2018

Adds a rotation transformation to the transformations module

Changes made in this Pull Request:

  • trajectory coordinates may now be rotated by a given angle and an axis. The axis
    is formed by a given position or the center of mass/(geometry of an AtomGroup
  • added tests for both ways of defining a position

PR Checklist

  • Tests?
  • Docs?
  • CHANGELOG updated?
  • Issue raised/referenced?

@coveralls
Copy link

coveralls commented Jun 13, 2018

Coverage Status

Coverage decreased (-0.004%) to 89.963% when pulling 5ed599f on davidercruz:rotation-transform into 22dfcdb on MDAnalysis:develop.

@jbarnoud jbarnoud self-requested a review June 14, 2018 11:08
@davidercruz
Copy link
Contributor Author

davidercruz commented Jun 15, 2018

I just realised that this transformation may cause issues with molecule wrapping/unwrapping. An easy example would be a membrane system rotated by 90 degrees on an axis which is parallel to the plane of the membrane, in a non cubic box. For the system to "fill" the box would require taking atoms from its periodic images, which would change its composition.
Should I add a warning to the docs of the function?
I was looking at the wrapping functions wrap, pack_into_box and apply_PBC, and I don't think they will fail graciously in this case 🤔

@jbarnoud
Copy link
Contributor

jbarnoud commented Jun 15, 2018 via email

@Tsjerk
Copy link

Tsjerk commented Jun 16, 2018 via email

@davidercruz
Copy link
Contributor Author

@Tsjerk in the context of the API, the cumulative rotation could be stored in the TImestep object. Then every one of those operations would read that property of the timestep and perform the rotation.
Another solution is just adding the rotation to the timestep without actually changing the coordinates. Then at the end of the workflow a hidden transformation would rotate the coordinates if a rotation is present in the timestep.

For now I think it's better to warn the user in the docs. What do you think @jbarnoud ?

@jbarnoud
Copy link
Contributor

jbarnoud commented Jun 16, 2018 via email

from ..lib.transformations import rotation_matrix
from ..core.groups import AtomGroup

def rotate(angle, direction, center="geometry", **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

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

There is no reason to use **kwargs here. It is better to write explicitly all the arguments in the signature. The main issue with **kwargs is that it makes a bunch of argument validation moot: there will not be an error if I make a typo in an argument name for instance, or if I give too many arguments. Also, having a full signature is better on a documentation standpoint.

ts = u.trajectory.ts
angle = math.pi/2
ag = u.atoms()
rotated = MDAnalysis.transformations.rotate(angle, a)(ts)
Copy link
Contributor

Choose a reason for hiding this comment

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

a is not defined, and ag is not used. Typo I guess.

point: list, optional
list of the coordinates of the point from where a custom axis of rotation will
be defined.
ts: Timestep
Copy link
Contributor

Choose a reason for hiding this comment

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

The function does not take ts as an argument.

from ..lib.transformations import rotation_matrix
from ..core.groups import AtomGroup

def rotate(angle, direction, center="geometry", **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

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

It may be better to call the function rotateby to be consistent with AtomGroup.rotateby. Also, it may be easier to use AtomGroup.rotateby in your function rather than reimplementing half of it.

from MDAnalysisTests.datafiles import GRO

def test_rotate_custom_position():
u = mda.Universe(GRO)
Copy link
Member

Choose a reason for hiding this comment

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

so reading the GRO system requires file i/o etc. better would be to use make_Universe() (in core/test_groups etc). I can't remember if this gives nonzero positions, if it doesn't you can make your own fixture that does.

u = mda.Universe(GRO)
vector = np.float32([1,0,0])
pos = np.float32([0,0,0])
matrix = rotation_matrix(math.pi/2, vector, pos)[:3, :3]
Copy link
Member

Choose a reason for hiding this comment

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

np.pi not math.pi

Copy link
Contributor

@jbarnoud jbarnoud left a comment

Choose a reason for hiding this comment

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

A few comments on top of Richard's ones. The one on the tests is the most serious: please try to break your code on purpose and see if your tests fail.

ref = u.trajectory.ts
ref.positions = np.dot(ref.positions, matrix)
transformed = rotate(math.pi/2, vector, position = pos)(ref)
transformed = rotateby(math.pi/2, vector, position = pos)(ref)
Copy link
Contributor

Choose a reason for hiding this comment

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

The conventions in PEP8 say that there should not be spaces around the = here, but there should be spaces around the /.

ref.positions = np.dot(ref.positions, matrix)
selection = u.atoms.select_atoms('resid 1')
transformed = rotate(math.pi/2, vector, ag = selection, center='geometry')(ref)
transformed = rotateby(math.pi/2, vector, ag = selection, center='geometry')(ref)
Copy link
Contributor

Choose a reason for hiding this comment

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

Here you are overwriting your reference. transformed and ref are the same object so the test cannot fail.

from MDAnalysisTests.datafiles import GRO

def test_translate_custom_position():
def test_rotate_custom_position():
Copy link
Contributor

Choose a reason for hiding this comment

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

Should prabably be test_rotateby_....

@davidercruz
Copy link
Contributor Author

As the commit says, the typecasting to numpy.float32 in the rotateby function was causing floating point operation errors, and the tests were failing.
When rotation_matrix is called from within rotateby, the precision of the rotation matrix is reduced in half compared to when it is called outside, e.g.

# matrix from within rotateby 
array([[ 1.000000e+00,  0.000000e+00,  0.000000e+00],
       [ 0.000000e+00, -4.371139e-08, -1.000000e+00],
       [ 0.000000e+00,  1.000000e+00, -4.371139e-08]])

# matrix when calling rotation_matrix outside the transformation
array([[ 1.000000e+00,  0.000000e+00,  0.000000e+00],
       [ 0.000000e+00,  6.123234e-17, -1.000000e+00],
       [ 0.000000e+00,  1.000000e+00,  6.123234e-17]])

And this was causing the tests to fail. I removed all the typecasting in my code, since the rotation_matrix already performs calculations in double precision (np.float64)

from MDAnalysis.lib.transformations import rotation_matrix
from MDAnalysisTests import make_Universe

def rotateby_universes():
Copy link
Member

Choose a reason for hiding this comment

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

@pytest.fixture()
def rotateby_universes():

to make this work. Pytest then inspects arguments to test functions and sees if any names for missing arguments match its known fixtures

from ..lib.transformations import rotation_matrix
from ..core.groups import AtomGroup

def rotateby(angle, direction, center="geometry", pbc=None, ag=None, position=[]):
Copy link
Member

Choose a reason for hiding this comment

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

iirc there's some weird stuff that can happen with position=[] as a kwarg. Like I think you can append to that list and the default drifts over time... use =None

@richardjgowers
Copy link
Member

@davidercruz is there a list somewhere of the transforms you're doing next? ie when is wrap/unwrap happening? :)

@davidercruz
Copy link
Contributor Author

@richardjgowers there is a list in my GSOC project. Today I'll create a PR for a box centering transformation. PBC corrections come next. There is a wrap method in core/groups.py. I'll have to look at it, and see what I can use for the transformation.
Just to confirm, the expected unwrap behaviour is similar to -ur compact in gmx trjconv right?

@richardjgowers
Copy link
Member

@davidercruz there's also lib.mdamath.make_whole but it's shamefully slow (but I could make it faster if we had a cool transformation that used it...). I'm not great at gromacs, but it'd be cool if it unchopped molecules that had crossed a box boundary and got written to the trajectory in a "packed" way

@Tsjerk
Copy link

Tsjerk commented Jun 19, 2018 via email

pbc_arg = pbc
if position and len(position)>2:
position = position
elif isinstance(ag, AtomGroup):
Copy link
Contributor

Choose a reason for hiding this comment

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

This has the same issues I pointed in #1946:

  • it is better not to check for type, but instead try to do the thing (duck typing)
  • the center is not updated after the first frame

from ..core.groups import AtomGroup

def rotateby(angle, direction, center="geometry", pbc=None, ag=None, position=None):
'''
Copy link
Contributor

Choose a reason for hiding this comment

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

You mentioned a warning about periodic boundary conditions to add in the documentation. It should be added.

Parameters
----------
angle: float
rotation angle in radians
Copy link
Contributor

Choose a reason for hiding this comment

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

All other functions of MDAnalysis that work with angle expect degrees from the user. For consistency, this function should too.

----------
angle: float
rotation angle in radians
direction: list
Copy link
Contributor

Choose a reason for hiding this comment

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

Array-like

If True, all the atoms from the given AtomGroup will be moved to the unit cell
before calculating the center
position: list, optional
before calculating the center. Warning: Wrapping/unwrapping the trajectory or
Copy link
Contributor

Choose a reason for hiding this comment

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

Thins is indeed the warning. Though, it has nothing to do with the PBC option. It is a more general problem that should be discussed in the body of the docstring. You can add a "Warnings" section.

The pbc option is about wrapping prior to calculating the center. Setting pcs to False is not going to change the fact that the coordinates are modified in a different way than the reference frame.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right. I don't know what I was thinking, writing that there 😕

@@ -76,31 +77,44 @@ def rotateby(angle, direction, center="geometry", pbc=None, ag=None, position=No
or 'mass'
pbc: bool or None, optional
Copy link
Contributor

Choose a reason for hiding this comment

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

This option has the same issue as the one from the centering transformation.

position: list, optional
before calculating the center. Warning: Wrapping/unwrapping the trajectory or
performing PBC corrections may not be possible after rotating the trajectory.
position: array-like, optional
Copy link
Contributor

Choose a reason for hiding this comment

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

To be consistent with the other transformation, the option should be named point. The name of the user facing variable is more important than the name of the variable we manipulate inside; so the variable in the wrapper should be renamed so the argument is consistent to the user.

raise ValueError('{} is not a valid argument for center'.format(center))
angle = np.deg2rad(angle)
if position:
if len(position)!=3:
Copy link
Contributor

Choose a reason for hiding this comment

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

I mentioned it already; len on an array is a terrible idea. An array with a (1, 3) shape will have length 3, the same goes for an array of shape (3, 3). First convert to an array with np.asarray, then compare the shape with position.shape != (3, ). You may be permissive and accept (1, 3) as a shape as well.

@jbarnoud
Copy link
Contributor

I made a mistake and commented on a previous state. Still have a look at my last batch of comments, they are still all valid.

@jbarnoud
Copy link
Contributor

A couple more things:

  • pay attention to keep you documentation in sync with the code: your examples are still in radian;
  • your code and your tests rely on the same code: if the rotation_matrix function is broken your tests will not break; make sure that there are tests for that function;
  • why are you slicing the rotattion matrix?

@davidercruz
Copy link
Contributor Author

davidercruz commented Jun 25, 2018

@jbarnoud I'm slicing the rotation matrix because the output matrix is 4 by 4 by default.
I don't know why it does so. This is what happens in the code:

def rotation_matrix(angle, direction, point=None):
    # things are done before
    M = np.identity(4) # M is 4 by 4
    M[:3, :3] = R
    # things are done after
    return M # M is still 4 by 4

@orbeckst
Copy link
Member

@davidercruz note on "Why is M 4x4?": They are used for affine transformations, see #1939 (comment) for some notes. (That PR has nothing to do with this one but I happened to think about it a few days ago.)

from ..lib.transformations import rotation_matrix
from ..core.groups import AtomGroup

def rotateby(angle, direction, point=None, center="geometry", pbc=False, ag=None):
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't the pbc argument be wrap to be consistent with the remove center transformation?

@jbarnoud
Copy link
Contributor

jbarnoud commented Jul 6, 2018

The tests are failing. I strongly encourage you to run the tests locally when you do changes: https://github.com/MDAnalysis/mdanalysis/wiki/UnitTests#developers.

@jbarnoud jbarnoud merged commit 1a1b2f1 into MDAnalysis:develop Jul 7, 2018
@jbarnoud jbarnoud added this to the 0.19.0 milestone Jul 7, 2018
@jbarnoud jbarnoud self-assigned this Jul 7, 2018
@davidercruz davidercruz mentioned this pull request Jul 18, 2018
4 tasks
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.

6 participants