Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions scanpointgenerator/mutators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
###

from scanpointgenerator.mutators.randomoffsetmutator import RandomOffsetMutator
from scanpointgenerator.mutators.rotationmutator import RotationMutator
74 changes: 74 additions & 0 deletions scanpointgenerator/mutators/rotationmutator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
###
# Copyright (c) 2019 Diamond Light Source Ltd.
#
# All rights reserved. This program and the accompanying materials
# are made available under the terms of the Eclipse Public License v1.0
# which accompanies this distribution, and is available at
# http://www.eclipse.org/legal/epl-v10.html
#
# Contributors:
# Bryan Tester
#
###

from annotypes import Anno, Union, Array, Sequence
from math import cos, sin, pi
from scanpointgenerator.core import Mutator, Point

with Anno("Axes to apply rotation to, "
"in the order the offsets should be applied"):
AAxes = Array[str]
UAxes = Union[AAxes, Sequence[str], str]
with Anno("Centre of rotation"):
ACoR = Array[float]
UCoR = Union[ACoR, list]

Choose a reason for hiding this comment

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

Suggested change
UCoR = Union[ACoR, list]
UCoR = Union[ACoR, Sequence[float]]

with Anno("Angle by which to rotate points (in degrees)"):
ARotationAngle = float


@Mutator.register_subclass("scanpointgenerator:mutator/RotationMutator:1.0")
class RotationMutator(Mutator):
"""Mutator to apply a rotation to the points of an ND
ScanPointGenerator"""

def __init__(self, axes, angle, centreOfRotation):
# type: (UAxes, ARotationAngle, UCoR) -> None
self.angle = ARotationAngle(angle)
self.axes = AAxes(axes)
self.centreOfRotation = ACoR(centreOfRotation)

Choose a reason for hiding this comment

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

Need to check that this is 2D as well

assert len(self.axes) == 2, "Can only rotate in the plane of a pair" +\
"of orthogonal axes"

def mutate(self, point, idx):
rotated = Point()
rotated.indexes = point.indexes
rotated.lower = point.lower.copy()
rotated.upper = point.upper.copy()
rotated.duration = point.duration
pos = point.positions
rotated.positions = pos.copy()
i = self.axes[0]
j = self.axes[1]
i_off = self.centreOfRotation[0]
j_off = self.centreOfRotation[1]
rad = pi*(self.angle/180.0) # convert degrees to radians
rotated.positions[i] = (cos(rad) * (pos[i] - i_off) - sin(rad) * (pos[j] - j_off)) + i_off
rotated.positions[j] = (cos(rad) * (pos[j] - j_off) + sin(rad) * (pos[i] - i_off)) + j_off
if (i in point.lower and i in point.upper) or (j in point.lower and j in point.upper):
i_low = pos[i]
i_up = pos[i]
j_low = pos[j]
j_up = pos[j]
if j in point.lower:

Choose a reason for hiding this comment

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

j should always be in point.lower

j_low = point.lower[j]
if j in point.upper:
j_up = point.upper[j]
if i in point.lower:
i_low = point.lower[i]
if i in point.upper:
i_up = point.upper[i]
rotated.upper[i] = (cos(rad) * (i_up - i_off) - sin(rad) * (j_up - j_off)) + i_off
rotated.upper[j] = (cos(rad) * (j_up - j_off) + sin(rad) * (i_up - i_off)) + j_off
rotated.lower[i] = (cos(rad) * (i_low - i_off) - sin(rad) * (j_low - j_off)) + i_off
rotated.lower[j] = (cos(rad) * (j_low - j_off) + sin(rad) * (i_low - i_off)) + j_off
return rotated
224 changes: 224 additions & 0 deletions tests/test_mutators/test_rotationmutator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
import unittest

from test_util import ScanPointGeneratorTest
from scanpointgenerator.compat import range_
from scanpointgenerator.mutators import RotationMutator
from scanpointgenerator import Point

from pkg_resources import require
require("mock")
from mock import MagicMock

float_error_tolerance = 1e-12

class RandomOffsetMutatorTest(ScanPointGeneratorTest):

Choose a reason for hiding this comment

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

Suggested change
class RandomOffsetMutatorTest(ScanPointGeneratorTest):
class RotationMutatorTest(ScanPointGeneratorTest):


def test_init(self):
m = RotationMutator(["x", "y"], 30, [0., 0.])
self.assertEqual(30, m.angle)

def test_init_fails_for_invalid_axes(self):
self.assertRaises(AssertionError, RotationMutator, ["x"], 30, [0.])
self.assertRaises(AssertionError, RotationMutator, ["x", "y", "z"], 30, [0., 0., 0.])

