Skip to content
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
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
78 changes: 78 additions & 0 deletions scanpointgenerator/mutators/rotationmutator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
###
# 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, 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

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

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_up = pos[j]
j_low = 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
227 changes: 227 additions & 0 deletions tests/test_mutators/test_rotationmutator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
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

def make_point(j,k):
pt = Point()
pt.indexes = [j, k]
if k == 9:
pt.positions = {"x": j / 10., "y": k / 10.}
pt.lower = {"x": j / 10., "y": (k - 0.5) / 10.}
pt.upper = {"x": (j + 0.5) / 10., "y": 0.45}
elif k == 0:
pt.positions = {"x": j / 10., "y": k / 10.}
pt.lower = {"x": (j - 0.5) / 10., "y": 0.45}
pt.upper = {"x": j / 10., "y": (k + 0.5) / 10.}
else:
pt.positions = {"x": j / 10., "y": k / 10.}
pt.lower = {"x": j / 10., "y": (k - 0.5) / 10.}
pt.upper = {"x": j / 10., "y": (k + 0.5) / 10.}
return pt

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 = make_point(j, k)
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"]
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.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)

# check bounds still correct
self.assertEqual(original[i + 1].lower["x"], original[i].upper["x"])
self.assertEqual(original[i + 1].lower["y"], original[i].upper["y"])
self.assertEqual(mutated[i + 1].lower["x"], mutated[i].upper["x"])
self.assertEqual(mutated[i + 1].lower["y"], mutated[i].upper["y"])

def test_mutate_cor(self):
def point_gen():
for j in range_(10):
for k in range_(10):
pt = make_point(j, k)
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 = make_point(j, k)
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 = make_point(j, k)
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)