diff --git a/scanpointgenerator/mutators/__init__.py b/scanpointgenerator/mutators/__init__.py index c691e4d..beaf9da 100644 --- a/scanpointgenerator/mutators/__init__.py +++ b/scanpointgenerator/mutators/__init__.py @@ -14,3 +14,4 @@ ### from scanpointgenerator.mutators.randomoffsetmutator import RandomOffsetMutator +from scanpointgenerator.mutators.rotationmutator import RotationMutator \ No newline at end of file diff --git a/scanpointgenerator/mutators/rotationmutator.py b/scanpointgenerator/mutators/rotationmutator.py new file mode 100644 index 0000000..039378c --- /dev/null +++ b/scanpointgenerator/mutators/rotationmutator.py @@ -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) + 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: + 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 diff --git a/tests/test_mutators/test_rotationmutator.py b/tests/test_mutators/test_rotationmutator.py new file mode 100644 index 0000000..853ee6a --- /dev/null +++ b/tests/test_mutators/test_rotationmutator.py @@ -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)