def test_mutate_simple(self):
def point_gen():
for j in range_(10):
for k in range_(10):
pt = Point()
pt.indexes = [j, k]
pt.positions = {"x": j/10., "y": k/10.}
# TODO: generate upper & lower appropriately
pt.lower = {"x": (j-0.5)/10., "y": (k-0.5)/10.}
pt.upper = {"x": (j+0.5)/10., "y": (k+0.5)/10.}
yield pt
m = RotationMutator(["x", "y"], 30, [0., 0.])
original = [p for p in point_gen()]
mutated = [m.mutate(p, i) for i, p in enumerate(point_gen())]
for o, m in zip(original, mutated):
op_x, mp_x = o.positions["x"], m.positions["x"]
op_y, mp_y = o.positions["y"], m.positions["y"]
# TODO: test upper & lower appropriately...how should they be?
ou_x, mu_x = o.upper["x"], m.upper["x"]
ou_y, mu_y = o.upper["y"], m.upper["y"]
ol_x, ml_x = o.lower["x"], m.lower["x"]
ol_y, ml_y = o.lower["y"], m.lower["y"]
# self.assertNotEqual(op_x, mp_x)
# self.assertNotEqual(op_y, mp_y)
self.assertTrue(abs((op_x**2 + op_y**2) - (mp_x**2 + mp_y**2)) < float_error_tolerance)

for i in range(len(original) - 1):
# check distance between consecutive points is preserved
o_step = (original[i + 1].positions["x"] - original[i].positions["x"]) ** 2 + \
(original[i + 1].positions["y"] - original[i].positions["y"]) ** 2
m_step = (mutated[i + 1].positions["x"] - mutated[i].positions["x"]) ** 2 + \
(mutated[i + 1].positions["y"] - mutated[i].positions["y"]) ** 2
self.assertTrue(abs(o_step - m_step) < float_error_tolerance)

# check angle between points preserved
o_dot = (original[i + 1].positions["x"] * original[i].positions["x"]) + \
(original[i + 1].positions["y"] * original[i].positions["y"])
m_dot = (mutated[i + 1].positions["x"] * mutated[i].positions["x"]) + \
(mutated[i + 1].positions["y"] * mutated[i].positions["y"])
self.assertTrue(abs(o_dot - m_dot) < float_error_tolerance)

def test_mutate_cor(self):
def point_gen():
for j in range_(10):
for k in range_(10):
pt = Point()
pt.indexes = [j, k]
pt.positions = {"x": j/10., "y": k/10.}
pt.lower = {"x": (j-0.5)/10., "y": (k-0.5)/10.}
pt.upper = {"x": (j+0.5)/10., "y": (k+0.5)/10.}
yield pt
CoR = [2., 3.]
m = RotationMutator(["x", "y"], 30, CoR)
original = [p for p in point_gen()]
mutated = [m.mutate(p, i) for i, p in enumerate(point_gen())]
o_r = []
m_r = []
o_rel_x = []
o_rel_y = []
m_rel_x = []
m_rel_y = []
for o, m in zip(original, mutated):
op_x, mp_x = o.positions["x"], m.positions["x"]
op_y, mp_y = o.positions["y"], m.positions["y"]
ou_x, mu_x = o.upper["x"], m.upper["x"]
ou_y, mu_y = o.upper["y"], m.upper["y"]
ol_x, ml_x = o.lower["x"], m.lower["x"]
ol_y, ml_y = o.lower["y"], m.lower["y"]
# self.assertNotEqual(op_x, mp_x)
# self.assertNotEqual(op_y, mp_y)
o_rel_x += [op_x - CoR[0]]
o_rel_y += [op_y - CoR[1]]
m_rel_x += [mp_x - CoR[0]]
m_rel_y += [mp_y - CoR[1]]
o_r += [(op_x - CoR[0]) ** 2 + (op_y - CoR[1]) ** 2]
m_r += [(mp_x - CoR[0]) ** 2 + (mp_y - CoR[1]) ** 2]

self.assertTrue(abs(m_r[-1] - o_r[-1]) < float_error_tolerance)

for i in range(len(original) - 1):
# check distance between consecutive points is preserved
o_step = (original[i + 1].positions["x"] - original[i].positions["x"]) ** 2 + \
(original[i + 1].positions["y"] - original[i].positions["y"]) ** 2
m_step = (mutated[i + 1].positions["x"] - mutated[i].positions["x"]) ** 2 + \
(mutated[i + 1].positions["y"] - mutated[i].positions["y"]) ** 2
self.assertTrue(abs(o_step - m_step) < float_error_tolerance)

# check angle between points preserved
o_dot = (o_rel_x[i + 1] * o_rel_x[i]) + (o_rel_y[i + 1] * o_rel_y[i])
m_dot = (m_rel_x[i + 1] * m_rel_x[i]) + (m_rel_y[i + 1] * m_rel_y[i])
self.assertTrue(abs(o_dot - m_dot) < float_error_tolerance)

def test_mutate_90_degrees(self):
def point_gen():
for j in range_(10):
for k in range_(10):
pt = Point()
pt.indexes = [j, k]
pt.positions = {"x": j/10., "y": k/10.}
pt.lower = {"x": (j-0.5)/10., "y": (k-0.5)/10.}
pt.upper = {"x": (j+0.5)/10., "y": (k+0.5)/10.}
yield pt
m = RotationMutator(["x", "y"], 90, [0., 0.])
original = [p for p in point_gen()]
mutated = [m.mutate(p, i) for i, p in enumerate(point_gen())]
for o, m in zip(original, mutated):
op_x, mp_x = o.positions["x"], m.positions["x"]
op_y, mp_y = o.positions["y"], m.positions["y"]
ou_x, mu_x = o.upper["x"], m.upper["x"]
ou_y, mu_y = o.upper["y"], m.upper["y"]
ol_x, ml_x = o.lower["x"], m.lower["x"]
ol_y, ml_y = o.lower["y"], m.lower["y"]
# self.assertNotEqual(op_x, mp_x)
# self.assertNotEqual(op_y, mp_y)
self.assertTrue(abs((op_x**2 + op_y**2) - (mp_x**2 + mp_y**2)) < float_error_tolerance)
# rotate 90 degrees, mp_y = op_x, mp_x = -op_y
self.assertTrue(abs(op_x - mp_y) < float_error_tolerance)
self.assertTrue(abs(op_y + mp_x) < float_error_tolerance)

# check distance between consecutive points is preserved
for i in range(len(original) - 1):
o_step = (original[i + 1].positions["x"] - original[i].positions["x"]) ** 2 + \
(original[i + 1].positions["y"] - original[i].positions["y"]) ** 2
m_step = (mutated[i + 1].positions["x"] - mutated[i].positions["x"]) ** 2 + \
(mutated[i + 1].positions["y"] - mutated[i].positions["y"]) ** 2
self.assertTrue(abs(o_step - m_step) < float_error_tolerance)

def test_mutate_twice_opposite(self):
def point_gen():
for j in range_(10):
for k in range_(10):
pt = Point()
pt.indexes = [j, k]
pt.positions = {"x": j/10., "y": k/10.}
pt.lower = {"x": (j-0.5)/10., "y": (k-0.5)/10.}
pt.upper = {"x": (j+0.5)/10., "y": (k+0.5)/10.}
yield pt
m1 = RotationMutator(["x", "y"], 30, [0., 0.])
m2 = RotationMutator(["x", "y"], -30, [0., 0.])
original = [p for p in point_gen()]
mutated1 = [m1.mutate(p, i) for i, p in enumerate(point_gen())]
mutated2 = [m2.mutate(p, i) for i, p in enumerate(mutated1)]
for o, m in zip(original, mutated2):
op_x, mp_x = o.positions["x"], m.positions["x"]
op_y, mp_y = o.positions["y"], m.positions["y"]
ou_x, mu_x = o.upper["x"], m.upper["x"]
ou_y, mu_y = o.upper["y"], m.upper["y"]
ol_x, ml_x = o.lower["x"], m.lower["x"]
ol_y, ml_y = o.lower["y"], m.lower["y"]
# should be equal within floating point error
self.assertTrue(abs(mp_x - op_x) < float_error_tolerance)
self.assertTrue(abs(mu_x - ou_x) < float_error_tolerance)
self.assertTrue(abs(ml_x - ol_x) < float_error_tolerance)
self.assertTrue(abs(mp_y - op_y) < float_error_tolerance)
self.assertTrue(abs(mu_y - ou_y) < float_error_tolerance)
self.assertTrue(abs(ml_y - ol_y) < float_error_tolerance)


class TestSerialisation(unittest.TestCase):

def setUp(self):
self.l = MagicMock()
self.l_dict = MagicMock()
self.centreOfRotation = [0., 0.]
self.m = RotationMutator(["x", "y"], 45, self.centreOfRotation)

def test_to_dict(self):
self.l.to_dict.return_value = self.l_dict

expected_dict = dict()
expected_dict['typeid'] = "scanpointgenerator:mutator/RotationMutator:1.0"
expected_dict['angle'] = 45
expected_dict['axes'] = ["x", "y"]
expected_dict['centreOfRotation'] = self.centreOfRotation

d = self.m.to_dict()

self.assertEqual(expected_dict, d)

def test_from_dict(self):

_dict = dict()
_dict['angle'] = 45
_dict['axes'] = ["x", "y"]
_dict['centreOfRotation'] = self.centreOfRotation

units_dict = dict()
units_dict['x'] = 'mm'
units_dict['y'] = 'mm'

m = RotationMutator.from_dict(_dict)

self.assertEqual(45, m.angle)
self.assertEqual(self.centreOfRotation, m.centreOfRotation)


if __name__ == "__main__":
unittest.main(verbosity=2)