From 0db41f7f0adf3e2b6051ab237a84d95e55b0adba Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Thu, 20 Oct 2016 11:31:58 +0100 Subject: [PATCH 01/47] Add means of pre-calculating value arrays in generators --- .../generators/linegenerator.py | 28 ++++++++++++- .../generators/lissajousgenerator.py | 26 ++++++++++++ .../generators/spiralgenerator.py | 35 +++++++++++++++- tests/test_generators/test_linegenerator.py | 10 +++++ .../test_lissajousgenerator.py | 42 +++++++++++++++++++ tests/test_generators/test_spiralgenerator.py | 30 +++++++++++++ 6 files changed, 168 insertions(+), 3 deletions(-) diff --git a/scanpointgenerator/generators/linegenerator.py b/scanpointgenerator/generators/linegenerator.py index 0cd29b5..dfeade2 100644 --- a/scanpointgenerator/generators/linegenerator.py +++ b/scanpointgenerator/generators/linegenerator.py @@ -1,3 +1,5 @@ +import numpy as np + from scanpointgenerator.compat import range_ from scanpointgenerator.core import Generator from scanpointgenerator.core import Point @@ -32,6 +34,9 @@ def __init__(self, name, units, start, stop, num, alternate_direction=False): self.start = to_list(start) self.stop = to_list(stop) self.alternate_direction = alternate_direction + self.points = None + self.points_lower = None + self.points_upper = None if len(self.name) != len(set(self.name)): raise ValueError("Axis names cannot be duplicated; given %s" % @@ -68,8 +73,29 @@ def __init__(self, name, units, start, stop, num, alternate_direction=False): self.axes = self.name # For GDA - def iterator(self): + def produce_points(self): + self.points = {} + self.points_lower = {} + self.points_upper = {} + for axis in range_(self.num_axes): + axis_name = self.name[axis] + start = self.start[axis] + stop = self.stop[axis] + d = stop - start + n = self.num - 1. + s = d / n + upper_start = start + 0.5 * d / n + upper_stop = stop + 0.5 * d / n + lower_start = start - 0.5 * d / n + lower_stop = stop - 0.5 * d / n + self.points[axis_name] = np.linspace( + start, stop, self.num, dtype=np.float64) + self.points_upper[axis_name] = np.linspace( + upper_start, upper_stop, self.num, dtype=np.float64) + self.points_lower[axis_name] = np.linspace( + lower_start, lower_stop, self.num, dtype=np.float64) + def iterator(self): for i in range_(self.num): point = Point() diff --git a/scanpointgenerator/generators/lissajousgenerator.py b/scanpointgenerator/generators/lissajousgenerator.py index a4a7717..d757962 100644 --- a/scanpointgenerator/generators/lissajousgenerator.py +++ b/scanpointgenerator/generators/lissajousgenerator.py @@ -1,4 +1,5 @@ import math as m +import numpy as np from scanpointgenerator.compat import range_ from scanpointgenerator.core import Generator @@ -24,6 +25,9 @@ def __init__(self, names, units, box, num_lobes, num_points=None): self.names = names self.units = units + self.points = None + self.points_lower = None + self.points_upper = None if len(self.names) != len(set(self.names)): raise ValueError("Axis names cannot be duplicated; given %s" % @@ -54,6 +58,28 @@ def __init__(self, names, units, box, num_lobes, num_points=None): self.axes = self.names # For GDA + def _calc_arrays(self, offset): + x0, y0 = self.centre[0], self.centre[1] + A, B = self.x_max, self.y_max + a, b = self.x_freq, self.y_freq + d = self.phase_diff + f = lambda t: y0 + A * np.sin(a * 2 * m.pi * (t+offset)/self.num + d) + x = np.fromfunction(f, (self.num,), dtype=np.float64) + f = lambda t: B * np.sin(b * 2 * m.pi * (t+offset)/self.num) + y = np.fromfunction(f, (self.num,), dtype=np.float64) + return x, y + + def produce_points(self): + self.points = {} + self.points_lower = {} + self.points_upper = {} + + x = self.names[0] + y = self.names[1] + self.points[x], self.points[y] = self._calc_arrays(0) + self.points_upper[x], self.points_upper[y] = self._calc_arrays(0.5) + self.points_lower[x], self.points_lower[y] = self._calc_arrays(-0.5) + def _calc(self, i): """Calculate the coordinate for a given index""" x = self.centre[0] + \ diff --git a/scanpointgenerator/generators/spiralgenerator.py b/scanpointgenerator/generators/spiralgenerator.py index cd1de5b..b7e7523 100644 --- a/scanpointgenerator/generators/spiralgenerator.py +++ b/scanpointgenerator/generators/spiralgenerator.py @@ -1,4 +1,5 @@ import math as m +import numpy as np from scanpointgenerator.compat import range_ from scanpointgenerator.core import Generator @@ -16,8 +17,8 @@ def __init__(self, names, units, centre, radius, scale=1.0, names (list(str)): The scannable names e.g. ["x", "y"] units (str): The scannable units e.g. "mm" centre(list): List of two coordinates of centre point of spiral - radius(float): Radius of spiral - scale(float): Rate at which spiral expands; higher scale gives + radius(float): Maximum radius of spiral + scale(float): Gap between spiral arcs; higher scale gives fewer points for same radius alternate_direction(bool): Specifier to reverse direction if generator is nested @@ -29,6 +30,9 @@ def __init__(self, names, units, centre, radius, scale=1.0, self.radius = radius self.scale = scale self.alternate_direction = alternate_direction + self.points = None + self.points_lower = None + self.points_upper = None if len(self.names) != len(set(self.names)): raise ValueError("Axis names cannot be duplicated; given %s" % @@ -47,6 +51,33 @@ def __init__(self, names, units, centre, radius, scale=1.0, self.axes = self.names # For GDA + def _calc_arrays(self, offset): + # spiral equation : r = b * phi + # scale = 2 * pi * b + # parameterise phi with approximation: + # phi(t) = k * sqrt(t) (for some k) + # number of possible t is solved by sqrt(t) = max_r / b*k + b = self.scale / (2 * m.pi) + k = m.sqrt(4 * m.pi) # magic scaling factor for our angle steps + size = (self.radius) / (b * k) + size *= size + size = int(size) + 1 # TODO: Why the +1 ??? + phi_t = lambda t: k * np.sqrt(t + offset) + phi = np.fromfunction(phi_t, (size,), dtype=np.float64) + x = self.centre[0] + b * phi * np.sin(phi) + y = self.centre[1] + b * phi * np.cos(phi) + return x, y + + def produce_points(self): + self.points = {} + self.points_lower = {} + self.points_upper = {} + x = self.names[0] + y = self.names[1] + self.points_lower[x], self.points_lower[y] = self._calc_arrays(0) + self.points[x], self.points[y] = self._calc_arrays(0.5) + self.points_upper[x], self.points_upper[y] = self._calc_arrays(1.) + def _calc(self, i): """Calculate the coordinate for a given index""" theta = self.alpha * m.sqrt(i) diff --git a/tests/test_generators/test_linegenerator.py b/tests/test_generators/test_linegenerator.py index a92f8a1..d4efc1e 100644 --- a/tests/test_generators/test_linegenerator.py +++ b/tests/test_generators/test_linegenerator.py @@ -18,6 +18,16 @@ def test_init(self): self.assertEqual(self.g.index_names, ["x"]) self.assertEqual(self.g.axes, ["x"]) + def test_array_positions(self): + positions = [1.0, 3.0, 5.0, 7.0, 9.0] + lower = [0.0, 2.0, 4.0, 6.0, 8.0] + upper = [2.0, 4.0, 6.0, 8.0, 10.0] + indexes = [0, 1, 2, 3, 4] + self.g.produce_points() + self.assertEqual(positions, self.g.points['x'].tolist()) + self.assertEqual(lower, self.g.points_lower['x'].tolist()) + self.assertEqual(upper, self.g.points_upper['x'].tolist()) + def test_iterator(self): positions = [1.0, 3.0, 5.0, 7.0, 9.0] lower = [0.0, 2.0, 4.0, 6.0, 8.0] diff --git a/tests/test_generators/test_lissajousgenerator.py b/tests/test_generators/test_lissajousgenerator.py index e6a0241..0c38898 100644 --- a/tests/test_generators/test_lissajousgenerator.py +++ b/tests/test_generators/test_lissajousgenerator.py @@ -64,6 +64,48 @@ def test_iterator(self): self.assertEqual(p.indexes, [indexes[i]]) self.assertEqual(i, 9) + def test_array_positions(self): + g = LissajousGenerator(['x', 'y'], "mm", self.bounding_box, 1, num_points=10) + positions = [{'y': 0.0, 'x': 0.5}, + {'y': 0.47552825814757677, 'x': 0.4045084971874737}, + {'y': 0.2938926261462366, 'x': 0.15450849718747375}, + {'y': -0.2938926261462365, 'x': -0.15450849718747364}, + {'y': -0.4755282581475768, 'x': -0.40450849718747367}, + {'y': -1.2246467991473532e-16, 'x': -0.5}, + {'y': 0.47552825814757677, 'x': -0.4045084971874738}, + {'y': 0.2938926261462367, 'x': -0.1545084971874738}, + {'y': -0.2938926261462364, 'x': 0.1545084971874736}, + {'y': -0.4755282581475769, 'x': 0.4045084971874736}] + lower = [{'y': -0.29389262614623657, 'x': 0.47552825814757677}, + {'y': 0.29389262614623657, 'x': 0.4755282581475768}, + {'y': 0.4755282581475768, 'x': 0.2938926261462366}, + {'y': 6.123233995736766e-17, 'x': 6.123233995736766e-17}, + {'y': -0.47552825814757677, 'x': -0.2938926261462365}, + {'y': -0.2938926261462367, 'x': -0.47552825814757677}, + {'y': 0.29389262614623607, 'x': -0.47552825814757682}, + {'y': 0.4755282581475768, 'x': -0.2938926261462367}, + {'y': 1.8369701987210297e-16, 'x': -1.2246467991473532e-16}, + {'y': -0.4755282581475767, 'x': 0.29389262614623646}] + upper = [{'y': 0.29389262614623657, 'x': 0.4755282581475768}, + {'y': 0.4755282581475768, 'x': 0.2938926261462366}, + {'y': 6.123233995736766e-17, 'x': 6.123233995736766e-17}, + {'y': -0.47552825814757677, 'x': -0.2938926261462365}, + {'y': -0.2938926261462367, 'x': -0.47552825814757677}, + {'y': 0.29389262614623607, 'x': -0.47552825814757682}, + {'y': 0.4755282581475768, 'x': -0.2938926261462367}, + {'y': 1.8369701987210297e-16, 'x': -1.2246467991473532e-16}, + {'y': -0.4755282581475767, 'x': 0.29389262614623646}, + {'y': -0.29389262614623674, 'x': 0.47552825814757677}] + + g.produce_points() + points = [{'x':x, 'y':y} for (x, y) in zip(g.points['x'], g.points['y'])] + points_upper = [{'x':x, 'y':y} for (x, y) in zip(g.points_upper['x'], g.points_upper['y'])] + points_lower = [{'x':x, 'y':y} for (x, y) in zip(g.points_lower['x'], g.points_lower['y'])] + self.assertEqual(10, len(points)) + self.assertEquals(points, positions) + self.assertEquals(points_upper, upper) + self.assertEquals(points_lower, lower) + def test_to_dict(self): expected_dict = dict() box = dict() diff --git a/tests/test_generators/test_spiralgenerator.py b/tests/test_generators/test_spiralgenerator.py index d50fafc..fa36a91 100644 --- a/tests/test_generators/test_spiralgenerator.py +++ b/tests/test_generators/test_spiralgenerator.py @@ -22,6 +22,36 @@ def test_duplicate_name_raises(self): with self.assertRaises(ValueError): SpiralGenerator(["x", "x"], "mm", [0.0, 0.0], 1.0) + def test_array_positions(self): + positions = [{'y': -0.3211855677650875, 'x': 0.23663214944574582}, + {'y': -0.25037538922751695, 'x': -0.6440318266552169}, + {'y': 0.6946549630820702, 'x': -0.5596688286164636}, + {'y': 0.9919687803189761, 'x': 0.36066957248394327}, + {'y': 0.3924587351155914, 'x': 1.130650533568409}, + {'y': -0.5868891557832875, 'x': 1.18586065489788}, + {'y': -1.332029488076613, 'x': 0.5428735608675326}] + lower = [{'y': 0.0, 'x': 0.0}, + {'y': -0.5189218293602549, 'x': -0.2214272368007088}, + {'y': 0.23645222432582483, 'x': -0.7620433832656455}, + {'y': 0.9671992383675001, 'x': -0.13948222773063082}, + {'y': 0.7807653675717078, 'x': 0.8146440851904461}, + {'y': -0.09160107657707395, 'x': 1.2582363345925418}, + {'y': -1.0190886264001306, 'x': 0.9334439933089926}] + upper = [{'y': -0.5189218293602549, 'x': -0.2214272368007088}, + {'y': 0.23645222432582483, 'x': -0.7620433832656455}, + {'y': 0.9671992383675001, 'x': -0.13948222773063082}, + {'y': 0.7807653675717078, 'x': 0.8146440851904461}, + {'y': -0.09160107657707395, 'x': 1.2582363345925418}, + {'y': -1.0190886264001306, 'x': 0.9334439933089926}, + {'y': -1.4911377166541206, 'x': 0.06839234794968006}] + self.g.produce_points() + p = [{"x":x, "y":y} for (x, y) in zip(self.g.points['x'], self.g.points['y'])] + l = [{"x":x, "y":y} for (x, y) in zip(self.g.points_lower['x'], self.g.points_lower['y'])] + u = [{"x":x, "y":y} for (x, y) in zip(self.g.points_upper['x'], self.g.points_upper['y'])] + self.assertEqual(positions, p) + self.assertEqual(lower, l) + self.assertEqual(upper, u) + def test_iterator(self): positions = [{'y': -0.3211855677650875, 'x': 0.23663214944574582}, {'y': -0.25037538922751695, 'x': -0.6440318266552169}, From f861456670036cb4f1e4be4276ddd8c5b690a94b Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Mon, 24 Oct 2016 10:54:07 +0100 Subject: [PATCH 02/47] Adjust SectorROI contains_point calculation --- scanpointgenerator/rois/sector_roi.py | 28 +++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/scanpointgenerator/rois/sector_roi.py b/scanpointgenerator/rois/sector_roi.py index 7608666..20e8e13 100644 --- a/scanpointgenerator/rois/sector_roi.py +++ b/scanpointgenerator/rois/sector_roi.py @@ -14,16 +14,21 @@ def __init__(self, centre, radii, angles): self.angles = self.constrain_angles(angles) def constrain_angles(self, angles): - # sort out tricky angles (i.e. make sure angle[0] < angle[1]) + # constrain angles such that angles[0] < angles[1], + # angles[0] in [0, 2pi), and angles[1] <= angles[0] + 2pi a1 = angles[0] a2 = angles[1] if a2 < a1: a2 += 2 * pi if a2 < a1: # input describes the full circle - a1 = 0 - a2 = 2 * pi - return [a1, a2] + return [0, 2*pi] + # a1 <= a2 + diff = a2 - a1 + if diff >= 2*pi: + return [0, 2*pi] + a1 = (a1 + 2*pi) % (2*pi) + return [a1, a1+diff] def contains_point(self, point): angles = self.constrain_angles(self.angles) @@ -32,21 +37,14 @@ def contains_point(self, point): y = point[1] - self.centre[1] r = hypot(x, y) phi = atan2(y, x) - if phi < 0: - phi += 2 * pi + phi = (2*pi + phi) % (2*pi) - # Easy checks first if r < self.radii[0] or r > self.radii[1]: return False - if phi >= self.angles[0] and phi < self.angles[1]: - return True - sweep = angles[1] - angles[0] - if sweep >= 2 * pi: - return True - if phi < angles[0]: - phi += 2 * pi - return phi <= angles[0] + sweep + # angle along starting at angles[0] + theta = (phi - angles[0] + 2*pi) % (2*pi) + return theta <= sweep def to_dict(self): d = super(SectorROI, self).to_dict() From 4d77d8ef71a2a290d03d5ec31eafccbc934170f5 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Mon, 24 Oct 2016 10:57:23 +0100 Subject: [PATCH 03/47] Fix PointROI contains_point bug Used to test both axes against the x-axis, not comparing the y-axis --- scanpointgenerator/rois/point_roi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanpointgenerator/rois/point_roi.py b/scanpointgenerator/rois/point_roi.py index 7add541..bc8d624 100644 --- a/scanpointgenerator/rois/point_roi.py +++ b/scanpointgenerator/rois/point_roi.py @@ -12,7 +12,7 @@ def contains_point(self, point, epsilon=0): if epsilon == 0: return list(self.point) == list(point) x = point[0] - self.point[0] - y = point[0] - self.point[0] + y = point[1] - self.point[1] return x * x + y * y <= epsilon * epsilon def to_dict(self): From 2ba7de311fc02e9aa18e5afff2617d776ec6366b Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Mon, 24 Oct 2016 10:51:40 +0100 Subject: [PATCH 04/47] Add mask generation for points in regions --- scanpointgenerator/rois/circular_roi.py | 9 ++++++++ scanpointgenerator/rois/elliptical_roi.py | 18 +++++++++++++++ scanpointgenerator/rois/linear_roi.py | 27 ++++++++++++++++++++++ scanpointgenerator/rois/point_roi.py | 9 ++++++++ scanpointgenerator/rois/polygonal_roi.py | 21 +++++++++++++++++ scanpointgenerator/rois/rectangular_roi.py | 14 +++++++++++ scanpointgenerator/rois/sector_roi.py | 18 +++++++++++++++ tests/test_rois/test_circular_roi.py | 19 ++++++++++----- tests/test_rois/test_elliptical_roi.py | 11 ++++++--- tests/test_rois/test_linear_roi.py | 10 +++++++- tests/test_rois/test_point_roi.py | 14 +++++++++++ tests/test_rois/test_polygonal_roi.py | 14 +++++++++++ tests/test_rois/test_rectangular_roi.py | 9 ++++++++ tests/test_rois/test_sector_roi.py | 13 +++++++++++ 14 files changed, 196 insertions(+), 10 deletions(-) diff --git a/scanpointgenerator/rois/circular_roi.py b/scanpointgenerator/rois/circular_roi.py index b2c9d84..5a69a67 100644 --- a/scanpointgenerator/rois/circular_roi.py +++ b/scanpointgenerator/rois/circular_roi.py @@ -1,5 +1,6 @@ from scanpointgenerator.core import ROI import math as m +import numpy as np @ROI.register_subclass("scanpointgenerator:roi/CircularROI:1.0") @@ -21,6 +22,14 @@ def contains_point(self, point): else: return True + def mask_points(self, points): + x = points[0] - self.centre[0] + y = points[1] - self.centre[1] + x *= x + y *= y + r2 = self.radius * self.radius + return x + y <= r2 + def to_dict(self): """Convert object attributes into a dictionary""" diff --git a/scanpointgenerator/rois/elliptical_roi.py b/scanpointgenerator/rois/elliptical_roi.py index b99915f..23073f6 100644 --- a/scanpointgenerator/rois/elliptical_roi.py +++ b/scanpointgenerator/rois/elliptical_roi.py @@ -1,3 +1,4 @@ +import numpy as np from math import cos, sin from scanpointgenerator.core import ROI @@ -29,6 +30,23 @@ def contains_point(self, point): return (x * x) / (rx * rx) + (y * y) / (ry * ry) <= 1 + def mask_points(self, points): + x = points[0] - self.centre[0] + y = points[1] - self.centre[1] + if self.angle != 0: + phi = -self.angle + tx = x * cos(phi) - y * sin(phi) + ty = x * sin(phi) + y * cos(phi) + x = tx + y = ty + rx2 = self.semiaxes[0] * self.semiaxes[0] + ry2 = self.semiaxes[1] * self.semiaxes[1] + x *= x + x /= rx2 + y *= y + y /= ry2 + return x + y <= 1 + def to_dict(self): d = super(EllipticalROI, self).to_dict() d["centre"] = self.centre diff --git a/scanpointgenerator/rois/linear_roi.py b/scanpointgenerator/rois/linear_roi.py index 32e5f14..84d4b1e 100644 --- a/scanpointgenerator/rois/linear_roi.py +++ b/scanpointgenerator/rois/linear_roi.py @@ -1,4 +1,5 @@ from math import cos, sin +import numpy as np from scanpointgenerator.core import ROI @@ -40,6 +41,32 @@ def contains_point(self, point, epsilon=1e-15): d = abs(x * sphi + y * -cphi) return d < epsilon + def mask_points(self, points, epsilon=1e-15): + cphi = cos(self.angle) + sphi = sin(self.angle) + x = points[0] - self.start[0] + y = points[1] - self.start[1] + + # test for being past segment end-points + dp = x * cphi + y * sphi + mask = (dp >= 0) & (dp <= self.length) + + # distance is scalar projection (dot-product) of + # point difference to line normal (normal = (sphi, -cphi)) + dp = np.abs(x * sphi + y * -cphi) + mask &= dp < epsilon + + # include all points in an epsilon circle around endpoints + x *= x + y *= y + mask |= x + y <= epsilon * epsilon + x = points[0] - (self.start[0] + self.length * cphi) + y = points[1] - (self.start[1] + self.length * sphi) + x *= x + y *= y + mask |= x + y <= epsilon * epsilon + return mask + def to_dict(self): d = super(LinearROI, self).to_dict() d["start"] = self.start diff --git a/scanpointgenerator/rois/point_roi.py b/scanpointgenerator/rois/point_roi.py index bc8d624..6b4ac9b 100644 --- a/scanpointgenerator/rois/point_roi.py +++ b/scanpointgenerator/rois/point_roi.py @@ -1,3 +1,5 @@ +import numpy as np + from scanpointgenerator.core import ROI @@ -15,6 +17,13 @@ def contains_point(self, point, epsilon=0): y = point[1] - self.point[1] return x * x + y * y <= epsilon * epsilon + def mask_points(self, points, epsilon=0): + x = points[0] - self.point[0] + y = points[1] - self.point[1] + x *= x + y *= y + return (x + y) <= epsilon * epsilon + def to_dict(self): d = super(PointROI, self).to_dict() d["point"] = self.point diff --git a/scanpointgenerator/rois/polygonal_roi.py b/scanpointgenerator/rois/polygonal_roi.py index 22c2909..60a5654 100644 --- a/scanpointgenerator/rois/polygonal_roi.py +++ b/scanpointgenerator/rois/polygonal_roi.py @@ -1,4 +1,7 @@ +import numpy as np + from scanpointgenerator.core import ROI +from scanpointgenerator.compat import range_ @ROI.register_subclass("scanpointgenerator:roi/PolygonalROI:1.0") @@ -30,6 +33,24 @@ def contains_point(self, point): v1 = v2 return inside + def mask_points(self, points): + x = points[0] + y = points[1] + v1 = self.points[-1] + mask = np.full(len(x), False, dtype=bool) + for i in range_(0, len(self.points)): + v2 = self.points[i] + if (v2[1] == v1[1]): + # skip horizontal edges + v1 = v2 + continue + vmask = ((y < v2[1]) & (y >= v1[1])) | ((y < v1[1]) & (y >= v2[1])) + t = (y - v2[1]) / (v2[1] - v1[1]) + vmask &= x < v1[0] + t * (v2[0] - v1[0]) + mask ^= vmask + v1 = v2 + return mask + def to_dict(self): d = super(PolygonalROI, self).to_dict() d["points"] = self.points diff --git a/scanpointgenerator/rois/rectangular_roi.py b/scanpointgenerator/rois/rectangular_roi.py index 93d6fa1..6a8bcfc 100644 --- a/scanpointgenerator/rois/rectangular_roi.py +++ b/scanpointgenerator/rois/rectangular_roi.py @@ -1,3 +1,4 @@ +import numpy as np from math import cos, sin from scanpointgenerator.core import ROI @@ -30,6 +31,19 @@ def contains_point(self, point): return (x >= 0 and x < self.width) \ and (y >= 0 and y < self.height) + def mask_points(self, points): + x = points[0] - self.start[0] + y = points[1] - self.start[1] + if self.angle != 0: + phi = -self.angle + rx = x * cos(phi) - y * sin(phi) + ry = x * sin(phi) + y * cos(phi) + x = rx + y = ry + mask_x = np.logical_and(x >= 0, x < self.width) + mask_y = np.logical_and(y >= 0, y < self.height) + return np.logical_and(mask_x, mask_y) + def to_dict(self): d = super(RectangularROI, self).to_dict() d['start'] = self.start diff --git a/scanpointgenerator/rois/sector_roi.py b/scanpointgenerator/rois/sector_roi.py index 20e8e13..e355dd7 100644 --- a/scanpointgenerator/rois/sector_roi.py +++ b/scanpointgenerator/rois/sector_roi.py @@ -1,5 +1,6 @@ from scanpointgenerator.core import ROI from math import hypot, atan2, pi +import numpy as np @ROI.register_subclass("scanpointgenerator:roi/SectorROI:1.0") @@ -46,6 +47,23 @@ def contains_point(self, point): theta = (phi - angles[0] + 2*pi) % (2*pi) return theta <= sweep + def mask_points(self, points): + x = points[0] - self.centre[0] + y = points[1] - self.centre[1] + r2 = (np.square(x) + np.square(y)) + phi_0, phi_1 = self.constrain_angles(self.angles) + # phi_0 <= phi_1, phi_0 in [0, 2pi), phi_1 < 4pi + phi_x = np.arctan2(y, x) + # translate phi_x to range [0, 2pi] + phi_x = (2*pi + phi_x) % (2*pi) + # define phi_s and phi_x "offset from phi_0" + phi_s = phi_1 - phi_0 + phi_x -= phi_0 + 2*pi + phi_x %= 2*pi + mask = (r2 <= self.radii[1]) & (r2 >= self.radii[0]) + mask &= (phi_x <= phi_s) + return mask + def to_dict(self): d = super(SectorROI, self).to_dict() d["centre"] = self.centre diff --git a/tests/test_rois/test_circular_roi.py b/tests/test_rois/test_circular_roi.py index f70c9af..15a04f1 100644 --- a/tests/test_rois/test_circular_roi.py +++ b/tests/test_rois/test_circular_roi.py @@ -3,6 +3,8 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "..")) import unittest +import numpy as np + from test_util import ScanPointGeneratorTest from scanpointgenerator.rois.circular_roi import CircularROI @@ -30,17 +32,22 @@ def test_given_valid_params_then_set(self): class ContainsPointTest(unittest.TestCase): def setUp(self): - self.Circle = CircularROI([5.0, 15.0], 5.0) - - self.point = [7.0, 11.0] + self.roi = CircularROI([5.0, 15.0], 5.0) def test_given_valid_point_then_return_True(self): - self.assertTrue(self.Circle.contains_point(self.point)) + point = [7.0, 11.0] + self.assertTrue(self.roi.contains_point(point)) def test_given_point_outside_then_return_False(self): - self.point = [9.0, 11.0] + point = [9.0, 11.0] + self.assertFalse(self.roi.contains_point(point)) + + def test_mask_points(self): + points = [np.array([7., 9.]), np.array([11., 11.])] + expected = [True, False] + mask = self.roi.mask_points(points) + self.assertEqual(expected, mask.tolist()) - self.assertFalse(self.Circle.contains_point(self.point)) class DictTest(unittest.TestCase): diff --git a/tests/test_rois/test_elliptical_roi.py b/tests/test_rois/test_elliptical_roi.py index d9bb495..f21621d 100644 --- a/tests/test_rois/test_elliptical_roi.py +++ b/tests/test_rois/test_elliptical_roi.py @@ -3,6 +3,7 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "..")) import unittest from math import pi +import numpy as np from test_util import ScanPointGeneratorTest from scanpointgenerator.rois.elliptical_roi import EllipticalROI @@ -42,9 +43,6 @@ def test_point_contains(self): self.assertFalse(roi.contains_point(p)) def test_rotated_point_contains(self): - r = EllipticalROI([1, 2], [2, 1], 0) - p = (1, 3) - self.assertTrue(r.contains_point(p)) roi = EllipticalROI([1, 2], [2, 1], pi/6) p = (3, 1) self.assertFalse(roi.contains_point(p)) @@ -57,6 +55,13 @@ def test_rotated_point_contains(self): p = (-0.73, 1) self.assertTrue(roi.contains_point(p)) + def test_mask_points(self): + roi = EllipticalROI([1, 2], [2, 1], pi/6) + points = [np.array([3, 1, 2.73, 3.00, -0.73]), + np.array([1, 0, 3.00, 4.27, 1.000])] + expected = [False, False, True, False, True] + self.assertEquals(expected, roi.mask_points(points).tolist()) + def test_to_dict(self): roi = EllipticalROI([1.1, 2.2], [3.3, 4.4], pi/4) expected = { diff --git a/tests/test_rois/test_linear_roi.py b/tests/test_rois/test_linear_roi.py index b7fafaf..a68895d 100644 --- a/tests/test_rois/test_linear_roi.py +++ b/tests/test_rois/test_linear_roi.py @@ -2,6 +2,7 @@ import sys sys.path.append(os.path.join(os.path.dirname(__file__), "..")) import unittest +import numpy as np from math import pi, sqrt from test_util import ScanPointGeneratorTest @@ -78,12 +79,19 @@ def test_negative_angle(self): p = [-0.0001, 0.0001] self.assertFalse(l.contains_point(p)) - def test_wraparound_angle(self): l = LinearROI([0, 0], sqrt(2), 2*pi + pi/4) p = [1, 1] self.assertTrue(l.contains_point(p)) + def test_mask_points(self): + l = LinearROI([1, 1], 2*sqrt(2), -pi/4 - 2*pi) + px = [1, 2, 3, 1+1e-10, 3-1e-10, 2+1e-10, 2-1e-14, 3+1e-14, 1] + py = [1, 0, -1, 1 , -1 , 0+1e-10, 0+1e-14, -1-1e-14, 1+1e-14] + p = [np.array(px), np.array(py)] + expected = [True, True, True, False, False, False, True, True, True] + mask = l.mask_points(p, 1e-12) + self.assertEquals(expected, mask.tolist()) class LinearROIDictTest(unittest.TestCase): diff --git a/tests/test_rois/test_point_roi.py b/tests/test_rois/test_point_roi.py index 93554a7..6cec7bb 100644 --- a/tests/test_rois/test_point_roi.py +++ b/tests/test_rois/test_point_roi.py @@ -2,6 +2,7 @@ import sys sys.path.append(os.path.join(os.path.dirname(__file__), "..")) import unittest +import numpy as np from test_util import ScanPointGeneratorTest from scanpointgenerator.rois.point_roi import PointROI @@ -31,6 +32,19 @@ def test_contains_point_approx(self): p = [1 + 1e-14, 1 + 1e-14] self.assertFalse(roi.contains_point(p, 1e-14)) + def test_mask_points(self): + roi = PointROI([1, 2]) + px = [1, 0, 1+1e-15, 1, 1] + py = [2, 0, 2, 2+1e-15, 2+1e-14] + points = [np.array(px), np.array(py)] + expected = [True, False, True, True, False] + mask = roi.mask_points(points, 2e-15) + self.assertEqual(expected, mask.tolist()) + + mask = roi.mask_points(points, 0) + expected = [True, False, False, False, False] + self.assertEqual(expected, mask.tolist()) + def test_to_dict(self): roi = PointROI([1.1, 2.2]) expected = { diff --git a/tests/test_rois/test_polygonal_roi.py b/tests/test_rois/test_polygonal_roi.py index e728ec4..42aeb0e 100644 --- a/tests/test_rois/test_polygonal_roi.py +++ b/tests/test_rois/test_polygonal_roi.py @@ -3,6 +3,8 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "..")) import unittest +import numpy as np + from test_util import ScanPointGeneratorTest from scanpointgenerator.rois.polygonal_roi import PolygonalROI @@ -64,5 +66,17 @@ def test_complex_point_contains(self): p = [1.5, 1.5] # has winding number -2 self.assertFalse(roi.contains_point(p)) + def test_mask_points(self): + vertices = [[0, 0], [0, 2], [2, 2], [2, 1], + [1, 1], [1, 3], [3, 3], [3, 0]] + roi = PolygonalROI(vertices) + px = [0.5, 0.5, 1.5, 1.5, 2.5, 2.5, 3.5, -0.5, 3.5, 2, 3, 0] + py = [0.5, 2.5, 1.5, 2.5, 2.5, -0.5, 1.5, 0.5, 0.5, 0, 2, 1.5] + p = [np.array(px), np.array(py)] + expected = [True, False, False, True, True, False, False, False, False, + True, False, True] + mask = roi.mask_points(p) + self.assertEquals(expected, mask.tolist()) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_rois/test_rectangular_roi.py b/tests/test_rois/test_rectangular_roi.py index 6977ea3..381d978 100644 --- a/tests/test_rois/test_rectangular_roi.py +++ b/tests/test_rois/test_rectangular_roi.py @@ -2,6 +2,7 @@ import sys sys.path.append(os.path.join(os.path.dirname(__file__), "..")) import unittest +import numpy as np from math import pi from test_util import ScanPointGeneratorTest @@ -90,6 +91,14 @@ def test_rotated_rectangle(self): p = [0.99, 1.99] self.assertFalse(roi.contains_point(p)) + def test_mask(self): + roi = RectangularROI([1, 2], 1, 1, pi/4) + points = [np.array([2, 2, 1, 1, 1.7, 0.3, 1.01, 0.99]), + np.array([3, 2, 2, 3.4, 2.7, 2.705, 1.99, 1.99])] + expected = [False, False, True, True, True, True, False, False] + mask = roi.mask_points(points) + self.assertEqual(expected, mask.tolist()) + class DictTest(unittest.TestCase): def test_to_dict(self): diff --git a/tests/test_rois/test_sector_roi.py b/tests/test_rois/test_sector_roi.py index 52fe8f5..4e77d3f 100644 --- a/tests/test_rois/test_sector_roi.py +++ b/tests/test_rois/test_sector_roi.py @@ -3,6 +3,7 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "..")) import unittest from math import pi +import numpy as np from test_util import ScanPointGeneratorTest from scanpointgenerator.rois.sector_roi import SectorROI @@ -144,5 +145,17 @@ def test_negative_ranges(self): p = (0, -1) self.assertTrue(s.contains_point(p)) + def test_mask_points(self): + s = SectorROI((0, 0), (1, 2), (-pi/2, pi/2)) + p = [np.array([0, 1, 0, 2.05, 1, 0.7, -1.5, 0.00]), + np.array([0, 0, 1, 0.00, 1, 0.7, 0.00, 2.05])] + expected = [False, True, True, False, True, False, False, False] + mask = s.mask_points(p) + self.assertEquals(expected, mask.tolist()) + + s2 = SectorROI((0, 0), (1, 2), (-5*pi/2, -3*pi/2)) #the same sector + mask = s.mask_points(p) + self.assertEquals(expected, mask.tolist()) + if __name__ == "__main__": unittest.main() From 9b026a4a0454e4393684930953a5282870be2bb6 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Fri, 4 Nov 2016 15:06:36 +0000 Subject: [PATCH 05/47] Rewrite compound generator to handle new point generation mechanism --- scanpointgenerator/core/compoundgenerator.py | 387 +++++++++---------- scanpointgenerator/core/excluder.py | 3 + 2 files changed, 190 insertions(+), 200 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index 68b4501..47e8be5 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -1,4 +1,5 @@ import logging +import numpy as np from threading import Lock from scanpointgenerator.compat import range_ @@ -11,7 +12,7 @@ @Generator.register_subclass("scanpointgenerator:generator/CompoundGenerator:1.0") class CompoundGenerator(Generator): """Nest N generators, apply exclusion regions to relevant generator pairs - and apply any mutators before yielding points""" + and apply any mutators before yielding points""" def __init__(self, generators, excluders, mutators): """ @@ -21,236 +22,222 @@ def __init__(self, generators, excluders, mutators): mutators(list(Mutator)): List of Mutators to apply to each point """ - self.generators = generators self.excluders = excluders self.mutators = mutators - - self.alternate_direction = [] - self.point_sets = [] - self.index_dims = [] - self.index_names = [] self.axes = [] self.position_units = {} - - for generator in self.generators: + self.axes_points = {} + self.axes_points_lower = {} + self.axes_points_upper = {} + self.index_dims = [] + self.indexes = [] + self.alternate_direction = [g.alternate_direction for g in generators] + for generator in generators: logging.debug("Generator passed to Compound init") logging.debug(generator.to_dict()) - if isinstance(generator, self.__class__): raise TypeError("CompoundGenerators cannot be nested, nest" "its constituent parts instead") - - self.alternate_direction.append(generator.alternate_direction) - self.point_sets.append(list(generator.iterator())) self.axes += generator.axes - - self.index_dims += generator.index_dims - self.index_names += generator.index_names self.position_units.update(generator.position_units) - - self.num = 1 - self.periods = [] - for generator in self.generators[::-1]: - self.num *= generator.num - self.periods.insert(0, self.num) - - logging.debug("CompoundGenerator periods") - logging.debug(self.periods) - - if self.excluders: # Calculate number of remaining points and flatten - # index dimensions - remaining_points = 0 - for _ in self._filtered_base_iterator(): - # TODO: Faster with enumerate()? - remaining_points += 1 - self.index_dims = [remaining_points] - self.num = remaining_points - if len(self.axes) != len(set(self.axes)): - raise ValueError("Axis names cannot be duplicated; given %s" % - self.index_names) - - # These are set when using the get_point() interface - self._cached_iterator = None - self._cached_points = [] - self._cached_lock = Lock() - - def _get_sub_point(self, gen_index, point_num): - points = self.point_sets[gen_index] - axis_period = self.periods[gen_index] - axis_length = len(points) - # Can't use index_dims in case they have been flattened - # by an excluder - - point_index = \ - (point_num / (axis_period / axis_length)) % axis_length - loop_number = point_num / axis_period - - # Floor floats to ints for indexing - point_index = int(point_index) - loop_number = int(loop_number) - if self.alternate_direction[gen_index] and loop_number % 2: - point_index = (axis_length - 1) - point_index - reverse = True - else: - reverse = False - - sub_point = points[point_index] - - return reverse, sub_point - - def _base_iterator(self): - """ - Iterator to generate points by nesting each generator in self.generators - - Yields: - Point: Base points - """ - num_point_sets = len(self.point_sets) - for point_num in range_(self.num): - point = Point() - for gen_index in range_(num_point_sets - 1): - reverse, sub_point = self._get_sub_point(gen_index, point_num) - - # Outer indexes use positions - point.positions.update(sub_point.positions) - point.upper.update(sub_point.positions) - point.lower.update(sub_point.positions) - point.indexes += sub_point.indexes - - # If innermost generator, use bounds - reverse, sub_point = self._get_sub_point( - num_point_sets - 1, point_num) - point.positions.update(sub_point.positions) - if reverse: # Swap bounds if reversing - point.upper.update(sub_point.lower) - point.lower.update(sub_point.upper) - else: - point.upper.update(sub_point.upper) - point.lower.update(sub_point.lower) - point.indexes += sub_point.indexes - - yield point - - def _filtered_base_iterator(self): - """ - Iterator to filter out points based on Excluders - - Yields: - Point: Filtered points - """ - - for point in self._base_iterator(): - if self.contains_point(point): - yield point + raise ValueError("Axis names cannot be duplicated") - def iterator(self): - """ - Top level iterator to mutate points and yield them - - Yields: - Point: Mutated points - """ - if self.excluders: - iterator = self._filtered_base_iterator() - else: - iterator = self._base_iterator() - - for mutator in self.mutators: - iterator = mutator.mutate(iterator) - - if self.excluders: - point_index = 0 - for point in iterator: - point.indexes = [point_index] - point_index += 1 - yield point - else: - for point in iterator: - yield point - - def contains_point(self, point): - """ - Filter a Point through all Excluders - - Args: - point(Point): Point to check + self.generators = generators + self.generator_idx_scaling = {} - Returns: - bool: Whether point is contained by all Excluders - """ + def prepare(self): + self.num = 1 + self.indexes = [] + for generator in self.generators: + generator.produce_points() + self.axes_points.update(generator.points) + self.axes_points_lower.update(generator.points_lower) + self.axes_points_upper.update(generator.points_upper) + self.num *= generator.num + self.index_dims += generator.index_dims - contains_point = True + idx = {"size":generator.num, + "axes":list(generator.axes), + "generators":[generator], + "masks":[], + "tile":1, + "repeat":1} + self.indexes.append(idx) for excluder in self.excluders: - if not excluder.contains_point(point.positions): - contains_point = False - break + axis_1, axis_2 = excluder.scannables + + ##### + # first check if region spans two indexes - merge if so + ##### + idx_1 = [i for i in self.indexes if axis_1 in i["axes"]][0] + idx_2 = [i for i in self.indexes if axis_2 in i["axes"]][0] + order_diff = self.indexes.index(idx_1) - self.indexes.index(idx_2) + # merge "inner" into "outer" + if order_diff < -1 or order_diff > 1: + raise ValueError( + "Excluders must be defined on axes that are adjacent in \ + generator order") + if order_diff == 1: + idx_1, idx_2 = idx_2, idx_1 + order_diff = -1 + if order_diff == -1: + # idx_1 is "outer" - preserves axis ordering + + # need to appropriately scale the existing masks + # masks are "tiled" by the size of generators "below" them + # and their elements are "repeated" by the size of generators + # above them, so: + # |mask| * duplicates * repeates == |generators in index| + scale = 1 + for g in idx_2["generators"]: + scale *= g.num + for m in idx_1["masks"]: + m["repeat"] *= scale + scale = 1 + for g in idx_1["generators"]: + scale *= g.num + for m in idx_2["masks"]: + m["tile"] *= scale + idx_1["masks"] += idx_2["masks"] + idx_1["axes"] += idx_2["axes"] + idx_1["generators"] += idx_2["generators"] + idx_1["size"] *= idx_2["size"] + self.indexes.remove(idx_2) + idx = idx_1 + + ##### + # generate the mask for this region + ##### + gen_1 = [g for g in self.generators if axis_1 in g.axes][0] + gen_2 = [g for g in self.generators if axis_2 in g.axes][0] + # if gen_1 and gen_2 are different then the outer axis will have to + # have its elements repeated and the inner axis will have to have + # itself repeated - need to determine which is the inner axis + a1_scalef = np.repeat + a2_scalef = np.tile + order_diff = self.generators.index(gen_1) \ + - self.generators.index(gen_2) + + if order_diff < -1 or order_diff > 1: + raise ValueError( + "Excluders must be defined on axes that are adjacent in \ + generator order") + if order_diff == 1: + # axis order is reversed - swap "scaling" functions + a1_scalef, a2_scalef = a2_scalef, a1_scalef + order_diff = -1 + + points_1 = self.axes_points[axis_1] + points_2 = self.axes_points[axis_2] + if order_diff == -1: + points_1 = a1_scalef(points_1, gen_2.num) + points_2 = a2_scalef(points_2, gen_1.num) + + mask = excluder.create_mask(points_1, points_2) + + ##### + # Add new mask to index + ##### + tile = 1 + repeat = 1 + found_axis = False + # tile by product of generators "before" + # repeat by product of generators "after" + for g in idx["generators"]: + if axis_1 in g.axes or axis_2 in g.axes: + found_axis = True + else: + if found_axis: + repeat *= g.num + else: + tile *= g.num + m = {"repeat":repeat, "tile":tile, "mask":mask} + idx["masks"].append(m) + # end for excluder in self.excluders + ##### + + tile = 1 + repeat = 1 + ##### + # Generate full index mask and "apply" + ##### + for idx in self.indexes: + mask = np.full(idx["size"], True, dtype=np.bool) + for m in idx["masks"]: + assert len(m["mask"]) * m["repeat"] * m["tile"] == len(mask), \ + "Mask lengths are not consistent" + expanded = np.repeat(m["mask"], m["repeat"]) + expanded = np.tile(expanded, m["tile"]) + mask &= expanded + idx["mask"] = mask + idx["indicies"] = np.flatnonzero(mask) + if len(idx["indicies"]) == 0: + raise ValueError("Regions would exclude entire scan") + repeat *= len(idx["indicies"]) + self.num = repeat + for idx in self.indexes: + l = len(idx["indicies"]) + repeat /= l + idx["tile"] = tile + idx["repeat"] = repeat + tile *= l + + for idx in self.indexes: + tile = 1 + repeat = 1 + for g in idx["generators"]: + repeat *= g.num + for g in idx["generators"]: + repeat /= g.num + d = {"tile":tile, "repeat":repeat} + tile *= g.num + self.generator_idx_scaling[g] = d - return contains_point - - def get_point(self, num): - # This is the only thread safe function in scanpointgenerator - if self._cached_iterator is None: - self._cached_iterator = self.iterator() - - if num >= len(self._cached_points): - # Generate some more points and cache them - try: - self._cached_lock.acquire() - # Get npoints again in case someone else added them - npoints = len(self._cached_points) - for i in range(num - npoints + 1): - self._cached_points.append(next(self._cached_iterator)) - except: - self._cached_lock.release() - raise - else: - self._cached_lock.release() - return self._cached_points[num] + def iterator(self): + # just one axis for now + for n in range_(self.num): + yield self.get_point(n) + + def get_point(self, n): + # how far along each index are we? + if n >= self.num: + raise IndexError("Requested point is out of range") + p = Point() + for idx in self.indexes: + i = n // idx["repeat"] + i %= len(idx["indicies"]) + k = idx["indicies"][i] + # need point k along each generator in index + for g in idx["generators"]: + j = k // self.generator_idx_scaling[g]["repeat"] + j %= g.num + for axis in g.axes: + p.positions[axis] = g.points[axis][j] + p.lower[axis] = g.points_lower[axis][j] + p.upper[axis] = g.points_upper[axis][j] + return p def to_dict(self): """Convert object attributes into a dictionary""" - - d = dict() + d = {} d['typeid'] = self.typeid - - d['generators'] = [] - for generator in self.generators: - d['generators'].append(generator.to_dict()) - - d['excluders'] = [] - for excluder in self.excluders: - d['excluders'].append(excluder.to_dict()) - - d['mutators'] = [] - for mutator in self.mutators: - d['mutators'].append(mutator.to_dict()) - + d['generators'] = [g.to_dict() for g in self.generators] + d['excluders'] = [e.to_dict() for e in self.excluders] + d['mutators'] = [m.to_dict() for m in self.mutators] return d @classmethod def from_dict(cls, d): """ Create a CompoundGenerator instance from a serialised dictionary - Args: d(dict): Dictionary of attributes - Returns: CompoundGenerator: New CompoundGenerator instance """ - - generators = [] - for generator in d['generators']: - generators.append(Generator.from_dict(generator)) - - excluders = [] - for excluder in d['excluders']: - excluders.append(Excluder.from_dict(excluder)) - - mutators = [] - for mutator in d['mutators']: - mutators.append(Mutator.from_dict(mutator)) - + generators = [Generator.from_dict(g) for g in d['generators']] + excluders = [Excluder.from_dict(e) for e in d['excluders']] + mutators = [Mutator.from_dict(m) for m in d['mutators']] return cls(generators, excluders, mutators) diff --git a/scanpointgenerator/core/excluder.py b/scanpointgenerator/core/excluder.py index 16d8484..723512c 100644 --- a/scanpointgenerator/core/excluder.py +++ b/scanpointgenerator/core/excluder.py @@ -34,6 +34,9 @@ def contains_point(self, d): return self.roi.contains_point(sub_point) + def create_mask(self, x_points, y_points): + return self.roi.mask_points([x_points, y_points]) + def to_dict(self): """Convert object attributes into a dictionary""" From 4310c6acb82302a27e9ae52ca7d87cb3b6985260 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Tue, 8 Nov 2016 17:43:23 +0000 Subject: [PATCH 06/47] Handle generators that alternate directions in compound generator This proved to be tricky. The code for compound generator has gotten quite complicated. --- scanpointgenerator/core/compoundgenerator.py | 125 ++++++++++++++----- 1 file changed, 97 insertions(+), 28 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index 47e8be5..b16753c 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -67,22 +67,36 @@ def prepare(self): for excluder in self.excluders: axis_1, axis_2 = excluder.scannables + # ensure axis_1 is "outer" axis (if separate generators) + gen_1 = [g for g in self.generators if axis_1 in g.axes][0] + gen_2 = [g for g in self.generators if axis_2 in g.axes][0] + gen_diff = self.generators.index(gen_1) \ + - self.generators.index(gen_2) + if gen_diff < -1 or gen_diff > 1: + raise ValueError( + "Excluders must be defined on axes that are adjacent in \ + generator order") + if gen_diff == 1: + gen_1, gen_2 = gen_2, gen_1 + axis_1, axis_2 = axis_2, axis_1 + gen_diff = -1 + ##### # first check if region spans two indexes - merge if so ##### idx_1 = [i for i in self.indexes if axis_1 in i["axes"]][0] idx_2 = [i for i in self.indexes if axis_2 in i["axes"]][0] - order_diff = self.indexes.index(idx_1) - self.indexes.index(idx_2) + idx_diff = self.indexes.index(idx_1) - self.indexes.index(idx_2) # merge "inner" into "outer" - if order_diff < -1 or order_diff > 1: + if idx_diff < -1 or idx_diff > 1: raise ValueError( "Excluders must be defined on axes that are adjacent in \ generator order") - if order_diff == 1: + if idx_diff == 1: idx_1, idx_2 = idx_2, idx_1 - order_diff = -1 - if order_diff == -1: + idx_diff = -1 + if idx_diff == -1: # idx_1 is "outer" - preserves axis ordering # need to appropriately scale the existing masks @@ -110,37 +124,59 @@ def prepare(self): ##### # generate the mask for this region ##### - gen_1 = [g for g in self.generators if axis_1 in g.axes][0] - gen_2 = [g for g in self.generators if axis_2 in g.axes][0] # if gen_1 and gen_2 are different then the outer axis will have to # have its elements repeated and the inner axis will have to have # itself repeated - need to determine which is the inner axis - a1_scalef = np.repeat - a2_scalef = np.tile - order_diff = self.generators.index(gen_1) \ - - self.generators.index(gen_2) - - if order_diff < -1 or order_diff > 1: - raise ValueError( - "Excluders must be defined on axes that are adjacent in \ - generator order") - if order_diff == 1: - # axis order is reversed - swap "scaling" functions - a1_scalef, a2_scalef = a2_scalef, a1_scalef - order_diff = -1 points_1 = self.axes_points[axis_1] points_2 = self.axes_points[axis_2] - if order_diff == -1: - points_1 = a1_scalef(points_1, gen_2.num) - points_2 = a2_scalef(points_2, gen_1.num) - mask = excluder.create_mask(points_1, points_2) + doubled_mask = False # used for some cases of alternating generators + + if gen_1 is gen_2 and gen_1.alternate_direction: + # run *both* axes backwards + # but our mask will be a factor of 2 too big + doubled_mask = True + points_1 = np.append(points_1, points_1[::-1]) + points_2 = np.append(points_2, points_2[::-1]) + elif gen_1.alternate_direction and gen_2.alternate_direction: + doubled_mask = True + points_1 = np.append(points_1, points_1[::-1]) + points_2 = np.append(points_2, points_2[::-1]) + #tile = gen_1.num + #points_2 = np.tile(points_2, tile) + tile = gen_1.num / 2. + tmp = np.tile(points_2, int(tile)) + if tile % 1 != 0: + points_2 = np.append(tmp, points_2[:gen_2.num]) + points_2 = np.tile(points_2, 2) + points_1 = np.repeat(points_1, gen_2.num) + elif gen_2.alternate_direction: + points_1 = np.repeat(points_1, gen_2.num) + points_2 = np.append(points_2, points_2[::-1]) + tile = gen_1.num / 2. + points_2 = np.tile(points_2, int(tile)) + if tile % 1 != 0: + points_2 = np.append( + points_2, self.axes_points[axis_2]) + elif gen_1.alternate_direction: + doubled_mask = True + points_1 = np.append(points_1, points_1[::-1]) + points_1 = np.repeat(points_1, gen_2.num) + points_2 = np.tile(points_2, gen_1.num * 2) + elif gen_1 is not gen_2: + points_1 = np.repeat(points_1, gen_2.num) + points_2 = np.tile(points_2, gen_1.num) + + if axis_1 == excluder.scannables[0]: + mask = excluder.create_mask(points_1, points_2) + else: + mask = excluder.create_mask(points_2, points_1) ##### # Add new mask to index ##### - tile = 1 + tile = 0.5 if doubled_mask else 1 repeat = 1 found_axis = False # tile by product of generators "before" @@ -164,15 +200,36 @@ def prepare(self): # Generate full index mask and "apply" ##### for idx in self.indexes: + # if first generator alternates then we want a reverse mask + reverse = idx["generators"][0].alternate_direction + rmask = np.full(idx["size"], True, dtype=np.bool) if reverse else None mask = np.full(idx["size"], True, dtype=np.bool) for m in idx["masks"]: assert len(m["mask"]) * m["repeat"] * m["tile"] == len(mask), \ "Mask lengths are not consistent" expanded = np.repeat(m["mask"], m["repeat"]) - expanded = np.tile(expanded, m["tile"]) + if m["tile"] < 1: + # the second half of this mask forms part of the "reverse" + if reverse: + rmask &= expanded[len(expanded)//2:] + expanded = expanded[:len(expanded)//2] + elif m["tile"] % 1 != 0: + ex = np.tile(expanded, int(m["tile"])) + expanded = np.append(ex, expanded[:len(expanded)//2]) + if reverse: + rmask &= expanded + else: + expanded = np.tile(expanded, int(m["tile"])) + if reverse: + rmask &= expanded mask &= expanded idx["mask"] = mask idx["indicies"] = np.flatnonzero(mask) + idx["rmask"] = rmask + idx["rindicies"] = np.flatnonzero(rmask) if reverse else None + if reverse: + assert len(idx["indicies"]) == len(idx["rindicies"]), \ + "Index indicies and reverse direction indicies are not of equal length" if len(idx["indicies"]) == 0: raise ValueError("Regions would exclude entire scan") repeat *= len(idx["indicies"]) @@ -205,18 +262,30 @@ def get_point(self, n): if n >= self.num: raise IndexError("Requested point is out of range") p = Point() + prev_g = None + idx_reverse = False + gen_reverse = False for idx in self.indexes: + indicies = idx["indicies"] + if idx_reverse and idx["rindicies"] is not None: + indicies = idx["rindicies"] i = n // idx["repeat"] - i %= len(idx["indicies"]) - k = idx["indicies"][i] + i %= len(indicies) + k = indicies[i] # need point k along each generator in index for g in idx["generators"]: j = k // self.generator_idx_scaling[g]["repeat"] j %= g.num + if g.alternate_direction: + if gen_reverse: + j = g.num - j - 1 + gen_reverse = j % 2 == 1 for axis in g.axes: p.positions[axis] = g.points[axis][j] p.lower[axis] = g.points_lower[axis][j] p.upper[axis] = g.points_upper[axis][j] + prev_g = g + idx_reverse = gen_reverse return p def to_dict(self): From ed7f0784ebbda8a4e5115d56a2411566a07c5441 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Wed, 9 Nov 2016 11:05:15 +0000 Subject: [PATCH 07/47] Call mutators in CompoundGenerator.iterator This functionality went AWOL briefly during the rewrite. --- scanpointgenerator/core/compoundgenerator.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index b16753c..aaea6f9 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -253,9 +253,11 @@ def prepare(self): self.generator_idx_scaling[g] = d def iterator(self): - # just one axis for now - for n in range_(self.num): - yield self.get_point(n) + it = (self.get_point(n) for n in range_(self.num)) + for m in self.mutators: + it = m.mutate(it) + for p in it: + yield p def get_point(self, n): # how far along each index are we? From c428a68c935d8d8f5533ac680036d8657e566ae9 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Wed, 9 Nov 2016 11:08:24 +0000 Subject: [PATCH 08/47] Rewrite tests for CompoundGenerator after its big change --- tests/test_core/test_compoundgenerator.py | 574 +++++++++++++++++----- 1 file changed, 445 insertions(+), 129 deletions(-) diff --git a/tests/test_core/test_compoundgenerator.py b/tests/test_core/test_compoundgenerator.py index 8a941da..79b4dea 100644 --- a/tests/test_core/test_compoundgenerator.py +++ b/tests/test_core/test_compoundgenerator.py @@ -9,7 +9,9 @@ from scanpointgenerator import SpiralGenerator from scanpointgenerator import LissajousGenerator from scanpointgenerator import Excluder -from scanpointgenerator.rois import CircularROI +from scanpointgenerator.rois import CircularROI, RectangularROI +from scanpointgenerator.mutators import FixedDurationMutator, RandomOffsetMutator +from scanpointgenerator.compat import range_ from pkg_resources import require require("mock") @@ -17,34 +19,19 @@ class CompoundGeneratorTest(ScanPointGeneratorTest): - - def setUp(self): - self.x = LineGenerator("x", "mm", 1.0, 1.2, 3, True) - self.y = LineGenerator("y", "mm", 2.0, 2.1, 2, False) - self.z = LineGenerator("z", "mm", 1.0, 2.0, 2, False) - self.g = CompoundGenerator([self.y, self.x], [], []) - def test_init(self): - self.assertEqual(self.g.generators[0], self.y) - self.assertEqual(self.g.generators[1], self.x) - self.assertEqual(self.g.num, 6) - self.assertEqual(self.g.position_units, dict(y="mm", x="mm")) - self.assertEqual(self.g.index_dims, [2, 3]) - self.assertEqual(self.g.index_names, ["y", "x"]) - self.assertEqual(self.g.axes, ["y", "x"]) - - def test_get_point(self): - self.assertEqual(self.g.get_point(0).positions, - dict(x=1.0, y=2.0)) - self.assertEqual(self.g.get_point(5).positions, - dict(x=1.0, y=2.1)) - self.assertEqual(self.g.get_point(3).positions, - dict(x=1.2, y=2.1)) - self.assertRaises(StopIteration, self.g.get_point, 6) + x = LineGenerator("x", "mm", 1.0, 1.2, 3, True) + y = LineGenerator("y", "mm", 2.0, 2.1, 2, False) + g = CompoundGenerator([y, x], [], []) + self.assertEqual(g.generators[0], y) + self.assertEqual(g.generators[1], x) + self.assertEqual(g.position_units, dict(y="mm", x="mm")) + self.assertEqual(g.axes, ["y", "x"]) def test_given_compound_raise_error(self): + g = CompoundGenerator([], [], []) with self.assertRaises(TypeError): - CompoundGenerator([self.g], [], []) + CompoundGenerator([g], [], []) def test_duplicate_name_raises(self): x = LineGenerator("x", "mm", 1.0, 1.2, 3, True) @@ -52,92 +39,145 @@ def test_duplicate_name_raises(self): with self.assertRaises(ValueError): CompoundGenerator([y, x], [], []) - def test_contains_point_true(self): - point = MagicMock() - excluder = MagicMock() - excluder.contains_point.return_value = True - self.g.excluders = [excluder] - - response = self.g.contains_point(point) - - excluder.contains_point.assert_called_once_with(point.positions) - self.assertTrue(response) - - def test_contains_point_false(self): - point = MagicMock() - excluder = MagicMock() - excluder.contains_point.return_value = False - self.g.excluders = [excluder] - - response = self.g.contains_point(point) - - excluder.contains_point.assert_called_once_with(point.positions) - self.assertFalse(response) - - def test_positions(self): - xlower = [0.95, 1.05, 1.15, 1.25, 1.15, 1.05] - xpositions = [1.0, 1.1, 1.2, 1.2, 1.1, 1.0] - xupper = [1.05, 1.15, 1.25, 1.15, 1.05, 0.95] - ypositions = [2.0, 2.0, 2.0, 2.1, 2.1, 2.1] - xindexes = [0, 1, 2, 2, 1, 0] - yindexes = [0, 0, 0, 1, 1, 1] - - for i, p in enumerate(self.g.iterator()): - self.assertEqual(p.upper['x'], xupper[i]) - self.assertEqual(p.lower['x'], xlower[i]) - self.assertEqual(p.positions, dict(x=xpositions[i], y=ypositions[i])) - self.assertEqual(p.indexes, [yindexes[i], xindexes[i]]) - - def test_double_nest(self): - self.g = CompoundGenerator([self.z, self.y, self.x], [], []) - self.assertEqual(self.g.axes, ["z", "y", "x"]) - - xpositions = [1.0, 1.1, 1.2, 1.2, 1.1, 1.0, - 1.0, 1.1, 1.2, 1.2, 1.1, 1.0] - ypositions = [2.0, 2.0, 2.0, 2.1, 2.1, 2.1, - 2.0, 2.0, 2.0, 2.1, 2.1, 2.1] - zpositions = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 2.0, 2.0, 2.0, 2.0, 2.0, 2.0] - xindexes = [0, 1, 2, 2, 1, 0, 0, 1, 2, 2, 1, 0] - yindexes = [0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1] - zindexes = [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1] - - for i, p in enumerate(self.g.iterator()): - self.assertEqual(p.positions, dict( - x=xpositions[i], y=ypositions[i], z=zpositions[i])) - self.assertEqual(p.indexes, [zindexes[i], yindexes[i], xindexes[i]]) - - def test_iterator_with_region(self): - xpositions = [1.0, 1.1, 1.2, 1.1, 1.0] - ypositions = [2.0, 2.0, 2.0, 2.1, 2.1] - indexes = [0, 1, 2, 3, 4] - - circle = CircularROI([1.0, 2.0], 0.2) - excluder = Excluder(circle, ['x', 'y']) - - gen = CompoundGenerator([self.y, self.x], [excluder], []) - - for i, p in enumerate(gen.iterator()): - self.assertEqual(p.positions, dict( - x=xpositions[i], y=ypositions[i])) - self.assertEqual(p.indexes, [indexes[i]]) - self.assertEqual(gen.num, 5) - - def test_mutate_called(self): - mutator = MagicMock() - self.g.mutators = [mutator] - self.g.excluders = [ANY] - filtered = MagicMock() - self.g._filtered_base_iterator = MagicMock() - self.g._filtered_base_iterator.return_value = filtered + def test_iterator(self): + x = LineGenerator("x", "mm", 1.0, 2.0, 5, False) + y = LineGenerator("y", "mm", 1.0, 2.0, 5, False) + g = CompoundGenerator([y, x], [], []) + g.prepare() + points = list(g.iterator()) + expected_pos = [{"x":x/4., "y":y/4.} + for y in range_(4, 9) for x in range_(4, 9)] + self.assertEqual(expected_pos, [p.positions for p in points]) - for _ in self.g.iterator(): - pass - - mutator.mutate.assert_called_once_with(filtered) + def test_get_point(self): + x = LineGenerator("x", "mm", -1., 1, 5, False) + y = LineGenerator("y", "mm", -1., 1, 5, False) + z = LineGenerator("z", "mm", -1., 1, 5, False) + r = CircularROI([0., 0.], 1) + e = Excluder(r, ["x", "y"]) + g = CompoundGenerator([z, y, x], [e], []) + g.prepare() + points = [g.get_point(n) for n in range(0, g.num)] + pos = [p.positions for p in points] + expected = [(x/2., y/2., z/2.) for z in range_(-2, 3) + for y in range_(-2, 3) + for x in range_(-2, 3)] + expected = [{'x':x, 'y':y, 'z':z} for (x, y, z) in expected if x*x + y*y <= 1] + self.assertEqual(expected, pos) + + def test_get_point_large_scan(self): + s = SpiralGenerator(["x", "y"], "mm", [0, 0], 6, 1) #114 points + z = LineGenerator("z", "mm", 0, 1, 100) + w = LineGenerator("w", "mm", 0, 1, 5) + t = LineGenerator("t", "mm", 0, 1, 5) + rad1 = 2.8 + r1 = CircularROI([1., 1.], rad1) + e1 = Excluder(r1, ["x", "y"]) + rad2 = 2 + r2 = CircularROI([0.5, 0.5], rad2) + e2 = Excluder(r2, ["y", "z"]) + rad3 = 0.5 + r3 = CircularROI([0.5, 0.5], rad3) + e3 = Excluder(r3, ["w", "t"]) + g = CompoundGenerator([t, w, z, s], [e1, e2, e3], []) + g.prepare() + + spiral = [(x, y) for (x, y) in zip(s.points["x"], s.points["y"])] + zwt = [(z/99., w/4., t/4.) for t in range_(0, 5) + for w in range_(0, 5) + for z in range_(0, 100)] + expected = [(x, y, z, w, t) for (z, w, t) in zwt for (x, y) in spiral] + expected = [{"x":x, "y":y, "z":z, "w":w, "t":t} + for (x,y,z,w,t) in expected if + (x-1)*(x-1) + (y-1)*(y-1) <= rad1*rad1 and + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5) <= rad2*rad2 and + (w-0.5)*(w-0.5) + (t-0.5)*(t-0.5) <= rad3*rad3] + points = [g.get_point(n) for n in range_(0, g.num)] + pos = [p.positions for p in points] + # assertEqual on a sequence of dicts is *really* slow + for (e, p) in zip(expected, pos): + self.assertEquals(e.keys(), p.keys()) + for k in e.keys(): + self.assertAlmostEqual(e[k], p[k]) + + def test_alternating_simple(self): + y = LineGenerator("y", "mm", 1, 5, 5) + x = LineGenerator("x", "mm", 1, 5, 5, alternate_direction=True) + g = CompoundGenerator([y, x], [], []) + g.prepare() + expected = [] + for y in range(1, 6): + r = range(1, 6) if y % 2 == 1 else range(5, 0, -1) + for x in r: + expected.append({"y":float(y), "x":float(x)}) + points = [p.positions for p in list(g.iterator())] + self.assertEqual(expected, points) + + def test_alternating_regions(self): + y = LineGenerator("y", "mm", 1, 5, 5) + x = LineGenerator("x", "mm", 1, 5, 5, alternate_direction=True) + r1 = RectangularROI([2, 2], 2, 2) + e1 = Excluder(r1, ["y", "x"]) + g = CompoundGenerator([y, x], [e1], []) + g.prepare() + expected = [] + for y in range(1, 6): + r = range(1, 6) if y % 2 == 1 else range(5, 0, -1) + for x in r: + expected.append({"y":float(y), "x":float(x)}) + expected = [p for p in expected if + (p["y"] >= 2 and p["y"] < 4 and p["x"] >= 2 and p["x"] < 4)] + points = [p.positions for p in list(g.iterator())] + self.assertEqual(expected, points) + + def test_alternating_regions_2(self): + z = LineGenerator("z", "mm", 1, 5, 5) + y = LineGenerator("y", "mm", 1, 5, 5, alternate_direction=True) + x = LineGenerator("x", "mm", 1, 5, 5) + r1 = RectangularROI([2, 2], 2, 2) + e1 = Excluder(r1, ["x", "y"]) + e2 = Excluder(r1, ["z", "y"]) + g = CompoundGenerator([z, y, x], [e1, e2], []) #20 points + g.prepare() + actual = [p.positions for p in list(g.iterator())] + expected = [{"x":float(x), "y":float(y), "z":float(z)} + for z in range(1, 6) + for y in (range(1, 6) if z % 2 == 1 else range(5, 0, -1)) + for x in range(1, 6)] + expected = [p for p in expected + if p["x"] >= 2 and p["x"] < 4 and p["y"] >= 2 and p["y"] < 4 + and p["z"] >= 2 and p["z"] < 4] + self.assertEqual(expected, actual) + + def test_alternating_complex(self): + tg = LineGenerator("t", "mm", 1, 5, 5) + zg = LineGenerator("z", "mm", 1, 5, 5, alternate_direction=True) + yg = LineGenerator("y", "mm", 1, 5, 5, alternate_direction=True) + xg = LineGenerator("x", "mm", 1, 5, 5) + r1 = RectangularROI([3., 3.], 2., 2.) + e1 = Excluder(r1, ["y", "x"]) + e2 = Excluder(r1, ["z", "y"]) + g = CompoundGenerator([tg, zg, yg, xg], [e1, e2], []) + g.debug = True + g.prepare() + points = [p.positions for p in list(g.iterator())] + expected = [] + for t in range(1, 6): + r_1 = range(1,6) if t % 2 == 1 else range(5, 0, -1) + for z in r_1: + r_2 = range(1,6) if z % 2 == 1 else range(5, 0, -1) + for y in r_2: + for x in range(1, 6): + expected.append( + {"t":float(t), "z":float(z), + "y":float(y), "x":float(x)}) + expected = [p for p in expected if + (p["y"] >= 3 and p["y"] < 5 and p["x"] >= 3 and p["x"] < 5) and + (p["z"] >= 3 and p["z"] < 5 and p["y"] >= 3 and p["y"] < 5)] + self.assertEqual(expected, points) def test_line_spiral(self): - positions = [{'y': -0.3211855677650875, 'x': 0.23663214944574582, 'z': 0.0}, + expected = [{'y': -0.3211855677650875, 'x': 0.23663214944574582, 'z': 0.0}, {'y': -0.25037538922751695, 'x': -0.6440318266552169, 'z': 0.0}, {'y': 0.6946549630820702, 'x': -0.5596688286164636, 'z': 0.0}, {'y': 0.6946549630820702, 'x': -0.5596688286164636, 'z': 2.0}, @@ -146,19 +186,19 @@ def test_line_spiral(self): {'y': -0.3211855677650875, 'x': 0.23663214944574582, 'z': 4.0}, {'y': -0.25037538922751695, 'x': -0.6440318266552169, 'z': 4.0}, {'y': 0.6946549630820702, 'x': -0.5596688286164636, 'z': 4.0}, - {}] - + ] z = LineGenerator("z", "mm", 0.0, 4.0, 3) spiral = SpiralGenerator(['x', 'y'], "mm", [0.0, 0.0], 0.8, alternate_direction=True) - gen = CompoundGenerator([z, spiral], [], []) - - self.assertEqual(gen.axes, ["z", "x", "y"]) - - for i, p in enumerate(gen.iterator()): - self.assertEqual(p.positions, positions[i]) + g = CompoundGenerator([z, spiral], [], []) + g.prepare() + self.assertEqual(g.axes, ["z", "x", "y"]) + points = list(g.iterator()) + self.assertEqual(len(expected), len(points)) + for i, p in enumerate(points): + self.assertEqual(expected[i], p.positions) def test_line_lissajous(self): - positions = [{'y': 0.0, 'x': 0.5, 'z': 0.0}, + expected = [{'y': 0.0, 'x': 0.5, 'z': 0.0}, {'y': 0.2938926261462366, 'x': 0.15450849718747375, 'z': 0.0}, {'y': -0.4755282581475768, 'x': -0.40450849718747367, 'z': 0.0}, {'y': 0.47552825814757677, 'x': -0.4045084971874738, 'z': 0.0}, @@ -176,16 +216,292 @@ def test_line_lissajous(self): z = LineGenerator("z", "mm", 0.0, 4.0, 3) box = dict(centre=[0.0, 0.0], width=1.0, height=1.0) - lissajous = LissajousGenerator(['x', 'y'], "mm", box, 1, num_points=5) - gen = CompoundGenerator([z, lissajous], [], []) - - self.assertEqual(gen.axes, ["z", "x", "y"]) - - for i, p in enumerate(gen.iterator()): - self.assertEqual(p.positions, positions[i]) - + liss = LissajousGenerator(['x', 'y'], "mm", box, 1, num_points=5) + g = CompoundGenerator([z, liss], [], []) + g.prepare() + self.assertEqual(g.axes, ["z", "x", "y"]) + points = list(g.iterator()) + self.assertEqual(len(expected), len(points)) + for i, p in enumerate(points): + self.assertEqual(expected[i], p.positions) + + def test_horrible_scan(self): + lissajous = LissajousGenerator(["j1", "j2"], "mm", {"centre":[-0.5, 0.7], "width":2, "height":3.5}, 7, 100) + line2 = LineGenerator(["l2"], "mm", -3, 3, 7, alternate_direction = True) + line1 = LineGenerator(["l1"], "mm", -1, 2, 5, alternate_direction = True) + spiral = SpiralGenerator(["s1", "s2"], "mm", [1, 2], 5, 2.5, alternate_direction = True) + r1 = CircularROI([1, 1], 2) + r2 = CircularROI([-1, -1], 4) + r3 = CircularROI([1, 1], 1) + e1 = Excluder(r1, ["j1", "l2"]) + e2 = Excluder(r2, ["s2", "l1"]) + e3 = Excluder(r3, ["s1", "s2"]) + g = CompoundGenerator([lissajous, line2, line1, spiral], [e1, e2, e3], []) + g.prepare() + idx = range_(0, lissajous.num) + p_liss = [(j1, j2, i) for (j1, j2, i) in zip(lissajous.points["j1"], lissajous.points["j2"], idx)] + idx = range_(0, line2.num) + p_l2liss = [(l2, j1, j2, i2) for (j1, j2, i) in p_liss for (l2, i2) in + (zip(line2.points["l2"], idx) if i % 2 == 0 else zip(line2.points["l2"][::-1], idx))] + idx = range_(0, line1.num) + p_l1l2liss = [(l1, l2, j1, j2, i2) for (l2, j1, j2, i) in p_l2liss for (l1, i2) in + (zip(line1.points["l1"], idx) if i % 2 == 0 else + zip(line1.points["l1"][::-1], idx))] + points = [(s1, s2, l1, l2, j1, j2) for (l1, l2, j1, j2, i) in p_l1l2liss for (s1, s2) in + (zip(spiral.points["s1"], spiral.points["s2"]) if i % 2 == 0 else + zip(spiral.points["s1"][::-1], spiral.points["s2"][::-1]))] + + self.assertEqual(lissajous.num * line2.num * line1.num * spiral.num, len(points)) + points = [(s1, s2, l1, l2, j1, j2) for (s1, s2, l1, l2, j1, j2) in points if + (j1-1)**2 + (l2-1)**2 <= 4 and + (s2+1)**2 + (l1+1)**2 <= 16 and + (s1-1)**2 + (s2-1)**2 <= 1] + self.assertEqual(len(points), g.num) + generated_points = list(g.iterator()) + self.assertEqual(len(points), len(generated_points)) + + actual = [p.positions for p in generated_points] + expected = [{"j1":j1, "j2":j2, "l2":l2, "l1":l1, "s1":s1, "s2":s2} + for (s1, s2, l1, l2, j1, j2) in points] + self.assertEqual(expected, actual) + + def test_double_spiral_scan(self): + line1 = LineGenerator(["l1"], "mm", -1, 2, 5) + spiral_s = SpiralGenerator(["s1", "s2"], "mm", [1, 2], 5, 2.5, alternate_direction = True) + spiral_t = SpiralGenerator(["t1", "t2"], "mm", [0, 0], 5, 2.5, alternate_direction = True) + line2 = LineGenerator(["l2"], "mm", -1, 2, 5, alternate_direction = True) + r = CircularROI([0, 0], 1) + e1 = Excluder(r, ["s1", "l1"]) + e2 = Excluder(r, ["l2", "t1"]) + g = CompoundGenerator([line1, spiral_s, spiral_t, line2], [e1, e2], []) + g.prepare() + idx = range_(0, line1.num) + p_l1 = [(l1, i) for (l1, i) in zip(line1.points["l1"], idx)] + idx = range_(spiral_s.num) + p_ss_l1 = [(s1, s2, l1, i2) for (l1, i) in p_l1 for (s1, s2, i2) in + (zip(spiral_s.points["s1"], spiral_s.points["s2"], idx) if i % 2 == 0 else + zip(spiral_s.points["s1"][::-1], spiral_s.points["s2"][::-1], idx))] + idx = range_(spiral_t.num) + p_st_ss_l1 = [(t1, t2, s1, s2, l1, i2) for (s1, s2, l1, i) in p_ss_l1 for (t1, t2, i2) in + (zip(spiral_t.points["t1"], spiral_t.points["t2"], idx) if i % 2 == 0 else + zip(spiral_t.points["t1"][::-1], spiral_t.points["t2"][::-1], idx))] + points = [(l2, t1, t2, s1, s2, l1) for (t1, t2, s1, s2, l1, i) in p_st_ss_l1 for l2 in + (line2.points["l2"] if i % 2 == 0 else line2.points["l2"][::-1])] + + expected = [{"l2":l2, "t1":t1, "t2":t2, "s1":s1, "s2":s2, "l1":l1} + for (l2, t1, t2, s1, s2, l1) in points if + s1*s1 + l1*l1 <= 1 and l2*l2 + t1*t1 <= 1] + + actual = [p.positions for p in list(g.iterator())] + self.assertEqual(expected, actual) + + def test_mutators(self): + mutator_1 = FixedDurationMutator(0.2) + mutator_2 = RandomOffsetMutator(0, ["x"], {"x":1}) + mutator_2.get_random_number = MagicMock(return_value=0.1) + x = LineGenerator('x', 'mm', 1, 5, 5) + g = CompoundGenerator([x], [], [mutator_1, mutator_2]) + g.prepare() + x_pos = 1 + for p in g.iterator(): + self.assertEqual(0.2, p.duration) + self.assertEqual({"x":x_pos + 0.1}, p.positions) + x_pos += 1 + self.assertEqual(6, x_pos) + +class CompoundGeneratorInternalDataTests(ScanPointGeneratorTest): + """Tests on datastructures internal to CompoundGenerator""" + + def test_post_prepare(self): + x = LineGenerator("x", "mm", 1.0, 1.2, 3, True) + y = LineGenerator("y", "mm", 2.0, 2.1, 2, False) + g = CompoundGenerator([y, x], [], []) + g.prepare() + self.assertListAlmostEqual([1.0, 1.1, 1.2], g.axes_points["x"].tolist()) + self.assertListAlmostEqual([0.95, 1.05, 1.15], g.axes_points_lower["x"].tolist()) + self.assertListAlmostEqual([1.05, 1.15, 1.25], g.axes_points_upper["x"].tolist()) + self.assertListAlmostEqual([2.0, 2.1], g.axes_points["y"].tolist()) + self.assertListAlmostEqual([1.95, 2.05], g.axes_points_lower["y"].tolist()) + self.assertListAlmostEqual([2.05, 2.15], g.axes_points_upper["y"].tolist()) + self.assertEqual(g.num, 6) + self.assertEqual(g.index_dims, [2, 3]) + + self.assertEqual(2, len(g.indexes)) + self.assertEqual(["y"], g.indexes[0]["axes"]) + self.assertEqual([True] * 2, g.indexes[0]["mask"].tolist()) + self.assertEqual(["x"], g.indexes[1]["axes"]) + self.assertEqual([True] * 3, g.indexes[1]["mask"].tolist()) + + def test_prepare_with_regions(self): + x = LineGenerator("x", "mm", 0, 1, 5, False) + y = LineGenerator("y", "mm", 0, 1, 5, False) + circle = CircularROI([0., 0.], 1) + excluder = Excluder(circle, ['x', 'y']) + g = CompoundGenerator([y, x], [excluder], []) + g.prepare() + self.assertEqual(1, len(g.indexes)) + self.assertEqual(["y", "x"], g.indexes[0]["axes"]) + expected_mask = [(x/4.)**2 + (y/4.)**2 <= 1 + for y in range(0, 5) for x in range(0, 5)] + self.assertEqual(expected_mask, g.indexes[0]["mask"].tolist()) + self.assertIsNone(g.indexes[0]["rmask"]) + + def test_simple_mask(self): + x = LineGenerator("x", "mm", -1.0, 1.0, 5, False) + y = LineGenerator("y", "mm", -1.0, 1.0, 5, False) + r = CircularROI([0, 0], 1) + e = Excluder(r, ["x", "y"]) + g = CompoundGenerator([y, x], [e], []) + g.prepare() + p = [(x/2., y/2.) for y in range_(-2, 3) for x in range_(-2, 3)] + expected_mask = [x*x + y*y <= 1 for (x, y) in p] + self.assertEqual(expected_mask, g.indexes[0]["mask"].tolist()) + + def test_simple_mask_alternating(self): + x = LineGenerator("x", "mm", -1.0, 1.0, 5, alternate_direction=True) + y = LineGenerator("y", "mm", -1.0, 1.0, 5, alternate_direction=False) + r = CircularROI([0.5, 0], 1) + e = Excluder(r, ["x", "y"]) + g = CompoundGenerator([y, x], [e], []) + g.prepare() + reverse = False + p = [] + for y in range_(-2, 3): + if reverse: + p += [(x/2., y/2.) for x in range_(2, -3, -1)] + else: + p += [(x/2., y/2.) for x in range_(-2, 3)] + reverse = not reverse + expected_mask = [(x-0.5)**2 + y**2 <= 1**2 for (x, y) in p] + self.assertEqual(expected_mask, g.indexes[0]["mask"].tolist()) + + def test_double_mask_alternating_spiral(self): + zgen = LineGenerator("z", "mm", 0.0, 4.0, 5) + spiral = SpiralGenerator(['x', 'y'], "mm", [0.0, 0.0], 3, alternate_direction=True) #29 points + r1 = RectangularROI([-2, -2], 4, 3) + r2 = RectangularROI([-2, 0], 4, 3) + e1 = Excluder(r1, ["y", "x"]) + e2 = Excluder(r2, ["y", "z"]) + g = CompoundGenerator([zgen, spiral], [e1, e2], []) + g.prepare() + xy = list(zip(g.axes_points['x'], g.axes_points['y'])) + p = [] + for z in range_(0, 5): + p += [(x, y, z) for (x, y) in (xy if z % 2 == 0 else xy[::-1])] + expected = [x >= -2 and x < 1 and y >= -2 and y < 2 + and z >= 0 and z < 3 for (x, y, z) in p] + actual = g.indexes[0]["mask"].tolist() + self.assertEqual(expected, actual) + + def test_double_mask_spiral(self): + zgen = LineGenerator("z", "mm", 0.0, 4.0, 5) + spiral = SpiralGenerator(['x', 'y'], "mm", [0.0, 0.0], 3) #29 points + r1 = RectangularROI([-2, -2], 4, 3) + r2 = RectangularROI([-2, 0], 4, 3) + e1 = Excluder(r1, ["y", "x"]) + e2 = Excluder(r2, ["y", "z"]) + g = CompoundGenerator([zgen, spiral], [e1, e2], []) + g.prepare() + p = list(zip(g.axes_points['x'], g.axes_points['y'])) + p = [(x, y, z) for z in range_(0, 5) for (x, y) in p] + expected = [x >= -2 and x < 1 and y >= -2 and y < 2 + and z >= 0 and z < 3 for (x, y, z) in p] + actual = g.indexes[0]["mask"].tolist() + self.assertEqual(expected, actual) + + def test_simple_mask_alternating_spiral(self): + z = LineGenerator("z", "mm", 0.0, 4.0, 5) + spiral = SpiralGenerator(['x', 'y'], "mm", [0.0, 0.0], 3, alternate_direction=True) #29 points + r = RectangularROI([-2, -2], 3, 4) + e = Excluder(r, ["x", "y"]) + g = CompoundGenerator([z, spiral], [e], []) + g.prepare() + p = list(zip(g.axes_points['x'], g.axes_points['y'])) + expected = [x >= -2 and x < 1 and y >= -2 and y < 2 for (x, y) in p] + expected_r = [x >= -2 and x < 1 and y >= -2 and y < 2 for (x, y) in p[::-1]] + actual = g.indexes[1]["mask"].tolist() + actual_r = g.indexes[1]["rmask"].tolist() + self.assertEqual(expected, actual) + self.assertEqual(expected_r, actual_r) + + def test_double_mask(self): + x = LineGenerator("x", "mm", -1.0, 1.0, 5, False) + y = LineGenerator("y", "mm", -1.0, 1.0, 5, False) + z = LineGenerator("z", "mm", -1.0, 1.0, 5, False) + r = CircularROI([0.1, 0.2], 1) + e1 = Excluder(r, ["x", "y"]) + e2 = Excluder(r, ["y", "z"]) + g = CompoundGenerator([z, y, x], [e1, e2], []) + g.prepare() + p = [(x/2., y/2., z/2.) for z in range_(-2, 3) + for y in range_(-2, 3) + for x in range_(-2, 3)] + m1 = [(x-0.1)**2 + (y-0.2)**2 <= 1 for (x, y, z) in p] + m2 = [(y-0.1)**2 + (z-0.2)**2 <= 1 for (x, y, z) in p] + expected_mask = [(b1 and b2) for (b1, b2) in zip(m1, m2)] + self.assertEqual(expected_mask, g.indexes[0]["mask"].tolist()) + + def test_complex_masks(self): + tg = LineGenerator("t", "mm", 1, 5, 5) + zg = LineGenerator("z", "mm", 1, 5, 5, alternate_direction=True) + yg = LineGenerator("y", "mm", 1, 5, 5, alternate_direction=True) + xg = LineGenerator("x", "mm", 1, 5, 5) + r1 = RectangularROI([3., 3.], 2., 2.) + e1 = Excluder(r1, ["y", "x"]) + e2 = Excluder(r1, ["z", "y"]) + g = CompoundGenerator([tg, zg, yg, xg], [e1, e2], []) + g.prepare() + + t_mask = [True] * 5 + zp = range(1, 6) + yzp = [(y, z) for z in zp for y in (range(1, 6) if z % 2 == 1 else range(5, 0, -1))] + xyzp = [(x, y, z) for (y, z) in yzp for x in range(1, 6)] + xyz_mask = [x >= 3 and x < 5 and y >= 3 and y < 5 and z >= 3 and z < 5 + for (x, y, z) in xyzp] + + r_zp = range(5, 0, -1) + r_yzp = [(y, z) for z in r_zp for y in (range(1, 6) if z % 2 == 1 else range(5, 0, -1))] + r_xyzp = [(x, y, z) for (y, z) in r_yzp for x in range(1, 6)] + r_xyz_mask = [x >= 3 and x < 5 and y >= 3 and y < 5 and z >= 3 and z < 5 + for (x, y, z) in r_xyzp] + self.assertEqual(t_mask, g.indexes[0]["mask"].tolist()) + self.assertIsNone(g.indexes[0]["rmask"]) + self.assertEqual(xyz_mask, g.indexes[1]["mask"].tolist()) + self.assertEqual(r_xyz_mask, g.indexes[1]["rmask"].tolist()) + + def test_separate_indexes(self): + x1 = LineGenerator("x1", "mm", -1.0, 1.0, 5, False) + y1 = LineGenerator("y1", "mm", -1.0, 1.0, 5, False) + z1 = LineGenerator("z1", "mm", -1.0, 1.0, 5, False) + x2 = LineGenerator("x2", "mm", -1.0, 1.0, 5, False) + y2 = LineGenerator("y2", "mm", -1.0, 1.0, 5, False) + x3 = LineGenerator("x3", "mm", 0, 1.0, 5, False) + y3 = LineGenerator("y3", "mm", 0, 1.0, 5, False) + r = CircularROI([0, 0], 1) + e1 = Excluder(r, ["x1", "y1"]) + e2 = Excluder(r, ["y1", "z1"]) + e3 = Excluder(r, ["x1", "y1"]) + e4 = Excluder(r, ["x2", "y2"]) + e5 = Excluder(r, ["x3", "y3"]) + g = CompoundGenerator( + [x3, y3, y2, x2, z1, y1, x1], + [e1, e2, e3, e4, e5], + []) + g.prepare() + p = [(x/2., y/2., z/2.) for z in range_(-2, 3) + for y in range_(-2, 3) + for x in range_(-2, 3)] + m1 = [x*x + y*y <= 1 for (x, y, z) in p] + m2 = [y*y + z*z <= 1 for (x, y, z) in p] + expected_mask = [(b1 and b2) for (b1, b2) in zip(m1, m2)] + self.assertEqual(expected_mask, g.indexes[2]["mask"].tolist()) + p = [(x/2., y/2.) for y in range_(-2, 3) for x in range_(-2, 3)] + expected_mask = [x*x + y*y <= 1 for (x, y) in p] + self.assertEqual(expected_mask, g.indexes[1]["mask"].tolist()) + p = [(x/4., y/4.) for y in range_(0, 5) for x in range_(0, 5)] + expected_mask = [x*x + y*y <= 1 for (x, y) in p] + self.assertEqual(expected_mask, g.indexes[0]["mask"].tolist()) -@patch('scanpointgenerator.compoundgenerator.CompoundGenerator._base_iterator') class TestSerialisation(unittest.TestCase): def setUp(self): @@ -203,7 +519,7 @@ def setUp(self): self.e1 = MagicMock() self.e1_dict = MagicMock() - def test_to_dict(self, _): + def test_to_dict(self): self.g = CompoundGenerator([self.l2, self.l1], [self.e1], [self.m1]) self.l1.to_dict.return_value = self.l1_dict @@ -228,7 +544,7 @@ def test_to_dict(self, _): @patch('scanpointgenerator.compoundgenerator.Mutator') @patch('scanpointgenerator.compoundgenerator.Excluder') @patch('scanpointgenerator.compoundgenerator.Generator') - def test_from_dict(self, gen_mock, ex_mock, mutator_mock, _): + def test_from_dict(self, gen_mock, ex_mock, mutator_mock): self.g = CompoundGenerator([self.l2, self.l1], [self.e1], [self.m1]) gen_mock.from_dict.side_effect = [self.l2, self.l1] @@ -252,4 +568,4 @@ def test_from_dict(self, gen_mock, ex_mock, mutator_mock, _): self.assertEqual(gen.excluders[0], self.e1) if __name__ == "__main__": - unittest.main() + unittest.main(verbosity=2) From 87399831635f73528109d7dc163e3a71d4ee84bd Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Wed, 9 Nov 2016 11:11:39 +0000 Subject: [PATCH 09/47] Fix up other tests after Generator changes --- tests/test_generators/test_lissajousgenerator.py | 16 ++++++++++------ tests/test_generators/test_spiralgenerator.py | 6 +++--- tests/test_mutators/test_randomoffsetmutator.py | 2 ++ tests/test_util.py | 6 +++--- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/tests/test_generators/test_lissajousgenerator.py b/tests/test_generators/test_lissajousgenerator.py index 0c38898..38e32f1 100644 --- a/tests/test_generators/test_lissajousgenerator.py +++ b/tests/test_generators/test_lissajousgenerator.py @@ -57,12 +57,16 @@ def test_iterator(self): {'y': -0.29389262614623674, 'x': 0.47552825814757677}] indexes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - for i, p in enumerate(g.iterator()): - self.assertEqual(p.positions, positions[i]) - self.assertEqual(p.lower, lower[i]) - self.assertEqual(p.upper, upper[i]) - self.assertEqual(p.indexes, [indexes[i]]) - self.assertEqual(i, 9) + points = list(g.iterator()) + self.assertEqual(10, len(points)) + result = [{'x':p.positions['x'], 'y':p.positions['y']} for p in points] + self.assertEquals(result, positions) + result = [{'x':p.upper['x'], 'y':p.upper['y']} for p in points] + self.assertEquals(result, upper) + result = [{'x':p.lower['x'], 'y':p.lower['y']} for p in points] + self.assertEquals(result, lower) + result = [p.indexes[0] for p in points] + self.assertEquals(result, indexes) def test_array_positions(self): g = LissajousGenerator(['x', 'y'], "mm", self.bounding_box, 1, num_points=10) diff --git a/tests/test_generators/test_spiralgenerator.py b/tests/test_generators/test_spiralgenerator.py index fa36a91..24ee8c2 100644 --- a/tests/test_generators/test_spiralgenerator.py +++ b/tests/test_generators/test_spiralgenerator.py @@ -77,9 +77,9 @@ def test_iterator(self): indexes = [0, 1, 2, 3, 4, 5, 6] for i, p in enumerate(self.g.iterator()): - self.assertEqual(p.positions, positions[i]) - self.assertEqual(p.lower, lower[i]) - self.assertEqual(p.upper, upper[i]) + self.assertEqual(positions[i], p.positions) + self.assertEqual(lower[i], p.lower) + self.assertEqual(upper[i], p.upper) self.assertEqual(p.indexes, [indexes[i]]) self.assertEqual(i, 6) diff --git a/tests/test_mutators/test_randomoffsetmutator.py b/tests/test_mutators/test_randomoffsetmutator.py index 9f5cffa..949f787 100644 --- a/tests/test_mutators/test_randomoffsetmutator.py +++ b/tests/test_mutators/test_randomoffsetmutator.py @@ -89,6 +89,7 @@ def test_order_of_offsets(self): mutator = RandomOffsetMutator(1, ["y", "x"], dict(x=0.25, y=0.25)) gen = CompoundGenerator([line1, line2], [], [mutator]) + gen.prepare() p = next(gen.iterator()) self.assertAlmostEqual(p.positions['x'], 1.0791957060000001, places=10) self.assertAlmostEqual(p.positions['y'], 2.12147549275, places=10) @@ -96,6 +97,7 @@ def test_order_of_offsets(self): # Swap order of axes in mutator; should swap offsets applied to x and y mutator = RandomOffsetMutator(1, ["x", "y"], dict(x=0.25, y=0.25)) gen = CompoundGenerator([line1, line2], [], [mutator]) + gen.prepare() p = next(gen.iterator()) self.assertAlmostEqual(p.positions['x'], 1.12147549275, places=10) self.assertAlmostEqual(p.positions['y'], 2.0791957060000001, places=10) diff --git a/tests/test_util.py b/tests/test_util.py index f3458f9..4890e54 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -7,14 +7,14 @@ class ScanPointGeneratorTest(unittest.TestCase): - def assertListAlmostEqual(self, actual, expected): + def assertListAlmostEqual(self, actual, expected, delta=1e-15): self.assertEqual(len(actual), len(expected)) for a, e in zip(actual, expected): if type(a) in (list, tuple): self.assertEqual(type(a), type(e)) - self.assertListAlmostEqual(a, e) + self.assertListAlmostEqual(a, e, delta) else: - self.assertAlmostEqual(a, e, delta=0.000001) + self.assertAlmostEqual(a, e) def assertIteratorProduces(self, iterator, all_expected): for expected in all_expected: From ae15b8db30d726990b3ba2ee1885e093b0b933bf Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Wed, 9 Nov 2016 11:13:27 +0000 Subject: [PATCH 10/47] Add time-sensitive test for compound generator for a large scan. Tests that point preparation for ~100 million points (before region filtering) happens within a few seconds. --- .../test_compoundgenerator_performance.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/test_core/test_compoundgenerator_performance.py diff --git a/tests/test_core/test_compoundgenerator_performance.py b/tests/test_core/test_compoundgenerator_performance.py new file mode 100644 index 0000000..c25c04b --- /dev/null +++ b/tests/test_core/test_compoundgenerator_performance.py @@ -0,0 +1,50 @@ +import os +import sys +sys.path.append(os.path.join(os.path.dirname(__file__), "..")) +import unittest +import time + +from test_util import ScanPointGeneratorTest +from scanpointgenerator import CompoundGenerator +from scanpointgenerator import LineGenerator +from scanpointgenerator import SpiralGenerator +from scanpointgenerator import Excluder +from scanpointgenerator.rois import CircularROI +from scanpointgenerator.mutators import FixedDurationMutator, RandomOffsetMutator + +class CompoundGeneratorPerformanceTest(ScanPointGeneratorTest): + def test_200_million_time_constraint(self): + start_time = time.time() + + s = SpiralGenerator( + ["x", "y"], "mm", [0, 0], 6, 0.02, + alternate_direction=True) # ~2e5 points + z = LineGenerator("z", "mm", 0, 1, 100) #1e2 points + w = LineGenerator("w", "mm", 0, 1, 10) #1e1 points + r1 = CircularROI([-0.7, 4], 0.5) + r2 = CircularROI([0.5, 0.5], 0.3) + r3 = CircularROI([0.2, 4], 0.5) + e1 = Excluder(r1, ["x", "y"]) + e2 = Excluder(r2, ["w", "z"]) + e3 = Excluder(r3, ["z", "y"]) + fm = FixedDurationMutator(0.1) + om = RandomOffsetMutator(0, ["x", "y"], {"x":0.2, "y":0.2}) + g = CompoundGenerator([w, z, s], [e1, e3, e2], [fm, om]) + g.prepare() # g.num ~3e5 + + end_time = time.time() + #self.assertLess(end_time - start_time, 5) + # TravisCI VMs are sometimes pretty weak + # if this test becomes problematic then we'll just have to remove it + self.assertLess(end_time - start_time, 12) + + # we dont care about this right now + #start_time = time.time() + #for p in g.iterator(): + # pass + #end_time = time.time() + ## point objects are quite expensive to create + #self.assertLess(end_time - start_time, 20) + +if __name__ == "__main__": + unittest.main(verbosity=2) From 3c69be552c1d2f5c24a72c47dce9756ee2b98040 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Fri, 11 Nov 2016 17:55:24 +0000 Subject: [PATCH 11/47] Change the way CompoundGenerator handles alternating generators Nested generators should always alternate back and forth (from their perspective). Adds the constraint that any set of flattened axes must all share a common alternate_direction setting to allows reversal of a full dimension. Adds altenerate_direction to LissajousGenerator. --- scanpointgenerator/core/compoundgenerator.py | 92 ++---- .../generators/lissajousgenerator.py | 4 +- tests/test_core/test_compoundgenerator.py | 298 +++++++++++++----- .../test_compoundgenerator_performance.py | 7 +- 4 files changed, 266 insertions(+), 135 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index aaea6f9..0bad651 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -62,7 +62,8 @@ def prepare(self): "generators":[generator], "masks":[], "tile":1, - "repeat":1} + "repeat":1, + "alternate":generator.alternate_direction} self.indexes.append(idx) for excluder in self.excluders: @@ -74,8 +75,8 @@ def prepare(self): - self.generators.index(gen_2) if gen_diff < -1 or gen_diff > 1: raise ValueError( - "Excluders must be defined on axes that are adjacent in \ - generator order") + "Excluders must be defined on axes that are adjacent in " \ + "generator order") if gen_diff == 1: gen_1, gen_2 = gen_2, gen_1 axis_1, axis_2 = axis_2, axis_1 @@ -88,11 +89,15 @@ def prepare(self): idx_1 = [i for i in self.indexes if axis_1 in i["axes"]][0] idx_2 = [i for i in self.indexes if axis_2 in i["axes"]][0] idx_diff = self.indexes.index(idx_1) - self.indexes.index(idx_2) + if idx_1["alternate"] != idx_2["alternate"]: + raise ValueError( + "Generators tied by regions must have the same " \ + "alternate_direction setting") # merge "inner" into "outer" if idx_diff < -1 or idx_diff > 1: raise ValueError( - "Excluders must be defined on axes that are adjacent in \ - generator order") + "Excluders must be defined on axes that are adjacent in " \ + "generator order") if idx_diff == 1: idx_1, idx_2 = idx_2, idx_1 idx_diff = -1 @@ -126,47 +131,30 @@ def prepare(self): ##### # if gen_1 and gen_2 are different then the outer axis will have to # have its elements repeated and the inner axis will have to have - # itself repeated - need to determine which is the inner axis + # itself repeated - gen_1 is always inner axis points_1 = self.axes_points[axis_1] points_2 = self.axes_points[axis_2] doubled_mask = False # used for some cases of alternating generators - if gen_1 is gen_2 and gen_1.alternate_direction: + if gen_1 is gen_2 and idx["alternate"]: # run *both* axes backwards # but our mask will be a factor of 2 too big doubled_mask = True points_1 = np.append(points_1, points_1[::-1]) points_2 = np.append(points_2, points_2[::-1]) - elif gen_1.alternate_direction and gen_2.alternate_direction: + elif idx["alternate"]: doubled_mask = True points_1 = np.append(points_1, points_1[::-1]) points_2 = np.append(points_2, points_2[::-1]) - #tile = gen_1.num - #points_2 = np.tile(points_2, tile) - tile = gen_1.num / 2. - tmp = np.tile(points_2, int(tile)) - if tile % 1 != 0: - points_2 = np.append(tmp, points_2[:gen_2.num]) - points_2 = np.tile(points_2, 2) - points_1 = np.repeat(points_1, gen_2.num) - elif gen_2.alternate_direction: - points_1 = np.repeat(points_1, gen_2.num) - points_2 = np.append(points_2, points_2[::-1]) - tile = gen_1.num / 2. - points_2 = np.tile(points_2, int(tile)) - if tile % 1 != 0: - points_2 = np.append( - points_2, self.axes_points[axis_2]) - elif gen_1.alternate_direction: - doubled_mask = True - points_1 = np.append(points_1, points_1[::-1]) + points_2 = np.tile(points_2, gen_1.num) points_1 = np.repeat(points_1, gen_2.num) - points_2 = np.tile(points_2, gen_1.num * 2) elif gen_1 is not gen_2: points_1 = np.repeat(points_1, gen_2.num) points_2 = np.tile(points_2, gen_1.num) + # else not needed; do nothing if gen_1 is gen_2 and not alternating + if axis_1 == excluder.scannables[0]: mask = excluder.create_mask(points_1, points_2) @@ -200,36 +188,19 @@ def prepare(self): # Generate full index mask and "apply" ##### for idx in self.indexes: - # if first generator alternates then we want a reverse mask - reverse = idx["generators"][0].alternate_direction - rmask = np.full(idx["size"], True, dtype=np.bool) if reverse else None mask = np.full(idx["size"], True, dtype=np.bool) for m in idx["masks"]: assert len(m["mask"]) * m["repeat"] * m["tile"] == len(mask), \ "Mask lengths are not consistent" expanded = np.repeat(m["mask"], m["repeat"]) - if m["tile"] < 1: - # the second half of this mask forms part of the "reverse" - if reverse: - rmask &= expanded[len(expanded)//2:] - expanded = expanded[:len(expanded)//2] - elif m["tile"] % 1 != 0: + if m["tile"] % 1 != 0: ex = np.tile(expanded, int(m["tile"])) expanded = np.append(ex, expanded[:len(expanded)//2]) - if reverse: - rmask &= expanded else: expanded = np.tile(expanded, int(m["tile"])) - if reverse: - rmask &= expanded mask &= expanded idx["mask"] = mask idx["indicies"] = np.flatnonzero(mask) - idx["rmask"] = rmask - idx["rindicies"] = np.flatnonzero(rmask) if reverse else None - if reverse: - assert len(idx["indicies"]) == len(idx["rindicies"]), \ - "Index indicies and reverse direction indicies are not of equal length" if len(idx["indicies"]) == 0: raise ValueError("Regions would exclude entire scan") repeat *= len(idx["indicies"]) @@ -260,34 +231,39 @@ def iterator(self): yield p def get_point(self, n): - # how far along each index are we? if n >= self.num: raise IndexError("Requested point is out of range") p = Point() - prev_g = None - idx_reverse = False - gen_reverse = False + + # need to know how far along each index we are + # and, in the case of alternating indicies, how + # many times we've run through them + kc = 0 # the "cumulative" k for each index for idx in self.indexes: indicies = idx["indicies"] - if idx_reverse and idx["rindicies"] is not None: - indicies = idx["rindicies"] i = n // idx["repeat"] + r = i // len(indicies) i %= len(indicies) k = indicies[i] + idx_reverse = False + if idx["alternate"] and kc % 2 == 1: + indicies = indicies[::-1] + idx_reverse = True + kc *= len(indicies) + kc += k + k = indicies[i] # need point k along each generator in index + # in alternating case, need to sometimes go backward for g in idx["generators"]: j = k // self.generator_idx_scaling[g]["repeat"] + gr = j // g.num j %= g.num - if g.alternate_direction: - if gen_reverse: - j = g.num - j - 1 - gen_reverse = j % 2 == 1 + if idx["alternate"] and g is not idx["generators"][0] and gr % 2 == 1: + j = g.num - j - 1 for axis in g.axes: p.positions[axis] = g.points[axis][j] p.lower[axis] = g.points_lower[axis][j] p.upper[axis] = g.points_upper[axis][j] - prev_g = g - idx_reverse = gen_reverse return p def to_dict(self): diff --git a/scanpointgenerator/generators/lissajousgenerator.py b/scanpointgenerator/generators/lissajousgenerator.py index d757962..f7e7ee4 100644 --- a/scanpointgenerator/generators/lissajousgenerator.py +++ b/scanpointgenerator/generators/lissajousgenerator.py @@ -10,7 +10,8 @@ class LissajousGenerator(Generator): """Generate the points of a Lissajous curve""" - def __init__(self, names, units, box, num_lobes, num_points=None): + def __init__(self, names, units, box, num_lobes, + num_points=None, alternate_direction=False): """ Args: names (list(str)): The scannable names e.g. ["x", "y"] @@ -28,6 +29,7 @@ def __init__(self, names, units, box, num_lobes, num_points=None): self.points = None self.points_lower = None self.points_upper = None + self.alternate_direction = alternate_direction if len(self.names) != len(set(self.names)): raise ValueError("Axis names cannot be duplicated; given %s" % diff --git a/tests/test_core/test_compoundgenerator.py b/tests/test_core/test_compoundgenerator.py index 79b4dea..1798e54 100644 --- a/tests/test_core/test_compoundgenerator.py +++ b/tests/test_core/test_compoundgenerator.py @@ -9,7 +9,7 @@ from scanpointgenerator import SpiralGenerator from scanpointgenerator import LissajousGenerator from scanpointgenerator import Excluder -from scanpointgenerator.rois import CircularROI, RectangularROI +from scanpointgenerator.rois import CircularROI, RectangularROI, EllipticalROI, SectorROI from scanpointgenerator.mutators import FixedDurationMutator, RandomOffsetMutator from scanpointgenerator.compat import range_ @@ -113,16 +113,38 @@ def test_alternating_simple(self): points = [p.positions for p in list(g.iterator())] self.assertEqual(expected, points) - def test_alternating_regions(self): - y = LineGenerator("y", "mm", 1, 5, 5) - x = LineGenerator("x", "mm", 1, 5, 5, alternate_direction=True) + def test_alternating_three_axis(self): + z = LineGenerator("z", "mm", 1, 2, 2) + y = LineGenerator("y", "mm", 1, 2, 2, True) + x = LineGenerator("x", "mm", 1, 3, 3, True) + g = CompoundGenerator([z, y, x], [], []) + g.prepare() + expected = [] + y_f = True + x_f = True + for z in range_(1, 3): + y_r = range_(1, 3) if y_f else range_(2, 0, -1) + y_f = not y_f + for y in y_r: + x_r = range_(1, 4) if x_f else range_(3, 0, -1) + x_f = not x_f + for x in x_r: + expected.append({"x":float(x), "y":float(y), "z":float(z)}) + actual = [p.positions for p in g.iterator()] + self.assertEqual(expected, actual) + + def test_alternating_with_region(self): + y = LineGenerator("y", "mm", 1, 5, 5, True) + x = LineGenerator("x", "mm", 1, 5, 5, True) r1 = RectangularROI([2, 2], 2, 2) e1 = Excluder(r1, ["y", "x"]) g = CompoundGenerator([y, x], [e1], []) g.prepare() expected = [] - for y in range(1, 6): - r = range(1, 6) if y % 2 == 1 else range(5, 0, -1) + x_f = True + for y in range_(1, 6): + r = range_(1, 6) if x_f else range(5, 0, -1) + x_f = not x_f for x in r: expected.append({"y":float(y), "x":float(x)}) expected = [p for p in expected if @@ -130,20 +152,135 @@ def test_alternating_regions(self): points = [p.positions for p in list(g.iterator())] self.assertEqual(expected, points) - def test_alternating_regions_2(self): + def test_inner_alternating(self): z = LineGenerator("z", "mm", 1, 5, 5) y = LineGenerator("y", "mm", 1, 5, 5, alternate_direction=True) - x = LineGenerator("x", "mm", 1, 5, 5) + x = LineGenerator("x", "mm", 1, 5, 5, alternate_direction=True) + r1 = RectangularROI([2, 2], 2, 2) + e1 = Excluder(r1, ["x", "y"]) + g = CompoundGenerator([z, y, x], [e1], []) + g.prepare() + actual = [p.positions for p in list(g.iterator())] + expected = [] + iy = 0 + ix = 0 + for z in range_(1, 6): + for y in (range(1, 6) if iy % 2 == 0 else range(5, 0, -1)): + for x in (range(1, 6) if ix % 2 == 0 else range(5, 0, -1)): + if x >= 2 and x < 4 and y >= 2 and y < 4: + expected.append( + {"x":float(x), "y":float(y), "z":float(z)}) + ix += 1 + iy += 1 + self.assertEqual(expected, actual) + + def test_two_dim_inner_alternates(self): + wg = LineGenerator("w", "mm", 0, 1, 2) + zg = LineGenerator("z", "mm", 0, 1, 2) + yg = LineGenerator("y", "mm", 1, 3, 3, True) + xg = LineGenerator("x", "mm", 0, 1, 2, True) + r1 = EllipticalROI([0, 1], [1, 2]) + r2 = SectorROI([0, 0], [0.2, 1], [0, 7]) + e1 = Excluder(r1, ['x', 'y']) + e2 = Excluder(r2, ['w', 'z']) + g = CompoundGenerator([wg, zg, yg, xg], [e1, e2], []) + g.prepare() + actual = [p.positions for p in g.iterator()] + expected = [(0, 3, 1, 0), (0, 2, 1, 0), (1, 1, 1, 0), (0, 1, 1, 0), + (0, 1, 0, 1), (1, 1, 0, 1), (0, 2, 0, 1), (0, 3, 0, 1)] + expected = [{"x":float(x), "y":float(y), "z":float(z), "w":float(w)} + for (x, y, z, w) in expected] + self.assertEqual(expected, actual) + + def test_three_dim_middle_alternates(self): + tg = LineGenerator("t", "mm", 1, 5, 5) + zg = LineGenerator("z", "mm", -1, 3, 5, True) + spiral = SpiralGenerator(["s1", "s2"], "mm", [1, 1], 2, 1, True) + yg = LineGenerator("y", "mm", 0, 4, 5) + xg = LineGenerator("x", "mm", 0, 4, 5) + r1 = CircularROI([0, 0], 1) + e1 = Excluder(r1, ["s1", "z"]) + e2 = Excluder(r1, ["y", "x"]) + g = CompoundGenerator([tg, zg, spiral, yg, xg], [e2, e1], []) + g.prepare() + + it = 0 + iz = 0 + iy = 0 + ix = 0 + tzs = [] + points = [] + for t in range_(1, 6): + for z in (range_(-1, 4) if it % 2 == 0 else range_(3, -2, -1)): + s1p = spiral.points["s1"] if iz % 2 == 0 else spiral.points["s1"][::-1] + s2p = spiral.points["s2"] if iz % 2 == 0 else spiral.points["s2"][::-1] + points += [(x, y, s1, s2, z, t) for (s1, s2) in zip(s1p, s2p) + for y in range(0, 5) for x in range(0, 5) + if s1*s1 + z*z <= 1 and y*y + x*x <= 1] + iz += 1 + it += 1 + expected = [{"x":float(x), "y":float(y), "s1":s1, "s2":s2, "z":float(z), "t":float(t)} + for (x, y, s1, s2, z, t) in points] + actual = [p.positions for p in list(g.iterator())] + for e, a in zip(expected, actual): + self.assertEqual(e, a) + + def test_triple_alternating_linked_gen(self): + tg = LineGenerator("t", "mm", 1, 5, 5) + zg = LineGenerator("z", "mm", -1, 3, 5, True) + yg = LineGenerator("y", "mm", 0, 4, 5, True) + xg = LineGenerator("x", "mm", 0, 4, 5, True) + r1 = RectangularROI([-1, -1], 5.5, 3.5) + r2 = RectangularROI([1, 0], 2.5, 2.5) + e1 = Excluder(r1, ["z", "y"]) + e2 = Excluder(r2, ["x", "y"]) + g = CompoundGenerator([tg, zg, yg, xg], [e1, e2], []) + g.prepare() + zf = True + yf = True + xf = True + expected = [] + for t in range_(1, 6): + zr = range_(-1, 4) if zf else range_(3, -2, -1) + zf = not zf + for z in zr: + yr = range_(0, 5) if yf else range_(4, -1, -1) + yf = not yf + for y in yr: + xr = range_(0, 5) if xf else range_(4, -1, -1) + xf = not xf + for x in xr: + if z >= -1 and z < 4.5 and y >= 0 and y < 2.5 \ + and x >= 1 and x < 3.5: + expected.append({"x":float(x), "y":float(y), + "z":float(z), "t":float(t)}) + actual = [p.positions for p in g.iterator()] + self.assertEqual(len(expected), len(actual)) + for e, a in zip(expected, actual): + self.assertEqual(e, a) + + def test_alternating_regions_2(self): + z = LineGenerator("z", "mm", 1, 5, 5, True) + y = LineGenerator("y", "mm", 1, 5, 5, True) + x = LineGenerator("x", "mm", 1, 5, 5, True) r1 = RectangularROI([2, 2], 2, 2) e1 = Excluder(r1, ["x", "y"]) e2 = Excluder(r1, ["z", "y"]) g = CompoundGenerator([z, y, x], [e1, e2], []) #20 points g.prepare() actual = [p.positions for p in list(g.iterator())] - expected = [{"x":float(x), "y":float(y), "z":float(z)} - for z in range(1, 6) - for y in (range(1, 6) if z % 2 == 1 else range(5, 0, -1)) - for x in range(1, 6)] + expected = [] + yf = True + xf = True + for z in range_(1, 6): + yr = range_(1, 6) if yf else range_(5, 0, -1) + yf = not yf + for y in yr: + xr = range_(1, 6) if xf else range_(5, 0, -1) + xf = not xf + for x in xr: + expected.append({"x":float(x), "y":float(y), "z":float(z)}) + expected = [p for p in expected if p["x"] >= 2 and p["x"] < 4 and p["y"] >= 2 and p["y"] < 4 and p["z"] >= 2 and p["z"] < 4] @@ -151,9 +288,9 @@ def test_alternating_regions_2(self): def test_alternating_complex(self): tg = LineGenerator("t", "mm", 1, 5, 5) - zg = LineGenerator("z", "mm", 1, 5, 5, alternate_direction=True) - yg = LineGenerator("y", "mm", 1, 5, 5, alternate_direction=True) - xg = LineGenerator("x", "mm", 1, 5, 5) + zg = LineGenerator("z", "mm", 1, 5, 5, True) + yg = LineGenerator("y", "mm", 1, 5, 5, True) + xg = LineGenerator("x", "mm", 1, 5, 5, True) r1 = RectangularROI([3., 3.], 2., 2.) e1 = Excluder(r1, ["y", "x"]) e2 = Excluder(r1, ["z", "y"]) @@ -162,12 +299,17 @@ def test_alternating_complex(self): g.prepare() points = [p.positions for p in list(g.iterator())] expected = [] - for t in range(1, 6): - r_1 = range(1,6) if t % 2 == 1 else range(5, 0, -1) + zf, yf, xf = True, True, True + for t in range_(1, 6): + r_1 = range_(1,6) if zf else range_(5, 0, -1) + zf = not zf for z in r_1: - r_2 = range(1,6) if z % 2 == 1 else range(5, 0, -1) + r_2 = range_(1,6) if yf else range_(5, 0, -1) + yf = not yf for y in r_2: - for x in range(1, 6): + r_3 = range_(1, 6) if xf else range_(5, 0, -1) + xf = not xf + for x in r_3: expected.append( {"t":float(t), "z":float(z), "y":float(y), "x":float(x)}) @@ -226,10 +368,13 @@ def test_line_lissajous(self): self.assertEqual(expected[i], p.positions) def test_horrible_scan(self): - lissajous = LissajousGenerator(["j1", "j2"], "mm", {"centre":[-0.5, 0.7], "width":2, "height":3.5}, 7, 100) - line2 = LineGenerator(["l2"], "mm", -3, 3, 7, alternate_direction = True) - line1 = LineGenerator(["l1"], "mm", -1, 2, 5, alternate_direction = True) - spiral = SpiralGenerator(["s1", "s2"], "mm", [1, 2], 5, 2.5, alternate_direction = True) + lissajous = LissajousGenerator( + ["j1", "j2"], "mm", + {"centre":[-0.5, 0.7], "width":2, "height":3.5}, + 7, 100, True) + line2 = LineGenerator(["l2"], "mm", -3, 3, 7, True) + line1 = LineGenerator(["l1"], "mm", -1, 2, 5, True) + spiral = SpiralGenerator(["s1", "s2"], "mm", [1, 2], 5, 2.5, True) r1 = CircularROI([1, 1], 2) r2 = CircularROI([-1, -1], 4) r3 = CircularROI([1, 1], 1) @@ -238,18 +383,23 @@ def test_horrible_scan(self): e3 = Excluder(r3, ["s1", "s2"]) g = CompoundGenerator([lissajous, line2, line1, spiral], [e1, e2, e3], []) g.prepare() - idx = range_(0, lissajous.num) - p_liss = [(j1, j2, i) for (j1, j2, i) in zip(lissajous.points["j1"], lissajous.points["j2"], idx)] - idx = range_(0, line2.num) - p_l2liss = [(l2, j1, j2, i2) for (j1, j2, i) in p_liss for (l2, i2) in - (zip(line2.points["l2"], idx) if i % 2 == 0 else zip(line2.points["l2"][::-1], idx))] - idx = range_(0, line1.num) - p_l1l2liss = [(l1, l2, j1, j2, i2) for (l2, j1, j2, i) in p_l2liss for (l1, i2) in - (zip(line1.points["l1"], idx) if i % 2 == 0 else - zip(line1.points["l1"][::-1], idx))] - points = [(s1, s2, l1, l2, j1, j2) for (l1, l2, j1, j2, i) in p_l1l2liss for (s1, s2) in - (zip(spiral.points["s1"], spiral.points["s2"]) if i % 2 == 0 else - zip(spiral.points["s1"][::-1], spiral.points["s2"][::-1]))] + + l2_f = True + l1_f = True + s_f = True + points = [] + for (j1, j2) in zip(lissajous.points["j1"], lissajous.points["j2"]): + l2p = line2.points["l2"] if l2_f else line2.points["l2"][::-1] + l2_f = not l2_f + for l2 in l2p: + l1p = line1.points["l1"] if l1_f else line1.points["l1"][::-1] + l1_f = not l1_f + for l1 in l1p: + sp = zip(spiral.points["s1"], spiral.points["s2"]) if s_f \ + else zip(spiral.points["s1"][::-1], spiral.points["s2"][::-1]) + s_f = not s_f + for (s1, s2) in sp: + points.append((s1, s2, l1, l2, j1, j2)) self.assertEqual(lissajous.num * line2.num * line1.num * spiral.num, len(points)) points = [(s1, s2, l1, l2, j1, j2) for (s1, s2, l1, l2, j1, j2) in points if @@ -263,30 +413,37 @@ def test_horrible_scan(self): actual = [p.positions for p in generated_points] expected = [{"j1":j1, "j2":j2, "l2":l2, "l1":l1, "s1":s1, "s2":s2} for (s1, s2, l1, l2, j1, j2) in points] - self.assertEqual(expected, actual) + for e, a in zip(expected, actual): + self.assertEqual(e, a) def test_double_spiral_scan(self): - line1 = LineGenerator(["l1"], "mm", -1, 2, 5) - spiral_s = SpiralGenerator(["s1", "s2"], "mm", [1, 2], 5, 2.5, alternate_direction = True) - spiral_t = SpiralGenerator(["t1", "t2"], "mm", [0, 0], 5, 2.5, alternate_direction = True) - line2 = LineGenerator(["l2"], "mm", -1, 2, 5, alternate_direction = True) + line1 = LineGenerator(["l1"], "mm", -1, 2, 5, True) + spiral_s = SpiralGenerator(["s1", "s2"], "mm", [1, 2], 5, 2.5, True) + spiral_t = SpiralGenerator(["t1", "t2"], "mm", [0, 0], 5, 2.5, True) + line2 = LineGenerator(["l2"], "mm", -1, 2, 5, True) r = CircularROI([0, 0], 1) e1 = Excluder(r, ["s1", "l1"]) e2 = Excluder(r, ["l2", "t1"]) g = CompoundGenerator([line1, spiral_s, spiral_t, line2], [e1, e2], []) g.prepare() - idx = range_(0, line1.num) - p_l1 = [(l1, i) for (l1, i) in zip(line1.points["l1"], idx)] - idx = range_(spiral_s.num) - p_ss_l1 = [(s1, s2, l1, i2) for (l1, i) in p_l1 for (s1, s2, i2) in - (zip(spiral_s.points["s1"], spiral_s.points["s2"], idx) if i % 2 == 0 else - zip(spiral_s.points["s1"][::-1], spiral_s.points["s2"][::-1], idx))] - idx = range_(spiral_t.num) - p_st_ss_l1 = [(t1, t2, s1, s2, l1, i2) for (s1, s2, l1, i) in p_ss_l1 for (t1, t2, i2) in - (zip(spiral_t.points["t1"], spiral_t.points["t2"], idx) if i % 2 == 0 else - zip(spiral_t.points["t1"][::-1], spiral_t.points["t2"][::-1], idx))] - points = [(l2, t1, t2, s1, s2, l1) for (t1, t2, s1, s2, l1, i) in p_st_ss_l1 for l2 in - (line2.points["l2"] if i % 2 == 0 else line2.points["l2"][::-1])] + + points = [] + s_f = True + t_f = True + l2_f = True + for l1 in line1.points["l1"]: + sp = zip(spiral_s.points['s1'], spiral_s.points['s2']) + sp = sp if s_f else list(sp)[::-1] + s_f = not s_f + for (s1, s2) in sp: + tp = zip(spiral_t.points["t1"], spiral_t.points["t2"]) + tp = tp if t_f else list(tp)[::-1] + t_f = not t_f + for (t1, t2) in tp: + l2p = line2.points['l2'] if l2_f else line2.points['l2'][::-1] + l2_f = not l2_f + for l2 in l2p: + points.append((l2, t1, t2, s1, s2, l1)) expected = [{"l2":l2, "t1":t1, "t2":t2, "s1":s1, "s2":s2, "l1":l1} for (l2, t1, t2, s1, s2, l1) in points if @@ -344,7 +501,6 @@ def test_prepare_with_regions(self): expected_mask = [(x/4.)**2 + (y/4.)**2 <= 1 for y in range(0, 5) for x in range(0, 5)] self.assertEqual(expected_mask, g.indexes[0]["mask"].tolist()) - self.assertIsNone(g.indexes[0]["rmask"]) def test_simple_mask(self): x = LineGenerator("x", "mm", -1.0, 1.0, 5, False) @@ -359,7 +515,7 @@ def test_simple_mask(self): def test_simple_mask_alternating(self): x = LineGenerator("x", "mm", -1.0, 1.0, 5, alternate_direction=True) - y = LineGenerator("y", "mm", -1.0, 1.0, 5, alternate_direction=False) + y = LineGenerator("y", "mm", -1.0, 1.0, 5, alternate_direction=True) r = CircularROI([0.5, 0], 1) e = Excluder(r, ["x", "y"]) g = CompoundGenerator([y, x], [e], []) @@ -376,7 +532,7 @@ def test_simple_mask_alternating(self): self.assertEqual(expected_mask, g.indexes[0]["mask"].tolist()) def test_double_mask_alternating_spiral(self): - zgen = LineGenerator("z", "mm", 0.0, 4.0, 5) + zgen = LineGenerator("z", "mm", 0.0, 4.0, 5, alternate_direction=True) spiral = SpiralGenerator(['x', 'y'], "mm", [0.0, 0.0], 3, alternate_direction=True) #29 points r1 = RectangularROI([-2, -2], 4, 3) r2 = RectangularROI([-2, 0], 4, 3) @@ -420,9 +576,7 @@ def test_simple_mask_alternating_spiral(self): expected = [x >= -2 and x < 1 and y >= -2 and y < 2 for (x, y) in p] expected_r = [x >= -2 and x < 1 and y >= -2 and y < 2 for (x, y) in p[::-1]] actual = g.indexes[1]["mask"].tolist() - actual_r = g.indexes[1]["rmask"].tolist() self.assertEqual(expected, actual) - self.assertEqual(expected_r, actual_r) def test_double_mask(self): x = LineGenerator("x", "mm", -1.0, 1.0, 5, False) @@ -443,9 +597,9 @@ def test_double_mask(self): def test_complex_masks(self): tg = LineGenerator("t", "mm", 1, 5, 5) - zg = LineGenerator("z", "mm", 1, 5, 5, alternate_direction=True) + zg = LineGenerator("z", "mm", 0, 4, 5, alternate_direction=True) yg = LineGenerator("y", "mm", 1, 5, 5, alternate_direction=True) - xg = LineGenerator("x", "mm", 1, 5, 5) + xg = LineGenerator("x", "mm", 2, 6, 5, alternate_direction=True) r1 = RectangularROI([3., 3.], 2., 2.) e1 = Excluder(r1, ["y", "x"]) e2 = Excluder(r1, ["z", "y"]) @@ -453,21 +607,21 @@ def test_complex_masks(self): g.prepare() t_mask = [True] * 5 - zp = range(1, 6) - yzp = [(y, z) for z in zp for y in (range(1, 6) if z % 2 == 1 else range(5, 0, -1))] - xyzp = [(x, y, z) for (y, z) in yzp for x in range(1, 6)] - xyz_mask = [x >= 3 and x < 5 and y >= 3 and y < 5 and z >= 3 and z < 5 - for (x, y, z) in xyzp] - - r_zp = range(5, 0, -1) - r_yzp = [(y, z) for z in r_zp for y in (range(1, 6) if z % 2 == 1 else range(5, 0, -1))] - r_xyzp = [(x, y, z) for (y, z) in r_yzp for x in range(1, 6)] - r_xyz_mask = [x >= 3 and x < 5 and y >= 3 and y < 5 and z >= 3 and z < 5 - for (x, y, z) in r_xyzp] + iy = 0 + ix = 0 + xyz_mask = [] + xyz = [] + for z in range_(0, 5): + for y in (range_(1, 6) if iy % 2 == 0 else range_(5, 0, -1)): + for x in (range_(2, 7) if ix % 2 == 0 else range_(6, 1, -1)): + xyz_mask.append( x >= 3 and x < 5 and y >= 3 and y < 5 + and z >= 3 and z < 5 ) + xyz.append((x, y, z)) + ix += 1 + iy += 1 + self.assertEqual(t_mask, g.indexes[0]["mask"].tolist()) - self.assertIsNone(g.indexes[0]["rmask"]) self.assertEqual(xyz_mask, g.indexes[1]["mask"].tolist()) - self.assertEqual(r_xyz_mask, g.indexes[1]["rmask"].tolist()) def test_separate_indexes(self): x1 = LineGenerator("x1", "mm", -1.0, 1.0, 5, False) diff --git a/tests/test_core/test_compoundgenerator_performance.py b/tests/test_core/test_compoundgenerator_performance.py index c25c04b..9a15105 100644 --- a/tests/test_core/test_compoundgenerator_performance.py +++ b/tests/test_core/test_compoundgenerator_performance.py @@ -17,10 +17,9 @@ def test_200_million_time_constraint(self): start_time = time.time() s = SpiralGenerator( - ["x", "y"], "mm", [0, 0], 6, 0.02, - alternate_direction=True) # ~2e5 points - z = LineGenerator("z", "mm", 0, 1, 100) #1e2 points - w = LineGenerator("w", "mm", 0, 1, 10) #1e1 points + ["x", "y"], "mm", [0, 0], 6, 0.02, True) # ~2e5 points + z = LineGenerator("z", "mm", 0, 1, 100, True) #1e2 points + w = LineGenerator("w", "mm", 0, 1, 10, True) #1e1 points r1 = CircularROI([-0.7, 4], 0.5) r2 = CircularROI([0.5, 0.5], 0.3) r3 = CircularROI([0.2, 4], 0.5) From 0082b8fa4e72558a1c6af50f637d45cf480c0029 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Mon, 14 Nov 2016 10:10:23 +0000 Subject: [PATCH 12/47] Rename internal index dict to dimension in compound generator Done to remove confusion when we start adding dataset indexes to points. --- scanpointgenerator/core/compoundgenerator.py | 111 +++++++++---------- tests/test_core/test_compoundgenerator.py | 39 ++++--- 2 files changed, 74 insertions(+), 76 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index 0bad651..e6371f2 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -29,8 +29,7 @@ def __init__(self, generators, excluders, mutators): self.axes_points = {} self.axes_points_lower = {} self.axes_points_upper = {} - self.index_dims = [] - self.indexes = [] + self.dimensions = [] self.alternate_direction = [g.alternate_direction for g in generators] for generator in generators: logging.debug("Generator passed to Compound init") @@ -44,27 +43,26 @@ def __init__(self, generators, excluders, mutators): raise ValueError("Axis names cannot be duplicated") self.generators = generators - self.generator_idx_scaling = {} + self.generator_dim_scaling = {} def prepare(self): self.num = 1 - self.indexes = [] + self.dimensions = [] for generator in self.generators: generator.produce_points() self.axes_points.update(generator.points) self.axes_points_lower.update(generator.points_lower) self.axes_points_upper.update(generator.points_upper) self.num *= generator.num - self.index_dims += generator.index_dims - idx = {"size":generator.num, + dim = {"size":generator.num, "axes":list(generator.axes), "generators":[generator], "masks":[], "tile":1, "repeat":1, "alternate":generator.alternate_direction} - self.indexes.append(idx) + self.dimensions.append(dim) for excluder in self.excluders: axis_1, axis_2 = excluder.scannables @@ -84,25 +82,26 @@ def prepare(self): ##### - # first check if region spans two indexes - merge if so + # first check if region spans two dimensions - merge if so ##### - idx_1 = [i for i in self.indexes if axis_1 in i["axes"]][0] - idx_2 = [i for i in self.indexes if axis_2 in i["axes"]][0] - idx_diff = self.indexes.index(idx_1) - self.indexes.index(idx_2) - if idx_1["alternate"] != idx_2["alternate"]: + dim_1 = [i for i in self.dimensions if axis_1 in i["axes"]][0] + dim_2 = [i for i in self.dimensions if axis_2 in i["axes"]][0] + dim_diff = self.dimensions.index(dim_1) \ + - self.dimensions.index(dim_2) + if dim_1["alternate"] != dim_2["alternate"]: raise ValueError( "Generators tied by regions must have the same " \ "alternate_direction setting") # merge "inner" into "outer" - if idx_diff < -1 or idx_diff > 1: + if dim_diff < -1 or dim_diff > 1: raise ValueError( "Excluders must be defined on axes that are adjacent in " \ "generator order") - if idx_diff == 1: - idx_1, idx_2 = idx_2, idx_1 - idx_diff = -1 - if idx_diff == -1: - # idx_1 is "outer" - preserves axis ordering + if dim_diff == 1: + dim_1, dim_2 = dim_2, dim_1 + dim_diff = -1 + if dim_diff == -1: + # dim_1 is "outer" - preserves axis ordering # need to appropriately scale the existing masks # masks are "tiled" by the size of generators "below" them @@ -110,21 +109,21 @@ def prepare(self): # above them, so: # |mask| * duplicates * repeates == |generators in index| scale = 1 - for g in idx_2["generators"]: + for g in dim_2["generators"]: scale *= g.num - for m in idx_1["masks"]: + for m in dim_1["masks"]: m["repeat"] *= scale scale = 1 - for g in idx_1["generators"]: + for g in dim_1["generators"]: scale *= g.num - for m in idx_2["masks"]: + for m in dim_2["masks"]: m["tile"] *= scale - idx_1["masks"] += idx_2["masks"] - idx_1["axes"] += idx_2["axes"] - idx_1["generators"] += idx_2["generators"] - idx_1["size"] *= idx_2["size"] - self.indexes.remove(idx_2) - idx = idx_1 + dim_1["masks"] += dim_2["masks"] + dim_1["axes"] += dim_2["axes"] + dim_1["generators"] += dim_2["generators"] + dim_1["size"] *= dim_2["size"] + self.dimensions.remove(dim_2) + dim = dim_1 ##### # generate the mask for this region @@ -138,13 +137,13 @@ def prepare(self): doubled_mask = False # used for some cases of alternating generators - if gen_1 is gen_2 and idx["alternate"]: + if gen_1 is gen_2 and dim["alternate"]: # run *both* axes backwards # but our mask will be a factor of 2 too big doubled_mask = True points_1 = np.append(points_1, points_1[::-1]) points_2 = np.append(points_2, points_2[::-1]) - elif idx["alternate"]: + elif dim["alternate"]: doubled_mask = True points_1 = np.append(points_1, points_1[::-1]) points_2 = np.append(points_2, points_2[::-1]) @@ -169,7 +168,7 @@ def prepare(self): found_axis = False # tile by product of generators "before" # repeat by product of generators "after" - for g in idx["generators"]: + for g in dim["generators"]: if axis_1 in g.axes or axis_2 in g.axes: found_axis = True else: @@ -178,7 +177,7 @@ def prepare(self): else: tile *= g.num m = {"repeat":repeat, "tile":tile, "mask":mask} - idx["masks"].append(m) + dim["masks"].append(m) # end for excluder in self.excluders ##### @@ -187,9 +186,9 @@ def prepare(self): ##### # Generate full index mask and "apply" ##### - for idx in self.indexes: - mask = np.full(idx["size"], True, dtype=np.bool) - for m in idx["masks"]: + for dim in self.dimensions: + mask = np.full(dim["size"], True, dtype=np.bool) + for m in dim["masks"]: assert len(m["mask"]) * m["repeat"] * m["tile"] == len(mask), \ "Mask lengths are not consistent" expanded = np.repeat(m["mask"], m["repeat"]) @@ -199,29 +198,29 @@ def prepare(self): else: expanded = np.tile(expanded, int(m["tile"])) mask &= expanded - idx["mask"] = mask - idx["indicies"] = np.flatnonzero(mask) - if len(idx["indicies"]) == 0: + dim["mask"] = mask + dim["indicies"] = np.flatnonzero(mask) + if len(dim["indicies"]) == 0: raise ValueError("Regions would exclude entire scan") - repeat *= len(idx["indicies"]) + repeat *= len(dim["indicies"]) self.num = repeat - for idx in self.indexes: - l = len(idx["indicies"]) + for dim in self.dimensions: + l = len(dim["indicies"]) repeat /= l - idx["tile"] = tile - idx["repeat"] = repeat + dim["tile"] = tile + dim["repeat"] = repeat tile *= l - for idx in self.indexes: + for dim in self.dimensions: tile = 1 repeat = 1 - for g in idx["generators"]: + for g in dim["generators"]: repeat *= g.num - for g in idx["generators"]: + for g in dim["generators"]: repeat /= g.num d = {"tile":tile, "repeat":repeat} tile *= g.num - self.generator_idx_scaling[g] = d + self.generator_dim_scaling[g] = d def iterator(self): it = (self.get_point(n) for n in range_(self.num)) @@ -239,26 +238,26 @@ def get_point(self, n): # and, in the case of alternating indicies, how # many times we've run through them kc = 0 # the "cumulative" k for each index - for idx in self.indexes: - indicies = idx["indicies"] - i = n // idx["repeat"] + for dim in self.dimensions: + indicies = dim["indicies"] + i = n // dim["repeat"] r = i // len(indicies) i %= len(indicies) k = indicies[i] - idx_reverse = False - if idx["alternate"] and kc % 2 == 1: + dim_reverse = False + if dim["alternate"] and kc % 2 == 1: indicies = indicies[::-1] - idx_reverse = True + dim_reverse = True kc *= len(indicies) kc += k k = indicies[i] # need point k along each generator in index # in alternating case, need to sometimes go backward - for g in idx["generators"]: - j = k // self.generator_idx_scaling[g]["repeat"] + for g in dim["generators"]: + j = k // self.generator_dim_scaling[g]["repeat"] gr = j // g.num j %= g.num - if idx["alternate"] and g is not idx["generators"][0] and gr % 2 == 1: + if dim["alternate"] and g is not dim["generators"][0] and gr % 2 == 1: j = g.num - j - 1 for axis in g.axes: p.positions[axis] = g.points[axis][j] diff --git a/tests/test_core/test_compoundgenerator.py b/tests/test_core/test_compoundgenerator.py index 1798e54..83f4576 100644 --- a/tests/test_core/test_compoundgenerator.py +++ b/tests/test_core/test_compoundgenerator.py @@ -481,13 +481,12 @@ def test_post_prepare(self): self.assertListAlmostEqual([1.95, 2.05], g.axes_points_lower["y"].tolist()) self.assertListAlmostEqual([2.05, 2.15], g.axes_points_upper["y"].tolist()) self.assertEqual(g.num, 6) - self.assertEqual(g.index_dims, [2, 3]) - self.assertEqual(2, len(g.indexes)) - self.assertEqual(["y"], g.indexes[0]["axes"]) - self.assertEqual([True] * 2, g.indexes[0]["mask"].tolist()) - self.assertEqual(["x"], g.indexes[1]["axes"]) - self.assertEqual([True] * 3, g.indexes[1]["mask"].tolist()) + self.assertEqual(2, len(g.dimensions)) + self.assertEqual(["y"], g.dimensions[0]["axes"]) + self.assertEqual([True] * 2, g.dimensions[0]["mask"].tolist()) + self.assertEqual(["x"], g.dimensions[1]["axes"]) + self.assertEqual([True] * 3, g.dimensions[1]["mask"].tolist()) def test_prepare_with_regions(self): x = LineGenerator("x", "mm", 0, 1, 5, False) @@ -496,11 +495,11 @@ def test_prepare_with_regions(self): excluder = Excluder(circle, ['x', 'y']) g = CompoundGenerator([y, x], [excluder], []) g.prepare() - self.assertEqual(1, len(g.indexes)) - self.assertEqual(["y", "x"], g.indexes[0]["axes"]) + self.assertEqual(1, len(g.dimensions)) + self.assertEqual(["y", "x"], g.dimensions[0]["axes"]) expected_mask = [(x/4.)**2 + (y/4.)**2 <= 1 for y in range(0, 5) for x in range(0, 5)] - self.assertEqual(expected_mask, g.indexes[0]["mask"].tolist()) + self.assertEqual(expected_mask, g.dimensions[0]["mask"].tolist()) def test_simple_mask(self): x = LineGenerator("x", "mm", -1.0, 1.0, 5, False) @@ -511,7 +510,7 @@ def test_simple_mask(self): g.prepare() p = [(x/2., y/2.) for y in range_(-2, 3) for x in range_(-2, 3)] expected_mask = [x*x + y*y <= 1 for (x, y) in p] - self.assertEqual(expected_mask, g.indexes[0]["mask"].tolist()) + self.assertEqual(expected_mask, g.dimensions[0]["mask"].tolist()) def test_simple_mask_alternating(self): x = LineGenerator("x", "mm", -1.0, 1.0, 5, alternate_direction=True) @@ -529,7 +528,7 @@ def test_simple_mask_alternating(self): p += [(x/2., y/2.) for x in range_(-2, 3)] reverse = not reverse expected_mask = [(x-0.5)**2 + y**2 <= 1**2 for (x, y) in p] - self.assertEqual(expected_mask, g.indexes[0]["mask"].tolist()) + self.assertEqual(expected_mask, g.dimensions[0]["mask"].tolist()) def test_double_mask_alternating_spiral(self): zgen = LineGenerator("z", "mm", 0.0, 4.0, 5, alternate_direction=True) @@ -546,7 +545,7 @@ def test_double_mask_alternating_spiral(self): p += [(x, y, z) for (x, y) in (xy if z % 2 == 0 else xy[::-1])] expected = [x >= -2 and x < 1 and y >= -2 and y < 2 and z >= 0 and z < 3 for (x, y, z) in p] - actual = g.indexes[0]["mask"].tolist() + actual = g.dimensions[0]["mask"].tolist() self.assertEqual(expected, actual) def test_double_mask_spiral(self): @@ -562,7 +561,7 @@ def test_double_mask_spiral(self): p = [(x, y, z) for z in range_(0, 5) for (x, y) in p] expected = [x >= -2 and x < 1 and y >= -2 and y < 2 and z >= 0 and z < 3 for (x, y, z) in p] - actual = g.indexes[0]["mask"].tolist() + actual = g.dimensions[0]["mask"].tolist() self.assertEqual(expected, actual) def test_simple_mask_alternating_spiral(self): @@ -575,7 +574,7 @@ def test_simple_mask_alternating_spiral(self): p = list(zip(g.axes_points['x'], g.axes_points['y'])) expected = [x >= -2 and x < 1 and y >= -2 and y < 2 for (x, y) in p] expected_r = [x >= -2 and x < 1 and y >= -2 and y < 2 for (x, y) in p[::-1]] - actual = g.indexes[1]["mask"].tolist() + actual = g.dimensions[1]["mask"].tolist() self.assertEqual(expected, actual) def test_double_mask(self): @@ -593,7 +592,7 @@ def test_double_mask(self): m1 = [(x-0.1)**2 + (y-0.2)**2 <= 1 for (x, y, z) in p] m2 = [(y-0.1)**2 + (z-0.2)**2 <= 1 for (x, y, z) in p] expected_mask = [(b1 and b2) for (b1, b2) in zip(m1, m2)] - self.assertEqual(expected_mask, g.indexes[0]["mask"].tolist()) + self.assertEqual(expected_mask, g.dimensions[0]["mask"].tolist()) def test_complex_masks(self): tg = LineGenerator("t", "mm", 1, 5, 5) @@ -620,8 +619,8 @@ def test_complex_masks(self): ix += 1 iy += 1 - self.assertEqual(t_mask, g.indexes[0]["mask"].tolist()) - self.assertEqual(xyz_mask, g.indexes[1]["mask"].tolist()) + self.assertEqual(t_mask, g.dimensions[0]["mask"].tolist()) + self.assertEqual(xyz_mask, g.dimensions[1]["mask"].tolist()) def test_separate_indexes(self): x1 = LineGenerator("x1", "mm", -1.0, 1.0, 5, False) @@ -648,13 +647,13 @@ def test_separate_indexes(self): m1 = [x*x + y*y <= 1 for (x, y, z) in p] m2 = [y*y + z*z <= 1 for (x, y, z) in p] expected_mask = [(b1 and b2) for (b1, b2) in zip(m1, m2)] - self.assertEqual(expected_mask, g.indexes[2]["mask"].tolist()) + self.assertEqual(expected_mask, g.dimensions[2]["mask"].tolist()) p = [(x/2., y/2.) for y in range_(-2, 3) for x in range_(-2, 3)] expected_mask = [x*x + y*y <= 1 for (x, y) in p] - self.assertEqual(expected_mask, g.indexes[1]["mask"].tolist()) + self.assertEqual(expected_mask, g.dimensions[1]["mask"].tolist()) p = [(x/4., y/4.) for y in range_(0, 5) for x in range_(0, 5)] expected_mask = [x*x + y*y <= 1 for (x, y) in p] - self.assertEqual(expected_mask, g.indexes[0]["mask"].tolist()) + self.assertEqual(expected_mask, g.dimensions[0]["mask"].tolist()) class TestSerialisation(unittest.TestCase): From 88d745d305af480e89a07290797a43adc2ebb5bf Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Mon, 14 Nov 2016 11:30:35 +0000 Subject: [PATCH 13/47] Add dimension indexes to points in CompoundGenerator --- scanpointgenerator/core/compoundgenerator.py | 3 +- tests/test_core/test_compoundgenerator.py | 117 +++++++++++++------ 2 files changed, 81 insertions(+), 39 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index e6371f2..865d6d6 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -246,13 +246,14 @@ def get_point(self, n): k = indicies[i] dim_reverse = False if dim["alternate"] and kc % 2 == 1: - indicies = indicies[::-1] + i = len(indicies) - i - 1 dim_reverse = True kc *= len(indicies) kc += k k = indicies[i] # need point k along each generator in index # in alternating case, need to sometimes go backward + p.indexes.append(i) for g in dim["generators"]: j = k // self.generator_dim_scaling[g]["repeat"] gr = j // g.num diff --git a/tests/test_core/test_compoundgenerator.py b/tests/test_core/test_compoundgenerator.py index 83f4576..b15c3cf 100644 --- a/tests/test_core/test_compoundgenerator.py +++ b/tests/test_core/test_compoundgenerator.py @@ -48,6 +48,8 @@ def test_iterator(self): expected_pos = [{"x":x/4., "y":y/4.} for y in range_(4, 9) for x in range_(4, 9)] self.assertEqual(expected_pos, [p.positions for p in points]) + expected_indexes = [[y, x] for y in range_(0, 5) for x in range_(0, 5)] + self.assertEqual(expected_indexes, [p.indexes for p in points]) def test_get_point(self): x = LineGenerator("x", "mm", -1., 1, 5, False) @@ -59,11 +61,17 @@ def test_get_point(self): g.prepare() points = [g.get_point(n) for n in range(0, g.num)] pos = [p.positions for p in points] - expected = [(x/2., y/2., z/2.) for z in range_(-2, 3) - for y in range_(-2, 3) + idx = [p.indexes for p in points] + xy_expected = [(x/2., y/2.) for y in range_(-2, 3) for x in range_(-2, 3)] - expected = [{'x':x, 'y':y, 'z':z} for (x, y, z) in expected if x*x + y*y <= 1] + xy_expected = [(x, y) for (x, y) in xy_expected + if x*x + y*y <= 1] + expected = [{"x":x, "y":y, "z":z/2.} for z in range_(-2, 3) + for (x, y) in xy_expected] self.assertEqual(expected, pos) + expected_idx = [[z, xy] for z in range_(5) + for xy in range_(len(xy_expected))] + self.assertEqual(expected_idx, idx) def test_get_point_large_scan(self): s = SpiralGenerator(["x", "y"], "mm", [0, 0], 6, 1) #114 points @@ -106,12 +114,15 @@ def test_alternating_simple(self): g = CompoundGenerator([y, x], [], []) g.prepare() expected = [] - for y in range(1, 6): - r = range(1, 6) if y % 2 == 1 else range(5, 0, -1) + expected_idx = [] + for y in range_(1, 6): + r = range_(1, 6) if y % 2 == 1 else range_(5, 0, -1) for x in r: expected.append({"y":float(y), "x":float(x)}) - points = [p.positions for p in list(g.iterator())] - self.assertEqual(expected, points) + expected_idx.append([y - 1, x - 1]) + points = list(g.iterator()) + self.assertEqual(expected, [p.positions for p in points]) + self.assertEqual(expected_idx, [p.indexes for p in points]) def test_alternating_three_axis(self): z = LineGenerator("z", "mm", 1, 2, 2) @@ -120,6 +131,7 @@ def test_alternating_three_axis(self): g = CompoundGenerator([z, y, x], [], []) g.prepare() expected = [] + expected_idx = [] y_f = True x_f = True for z in range_(1, 3): @@ -130,8 +142,10 @@ def test_alternating_three_axis(self): x_f = not x_f for x in x_r: expected.append({"x":float(x), "y":float(y), "z":float(z)}) - actual = [p.positions for p in g.iterator()] - self.assertEqual(expected, actual) + expected_idx.append([z-1, y-1, x-1]) + points = list(g.iterator()) + self.assertEqual(expected, [p.positions for p in points]) + self.assertEqual(expected_idx, [p.indexes for p in points]) def test_alternating_with_region(self): y = LineGenerator("y", "mm", 1, 5, 5, True) @@ -149,8 +163,10 @@ def test_alternating_with_region(self): expected.append({"y":float(y), "x":float(x)}) expected = [p for p in expected if (p["y"] >= 2 and p["y"] < 4 and p["x"] >= 2 and p["x"] < 4)] - points = [p.positions for p in list(g.iterator())] - self.assertEqual(expected, points) + expected_idx = [[xy] for xy in range_(len(expected))] + points = list(g.iterator()) + self.assertEqual(expected, [p.positions for p in points]) + self.assertEqual(expected_idx, [p.indexes for p in points]) def test_inner_alternating(self): z = LineGenerator("z", "mm", 1, 5, 5) @@ -160,19 +176,31 @@ def test_inner_alternating(self): e1 = Excluder(r1, ["x", "y"]) g = CompoundGenerator([z, y, x], [e1], []) g.prepare() - actual = [p.positions for p in list(g.iterator())] expected = [] - iy = 0 - ix = 0 + xy_expected = [] + x_f = True + for y in range_(1, 6): + for x in (range_(1, 6) if x_f else range(5, 0, -1)): + if x >= 2 and x < 4 and y >= 2 and y < 4: + xy_expected.append((x, y)) + x_f = not x_f + xy_f = True for z in range_(1, 6): - for y in (range(1, 6) if iy % 2 == 0 else range(5, 0, -1)): - for x in (range(1, 6) if ix % 2 == 0 else range(5, 0, -1)): - if x >= 2 and x < 4 and y >= 2 and y < 4: - expected.append( - {"x":float(x), "y":float(y), "z":float(z)}) - ix += 1 - iy += 1 - self.assertEqual(expected, actual) + for (x, y) in (xy_expected if xy_f else xy_expected[::-1]): + expected.append({"x":float(x), "y":float(y), "z":float(z)}) + xy_f = not xy_f + + expected_idx = [] + xy_f = True + for z in range_(0, 5): + xy_idx = range_(len(xy_expected)) if xy_f \ + else range_(len(xy_expected)-1, -1, -1) + expected_idx += [[z, xy] for xy in xy_idx] + xy_f = not xy_f + + points = list(g.iterator()) + self.assertEqual(expected, [p.positions for p in points]) + self.assertEqual(expected_idx, [p.indexes for p in points]) def test_two_dim_inner_alternates(self): wg = LineGenerator("w", "mm", 0, 1, 2) @@ -428,29 +456,42 @@ def test_double_spiral_scan(self): g.prepare() points = [] + l1s = [] + tl2 = [] + s_f = True - t_f = True - l2_f = True for l1 in line1.points["l1"]: sp = zip(spiral_s.points['s1'], spiral_s.points['s2']) sp = sp if s_f else list(sp)[::-1] s_f = not s_f - for (s1, s2) in sp: - tp = zip(spiral_t.points["t1"], spiral_t.points["t2"]) - tp = tp if t_f else list(tp)[::-1] - t_f = not t_f - for (t1, t2) in tp: - l2p = line2.points['l2'] if l2_f else line2.points['l2'][::-1] - l2_f = not l2_f - for l2 in l2p: - points.append((l2, t1, t2, s1, s2, l1)) + l1s += [(s1, s2, l1) for (s1, s2) in sp] + l2_f = True + for (t1, t2) in zip(spiral_t.points['t1'], spiral_t.points['t2']): + l2p = line2.points['l2'] if l2_f else line2.points['l2'][::-1] + l2_f = not l2_f + tl2 += [(l2, t1, t2) for l2 in l2p if l2*l2 + t1*t1 <= 1] + t_f = True + for (s1, s2, l1) in l1s: + inner = tl2 if t_f else tl2[::-1] + t_f = not t_f + points += [(l2, t1, t2, s1, s2, l1) for (l2, t1, t2) in inner + if s1*s1 + l1*l1 <= 1] + l1s_original = l1s + l1s = [(s1, s2, l1) for (s1, s2, l1) in l1s if s1*s1 + l1*l1 <= 1] expected = [{"l2":l2, "t1":t1, "t2":t2, "s1":s1, "s2":s2, "l1":l1} - for (l2, t1, t2, s1, s2, l1) in points if - s1*s1 + l1*l1 <= 1 and l2*l2 + t1*t1 <= 1] - - actual = [p.positions for p in list(g.iterator())] - self.assertEqual(expected, actual) + for (l2, t1, t2, s1, s2, l1) in points] + + expected_idx = [] + t_f = (l1s_original.index(l1s[0])) % 2 == 0 # t_f is False + for d1 in range_(len(l1s)): + expected_idx += [[d1, d2] for d2 in (range_(len(tl2)) if t_f else + range_(len(tl2) - 1, -1, -1))] + t_f = not t_f + + gpoints = list(g.iterator()) + self.assertEqual(expected, [p.positions for p in gpoints]) + self.assertEqual(expected_idx, [p.indexes for p in gpoints]) def test_mutators(self): mutator_1 = FixedDurationMutator(0.2) From deae585ec7128ec60df8a4af43f74e07331f48b6 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Tue, 15 Nov 2016 10:20:04 +0000 Subject: [PATCH 14/47] Use scisoftpy in Jython instead of numpy Allows the same numpy-like arrays to be used in Jython. Not everything is perfectly implemented and it's not as performant, but at least it'll work. --- scanpointgenerator/compat.py | 10 ++++++++++ scanpointgenerator/core/compoundgenerator.py | 7 +++---- scanpointgenerator/generators/linegenerator.py | 10 ++++------ scanpointgenerator/generators/lissajousgenerator.py | 7 +++---- scanpointgenerator/generators/spiralgenerator.py | 5 ++--- scanpointgenerator/rois/circular_roi.py | 1 - scanpointgenerator/rois/elliptical_roi.py | 1 - scanpointgenerator/rois/linear_roi.py | 2 +- scanpointgenerator/rois/point_roi.py | 2 -- scanpointgenerator/rois/polygonal_roi.py | 4 +--- scanpointgenerator/rois/rectangular_roi.py | 2 +- scanpointgenerator/rois/sector_roi.py | 5 +++-- tests/test_rois/test_circular_roi.py | 3 +-- tests/test_rois/test_elliptical_roi.py | 2 +- tests/test_rois/test_linear_roi.py | 2 +- tests/test_rois/test_point_roi.py | 2 +- tests/test_rois/test_polygonal_roi.py | 3 +-- tests/test_rois/test_rectangular_roi.py | 2 +- tests/test_rois/test_sector_roi.py | 2 +- 19 files changed, 35 insertions(+), 37 deletions(-) diff --git a/scanpointgenerator/compat.py b/scanpointgenerator/compat.py index 1d4596e..023dce4 100644 --- a/scanpointgenerator/compat.py +++ b/scanpointgenerator/compat.py @@ -1,5 +1,15 @@ +import os + try: range_ = xrange except NameError: # For Python3 range_ = range + + +if os.name == 'java': + import scisoftpy as numpy +else: + import numpy + +np = numpy diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index 865d6d6..f902ba9 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -1,8 +1,7 @@ import logging -import numpy as np from threading import Lock -from scanpointgenerator.compat import range_ +from scanpointgenerator.compat import range_, np from scanpointgenerator.core.generator import Generator from scanpointgenerator.core.point import Point from scanpointgenerator.core.excluder import Excluder @@ -187,7 +186,7 @@ def prepare(self): # Generate full index mask and "apply" ##### for dim in self.dimensions: - mask = np.full(dim["size"], True, dtype=np.bool) + mask = np.full(dim["size"], True, dtype=np.int8) for m in dim["masks"]: assert len(m["mask"]) * m["repeat"] * m["tile"] == len(mask), \ "Mask lengths are not consistent" @@ -199,7 +198,7 @@ def prepare(self): expanded = np.tile(expanded, int(m["tile"])) mask &= expanded dim["mask"] = mask - dim["indicies"] = np.flatnonzero(mask) + dim["indicies"] = np.nonzero(mask)[0] if len(dim["indicies"]) == 0: raise ValueError("Regions would exclude entire scan") repeat *= len(dim["indicies"]) diff --git a/scanpointgenerator/generators/linegenerator.py b/scanpointgenerator/generators/linegenerator.py index dfeade2..28f9563 100644 --- a/scanpointgenerator/generators/linegenerator.py +++ b/scanpointgenerator/generators/linegenerator.py @@ -1,6 +1,4 @@ -import numpy as np - -from scanpointgenerator.compat import range_ +from scanpointgenerator.compat import range_, np from scanpointgenerator.core import Generator from scanpointgenerator.core import Point @@ -89,11 +87,11 @@ def produce_points(self): lower_start = start - 0.5 * d / n lower_stop = stop - 0.5 * d / n self.points[axis_name] = np.linspace( - start, stop, self.num, dtype=np.float64) + float(start), float(stop), self.num) self.points_upper[axis_name] = np.linspace( - upper_start, upper_stop, self.num, dtype=np.float64) + float(upper_start), float(upper_stop), self.num) self.points_lower[axis_name] = np.linspace( - lower_start, lower_stop, self.num, dtype=np.float64) + float(lower_start), float(lower_stop), self.num) def iterator(self): for i in range_(self.num): diff --git a/scanpointgenerator/generators/lissajousgenerator.py b/scanpointgenerator/generators/lissajousgenerator.py index f7e7ee4..594aad9 100644 --- a/scanpointgenerator/generators/lissajousgenerator.py +++ b/scanpointgenerator/generators/lissajousgenerator.py @@ -1,7 +1,6 @@ import math as m -import numpy as np -from scanpointgenerator.compat import range_ +from scanpointgenerator.compat import range_, np from scanpointgenerator.core import Generator from scanpointgenerator.core import Point @@ -66,9 +65,9 @@ def _calc_arrays(self, offset): a, b = self.x_freq, self.y_freq d = self.phase_diff f = lambda t: y0 + A * np.sin(a * 2 * m.pi * (t+offset)/self.num + d) - x = np.fromfunction(f, (self.num,), dtype=np.float64) + x = f(np.arange(self.num)) f = lambda t: B * np.sin(b * 2 * m.pi * (t+offset)/self.num) - y = np.fromfunction(f, (self.num,), dtype=np.float64) + y = f(np.arange(self.num)) return x, y def produce_points(self): diff --git a/scanpointgenerator/generators/spiralgenerator.py b/scanpointgenerator/generators/spiralgenerator.py index b7e7523..1d76370 100644 --- a/scanpointgenerator/generators/spiralgenerator.py +++ b/scanpointgenerator/generators/spiralgenerator.py @@ -1,7 +1,6 @@ import math as m -import numpy as np -from scanpointgenerator.compat import range_ +from scanpointgenerator.compat import range_, np from scanpointgenerator.core import Generator from scanpointgenerator.core import Point @@ -63,7 +62,7 @@ def _calc_arrays(self, offset): size *= size size = int(size) + 1 # TODO: Why the +1 ??? phi_t = lambda t: k * np.sqrt(t + offset) - phi = np.fromfunction(phi_t, (size,), dtype=np.float64) + phi = phi_t(np.arange(size)) x = self.centre[0] + b * phi * np.sin(phi) y = self.centre[1] + b * phi * np.cos(phi) return x, y diff --git a/scanpointgenerator/rois/circular_roi.py b/scanpointgenerator/rois/circular_roi.py index 5a69a67..d32192b 100644 --- a/scanpointgenerator/rois/circular_roi.py +++ b/scanpointgenerator/rois/circular_roi.py @@ -1,6 +1,5 @@ from scanpointgenerator.core import ROI import math as m -import numpy as np @ROI.register_subclass("scanpointgenerator:roi/CircularROI:1.0") diff --git a/scanpointgenerator/rois/elliptical_roi.py b/scanpointgenerator/rois/elliptical_roi.py index 23073f6..65fa4a5 100644 --- a/scanpointgenerator/rois/elliptical_roi.py +++ b/scanpointgenerator/rois/elliptical_roi.py @@ -1,4 +1,3 @@ -import numpy as np from math import cos, sin from scanpointgenerator.core import ROI diff --git a/scanpointgenerator/rois/linear_roi.py b/scanpointgenerator/rois/linear_roi.py index 84d4b1e..ae6fac1 100644 --- a/scanpointgenerator/rois/linear_roi.py +++ b/scanpointgenerator/rois/linear_roi.py @@ -1,7 +1,7 @@ from math import cos, sin -import numpy as np from scanpointgenerator.core import ROI +from scanpointgenerator.compat import np @ROI.register_subclass("scanpointgenerator:roi/LinearROI:1.0") diff --git a/scanpointgenerator/rois/point_roi.py b/scanpointgenerator/rois/point_roi.py index 6b4ac9b..0922012 100644 --- a/scanpointgenerator/rois/point_roi.py +++ b/scanpointgenerator/rois/point_roi.py @@ -1,5 +1,3 @@ -import numpy as np - from scanpointgenerator.core import ROI diff --git a/scanpointgenerator/rois/polygonal_roi.py b/scanpointgenerator/rois/polygonal_roi.py index 60a5654..31a5a49 100644 --- a/scanpointgenerator/rois/polygonal_roi.py +++ b/scanpointgenerator/rois/polygonal_roi.py @@ -1,7 +1,5 @@ -import numpy as np - from scanpointgenerator.core import ROI -from scanpointgenerator.compat import range_ +from scanpointgenerator.compat import range_, np @ROI.register_subclass("scanpointgenerator:roi/PolygonalROI:1.0") diff --git a/scanpointgenerator/rois/rectangular_roi.py b/scanpointgenerator/rois/rectangular_roi.py index 6a8bcfc..4dcd5dc 100644 --- a/scanpointgenerator/rois/rectangular_roi.py +++ b/scanpointgenerator/rois/rectangular_roi.py @@ -1,7 +1,7 @@ -import numpy as np from math import cos, sin from scanpointgenerator.core import ROI +from scanpointgenerator.compat import np @ROI.register_subclass("scanpointgenerator:roi/RectangularROI:1.0") diff --git a/scanpointgenerator/rois/sector_roi.py b/scanpointgenerator/rois/sector_roi.py index e355dd7..f646a08 100644 --- a/scanpointgenerator/rois/sector_roi.py +++ b/scanpointgenerator/rois/sector_roi.py @@ -1,6 +1,7 @@ -from scanpointgenerator.core import ROI from math import hypot, atan2, pi -import numpy as np + +from scanpointgenerator.core import ROI +from scanpointgenerator.compat import np @ROI.register_subclass("scanpointgenerator:roi/SectorROI:1.0") diff --git a/tests/test_rois/test_circular_roi.py b/tests/test_rois/test_circular_roi.py index 15a04f1..a6dc5c7 100644 --- a/tests/test_rois/test_circular_roi.py +++ b/tests/test_rois/test_circular_roi.py @@ -3,10 +3,9 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "..")) import unittest -import numpy as np - from test_util import ScanPointGeneratorTest from scanpointgenerator.rois.circular_roi import CircularROI +from scanpointgenerator.compat import np class InitTest(unittest.TestCase): diff --git a/tests/test_rois/test_elliptical_roi.py b/tests/test_rois/test_elliptical_roi.py index f21621d..1f750b4 100644 --- a/tests/test_rois/test_elliptical_roi.py +++ b/tests/test_rois/test_elliptical_roi.py @@ -3,10 +3,10 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "..")) import unittest from math import pi -import numpy as np from test_util import ScanPointGeneratorTest from scanpointgenerator.rois.elliptical_roi import EllipticalROI +from scanpointgenerator.compat import np class EllipticalROITest(unittest.TestCase): diff --git a/tests/test_rois/test_linear_roi.py b/tests/test_rois/test_linear_roi.py index a68895d..96dc699 100644 --- a/tests/test_rois/test_linear_roi.py +++ b/tests/test_rois/test_linear_roi.py @@ -2,11 +2,11 @@ import sys sys.path.append(os.path.join(os.path.dirname(__file__), "..")) import unittest -import numpy as np from math import pi, sqrt from test_util import ScanPointGeneratorTest from scanpointgenerator.rois.linear_roi import LinearROI +from scanpointgenerator.compat import np class LinearROIInitTest(unittest.TestCase): diff --git a/tests/test_rois/test_point_roi.py b/tests/test_rois/test_point_roi.py index 6cec7bb..2eaeaa1 100644 --- a/tests/test_rois/test_point_roi.py +++ b/tests/test_rois/test_point_roi.py @@ -2,10 +2,10 @@ import sys sys.path.append(os.path.join(os.path.dirname(__file__), "..")) import unittest -import numpy as np from test_util import ScanPointGeneratorTest from scanpointgenerator.rois.point_roi import PointROI +from scanpointgenerator.compat import np class PointROITest(unittest.TestCase): diff --git a/tests/test_rois/test_polygonal_roi.py b/tests/test_rois/test_polygonal_roi.py index 42aeb0e..882ffda 100644 --- a/tests/test_rois/test_polygonal_roi.py +++ b/tests/test_rois/test_polygonal_roi.py @@ -3,10 +3,9 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "..")) import unittest -import numpy as np - from test_util import ScanPointGeneratorTest from scanpointgenerator.rois.polygonal_roi import PolygonalROI +from scanpointgenerator.compat import np class PolygonalROITests(unittest.TestCase): diff --git a/tests/test_rois/test_rectangular_roi.py b/tests/test_rois/test_rectangular_roi.py index 381d978..2f1ff0c 100644 --- a/tests/test_rois/test_rectangular_roi.py +++ b/tests/test_rois/test_rectangular_roi.py @@ -2,11 +2,11 @@ import sys sys.path.append(os.path.join(os.path.dirname(__file__), "..")) import unittest -import numpy as np from math import pi from test_util import ScanPointGeneratorTest from scanpointgenerator.rois.rectangular_roi import RectangularROI +from scanpointgenerator.compat import np class InitTest(unittest.TestCase): diff --git a/tests/test_rois/test_sector_roi.py b/tests/test_rois/test_sector_roi.py index 4e77d3f..8d0254f 100644 --- a/tests/test_rois/test_sector_roi.py +++ b/tests/test_rois/test_sector_roi.py @@ -3,10 +3,10 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "..")) import unittest from math import pi -import numpy as np from test_util import ScanPointGeneratorTest from scanpointgenerator.rois.sector_roi import SectorROI +from scanpointgenerator.compat import np class SectorInitTest(unittest.TestCase): From f2bf5b91ca0c2f2e59addeb684e080e6df8bf217 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Tue, 15 Nov 2016 10:38:49 +0000 Subject: [PATCH 15/47] Use in-place array operators in roi mask calculations It makes the code less clear, but saves on memory as new return arrays do not have to be created. This is particularly significant when it comes to Jython as the JVM may be memory constrained. --- scanpointgenerator/core/compoundgenerator.py | 7 ++++++- scanpointgenerator/rois/circular_roi.py | 10 +++++++--- scanpointgenerator/rois/elliptical_roi.py | 9 ++++++--- scanpointgenerator/rois/point_roi.py | 9 ++++++--- scanpointgenerator/rois/rectangular_roi.py | 6 ++++-- scanpointgenerator/rois/sector_roi.py | 6 ++++-- tests/test_rois/test_point_roi.py | 5 ++--- 7 files changed, 35 insertions(+), 17 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index f902ba9..d0247ab 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -151,7 +151,12 @@ def prepare(self): elif gen_1 is not gen_2: points_1 = np.repeat(points_1, gen_2.num) points_2 = np.tile(points_2, gen_1.num) - # else not needed; do nothing if gen_1 is gen_2 and not alternating + else: + # copy the points arrays anyway so the regions can + # safely perform any array operations in place + # this is advantageous in the cases above + points_1 = np.copy(points_1) + points_2 = np.copy(points_2) if axis_1 == excluder.scannables[0]: diff --git a/scanpointgenerator/rois/circular_roi.py b/scanpointgenerator/rois/circular_roi.py index d32192b..d34a857 100644 --- a/scanpointgenerator/rois/circular_roi.py +++ b/scanpointgenerator/rois/circular_roi.py @@ -22,12 +22,16 @@ def contains_point(self, point): return True def mask_points(self, points): - x = points[0] - self.centre[0] - y = points[1] - self.centre[1] + x = points[0] + x -= self.centre[0] + y = points[1] + y -= self.centre[1] + # use in place operations as much as possible (to save array creation) x *= x y *= y + x += y r2 = self.radius * self.radius - return x + y <= r2 + return x <= r2 def to_dict(self): """Convert object attributes into a dictionary""" diff --git a/scanpointgenerator/rois/elliptical_roi.py b/scanpointgenerator/rois/elliptical_roi.py index 65fa4a5..4bf145c 100644 --- a/scanpointgenerator/rois/elliptical_roi.py +++ b/scanpointgenerator/rois/elliptical_roi.py @@ -30,8 +30,10 @@ def contains_point(self, point): return (x * x) / (rx * rx) + (y * y) / (ry * ry) <= 1 def mask_points(self, points): - x = points[0] - self.centre[0] - y = points[1] - self.centre[1] + x = points[0] + x -= self.centre[0] + y = points[1] + y -= self.centre[1] if self.angle != 0: phi = -self.angle tx = x * cos(phi) - y * sin(phi) @@ -44,7 +46,8 @@ def mask_points(self, points): x /= rx2 y *= y y /= ry2 - return x + y <= 1 + x += y + return x <= 1 def to_dict(self): d = super(EllipticalROI, self).to_dict() diff --git a/scanpointgenerator/rois/point_roi.py b/scanpointgenerator/rois/point_roi.py index 0922012..290a125 100644 --- a/scanpointgenerator/rois/point_roi.py +++ b/scanpointgenerator/rois/point_roi.py @@ -16,11 +16,14 @@ def contains_point(self, point, epsilon=0): return x * x + y * y <= epsilon * epsilon def mask_points(self, points, epsilon=0): - x = points[0] - self.point[0] - y = points[1] - self.point[1] + x = points[0] + x -= self.point[0] + y = points[1] + y -= self.point[1] x *= x y *= y - return (x + y) <= epsilon * epsilon + x += y + return x <= epsilon * epsilon def to_dict(self): d = super(PointROI, self).to_dict() diff --git a/scanpointgenerator/rois/rectangular_roi.py b/scanpointgenerator/rois/rectangular_roi.py index 4dcd5dc..96c83fe 100644 --- a/scanpointgenerator/rois/rectangular_roi.py +++ b/scanpointgenerator/rois/rectangular_roi.py @@ -32,8 +32,10 @@ def contains_point(self, point): and (y >= 0 and y < self.height) def mask_points(self, points): - x = points[0] - self.start[0] - y = points[1] - self.start[1] + x = points[0] + x -= self.start[0] + y = points[1] + y -= self.start[1] if self.angle != 0: phi = -self.angle rx = x * cos(phi) - y * sin(phi) diff --git a/scanpointgenerator/rois/sector_roi.py b/scanpointgenerator/rois/sector_roi.py index f646a08..cba0f8c 100644 --- a/scanpointgenerator/rois/sector_roi.py +++ b/scanpointgenerator/rois/sector_roi.py @@ -49,8 +49,10 @@ def contains_point(self, point): return theta <= sweep def mask_points(self, points): - x = points[0] - self.centre[0] - y = points[1] - self.centre[1] + x = points[0] + y = points[1] + x -= self.centre[0] + y -= self.centre[1] r2 = (np.square(x) + np.square(y)) phi_0, phi_1 = self.constrain_angles(self.angles) # phi_0 <= phi_1, phi_0 in [0, 2pi), phi_1 < 4pi diff --git a/tests/test_rois/test_point_roi.py b/tests/test_rois/test_point_roi.py index 2eaeaa1..c479286 100644 --- a/tests/test_rois/test_point_roi.py +++ b/tests/test_rois/test_point_roi.py @@ -36,12 +36,11 @@ def test_mask_points(self): roi = PointROI([1, 2]) px = [1, 0, 1+1e-15, 1, 1] py = [2, 0, 2, 2+1e-15, 2+1e-14] - points = [np.array(px), np.array(py)] expected = [True, False, True, True, False] - mask = roi.mask_points(points, 2e-15) + mask = roi.mask_points([np.array(px), np.array(py)], 2e-15) self.assertEqual(expected, mask.tolist()) - mask = roi.mask_points(points, 0) + mask = roi.mask_points([np.array(px), np.array(py)], 0) expected = [True, False, False, False, False] self.assertEqual(expected, mask.tolist()) From f6938bf20b9f902302c85f3618c74a59a80115a8 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Mon, 21 Nov 2016 13:04:18 +0000 Subject: [PATCH 16/47] Permit omission of the alternate setting on outermost generator --- scanpointgenerator/core/compoundgenerator.py | 12 +++++++----- tests/test_core/test_compoundgenerator.py | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index d0247ab..102f34e 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -87,11 +87,6 @@ def prepare(self): dim_2 = [i for i in self.dimensions if axis_2 in i["axes"]][0] dim_diff = self.dimensions.index(dim_1) \ - self.dimensions.index(dim_2) - if dim_1["alternate"] != dim_2["alternate"]: - raise ValueError( - "Generators tied by regions must have the same " \ - "alternate_direction setting") - # merge "inner" into "outer" if dim_diff < -1 or dim_diff > 1: raise ValueError( "Excluders must be defined on axes that are adjacent in " \ @@ -99,6 +94,12 @@ def prepare(self): if dim_diff == 1: dim_1, dim_2 = dim_2, dim_1 dim_diff = -1 + if dim_1["alternate"] != dim_2["alternate"] \ + and dim_1 is not self.dimensions[0]: + raise ValueError( + "Generators tied by regions must have the same " \ + "alternate_direction setting") + # merge "inner" into "outer" if dim_diff == -1: # dim_1 is "outer" - preserves axis ordering @@ -121,6 +122,7 @@ def prepare(self): dim_1["axes"] += dim_2["axes"] dim_1["generators"] += dim_2["generators"] dim_1["size"] *= dim_2["size"] + dim_1["alternate"] |= dim_2["alternate"] self.dimensions.remove(dim_2) dim = dim_1 diff --git a/tests/test_core/test_compoundgenerator.py b/tests/test_core/test_compoundgenerator.py index b15c3cf..c1c8cee 100644 --- a/tests/test_core/test_compoundgenerator.py +++ b/tests/test_core/test_compoundgenerator.py @@ -288,7 +288,7 @@ def test_triple_alternating_linked_gen(self): self.assertEqual(e, a) def test_alternating_regions_2(self): - z = LineGenerator("z", "mm", 1, 5, 5, True) + z = LineGenerator("z", "mm", 1, 5, 5) y = LineGenerator("y", "mm", 1, 5, 5, True) x = LineGenerator("x", "mm", 1, 5, 5, True) r1 = RectangularROI([2, 2], 2, 2) @@ -399,7 +399,7 @@ def test_horrible_scan(self): lissajous = LissajousGenerator( ["j1", "j2"], "mm", {"centre":[-0.5, 0.7], "width":2, "height":3.5}, - 7, 100, True) + 7, 100) line2 = LineGenerator(["l2"], "mm", -3, 3, 7, True) line1 = LineGenerator(["l1"], "mm", -1, 2, 5, True) spiral = SpiralGenerator(["s1", "s2"], "mm", [1, 2], 5, 2.5, True) @@ -445,7 +445,7 @@ def test_horrible_scan(self): self.assertEqual(e, a) def test_double_spiral_scan(self): - line1 = LineGenerator(["l1"], "mm", -1, 2, 5, True) + line1 = LineGenerator(["l1"], "mm", -1, 2, 5) spiral_s = SpiralGenerator(["s1", "s2"], "mm", [1, 2], 5, 2.5, True) spiral_t = SpiralGenerator(["t1", "t2"], "mm", [0, 0], 5, 2.5, True) line2 = LineGenerator(["l2"], "mm", -1, 2, 5, True) From 7aabf9f315ec2d2c110d41c5c0a612f40e6276ed Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Mon, 21 Nov 2016 15:04:04 +0000 Subject: [PATCH 17/47] Change RectangularROI to include points on both sides of boundary i.e "x >= 0 && x < size" becomes "x >= 0 && x <= size" --- scanpointgenerator/rois/rectangular_roi.py | 8 ++++---- tests/test_core/test_compoundgenerator.py | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/scanpointgenerator/rois/rectangular_roi.py b/scanpointgenerator/rois/rectangular_roi.py index 96c83fe..5ab2615 100644 --- a/scanpointgenerator/rois/rectangular_roi.py +++ b/scanpointgenerator/rois/rectangular_roi.py @@ -28,8 +28,8 @@ def contains_point(self, point): ry = x * sin(phi) + y * cos(phi) x = rx y = ry - return (x >= 0 and x < self.width) \ - and (y >= 0 and y < self.height) + return (x >= 0 and x <= self.width) \ + and (y >= 0 and y <= self.height) def mask_points(self, points): x = points[0] @@ -42,8 +42,8 @@ def mask_points(self, points): ry = x * sin(phi) + y * cos(phi) x = rx y = ry - mask_x = np.logical_and(x >= 0, x < self.width) - mask_y = np.logical_and(y >= 0, y < self.height) + mask_x = np.logical_and(x >= 0, x <= self.width) + mask_y = np.logical_and(y >= 0, y <= self.height) return np.logical_and(mask_x, mask_y) def to_dict(self): diff --git a/tests/test_core/test_compoundgenerator.py b/tests/test_core/test_compoundgenerator.py index c1c8cee..f11c038 100644 --- a/tests/test_core/test_compoundgenerator.py +++ b/tests/test_core/test_compoundgenerator.py @@ -342,8 +342,8 @@ def test_alternating_complex(self): {"t":float(t), "z":float(z), "y":float(y), "x":float(x)}) expected = [p for p in expected if - (p["y"] >= 3 and p["y"] < 5 and p["x"] >= 3 and p["x"] < 5) and - (p["z"] >= 3 and p["z"] < 5 and p["y"] >= 3 and p["y"] < 5)] + (p["y"] >= 3 and p["y"] <= 5 and p["x"] >= 3 and p["x"] <= 5) and + (p["z"] >= 3 and p["z"] <= 5 and p["y"] >= 3 and p["y"] <= 5)] self.assertEqual(expected, points) def test_line_spiral(self): @@ -584,8 +584,8 @@ def test_double_mask_alternating_spiral(self): p = [] for z in range_(0, 5): p += [(x, y, z) for (x, y) in (xy if z % 2 == 0 else xy[::-1])] - expected = [x >= -2 and x < 1 and y >= -2 and y < 2 - and z >= 0 and z < 3 for (x, y, z) in p] + expected = [x >= -2 and x <= 1 and y >= -2 and y <= 2 + and z >= 0 and z <= 3 for (x, y, z) in p] actual = g.dimensions[0]["mask"].tolist() self.assertEqual(expected, actual) @@ -600,8 +600,8 @@ def test_double_mask_spiral(self): g.prepare() p = list(zip(g.axes_points['x'], g.axes_points['y'])) p = [(x, y, z) for z in range_(0, 5) for (x, y) in p] - expected = [x >= -2 and x < 1 and y >= -2 and y < 2 - and z >= 0 and z < 3 for (x, y, z) in p] + expected = [x >= -2 and x <= 1 and y >= -2 and y <= 2 + and z >= 0 and z <= 3 for (x, y, z) in p] actual = g.dimensions[0]["mask"].tolist() self.assertEqual(expected, actual) From aa400d9d81e487bdb04e7d2a1293d061e154a308 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Mon, 21 Nov 2016 14:36:54 +0000 Subject: [PATCH 18/47] Do not merge dimensions with rectangular regions over line generators This changes the alternating case slightly (may start in a different direction) --- scanpointgenerator/core/compoundgenerator.py | 55 ++++++++++++++++--- .../generators/linegenerator.py | 3 +- tests/test_core/test_compoundgenerator.py | 38 +++++++++---- 3 files changed, 78 insertions(+), 18 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index 102f34e..a2fa441 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -6,6 +6,8 @@ from scanpointgenerator.core.point import Point from scanpointgenerator.core.excluder import Excluder from scanpointgenerator.core.mutator import Mutator +from scanpointgenerator.rois import RectangularROI +from scanpointgenerator.generators import LineGenerator @Generator.register_subclass("scanpointgenerator:generator/CompoundGenerator:1.0") @@ -47,7 +49,46 @@ def __init__(self, generators, excluders, mutators): def prepare(self): self.num = 1 self.dimensions = [] - for generator in self.generators: + # we're going to mutate these structures + excluders = list(self.excluders) + generators = list(self.generators) + + # special case if we have rectangular regions on line generators + # we should restrict the resulting grid rather than merge dimensions + # this changes the alternating case a little (without doing this, we + # may have started in reverse direction) + for rect in [r for r in excluders \ + if isinstance(r.roi, RectangularROI) and r.roi.angle == 0]: + axis_1, axis_2 = rect.scannables[0], rect.scannables[1] + gen_1 = [g for g in generators if axis_1 in g.axes][0] + gen_2 = [g for g in generators if axis_2 in g.axes][0] + if gen_1 is gen_2: + continue + if isinstance(gen_1, LineGenerator) \ + and isinstance(gen_2, LineGenerator): + gen_1.produce_points() + gen_2.produce_points() + valid = np.full(gen_1.num, True, dtype=np.int8) + valid &= \ + gen_1.points[axis_1] <= rect.roi.width + rect.roi.start[0] + valid &= gen_1.points[axis_1] >= rect.roi.start[0] + points_1 = gen_1.points[axis_1][valid.astype(np.bool)] + valid = np.full(gen_2.num, True, dtype=np.int8) + valid &= \ + gen_2.points[axis_2] <= rect.roi.height + rect.roi.start[1] + valid &= gen_2.points[axis_2] >= rect.roi.start[1] + points_2 = gen_2.points[axis_2][valid.astype(np.bool)] + new_gen1 = LineGenerator( + gen_1.name, gen_1.units, points_1[0], points_1[-1], + len(points_1), gen_1.alternate_direction) + new_gen2 = LineGenerator( + gen_2.name, gen_2.units, points_2[0], points_2[-1], + len(points_2), gen_2.alternate_direction) + generators[generators.index(gen_1)] = new_gen1 + generators[generators.index(gen_2)] = new_gen2 + excluders.remove(rect) + + for generator in generators: generator.produce_points() self.axes_points.update(generator.points) self.axes_points_lower.update(generator.points_lower) @@ -63,13 +104,13 @@ def prepare(self): "alternate":generator.alternate_direction} self.dimensions.append(dim) - for excluder in self.excluders: + for excluder in excluders: axis_1, axis_2 = excluder.scannables # ensure axis_1 is "outer" axis (if separate generators) - gen_1 = [g for g in self.generators if axis_1 in g.axes][0] - gen_2 = [g for g in self.generators if axis_2 in g.axes][0] - gen_diff = self.generators.index(gen_1) \ - - self.generators.index(gen_2) + gen_1 = [g for g in generators if axis_1 in g.axes][0] + gen_2 = [g for g in generators if axis_2 in g.axes][0] + gen_diff = generators.index(gen_1) \ + - generators.index(gen_2) if gen_diff < -1 or gen_diff > 1: raise ValueError( "Excluders must be defined on axes that are adjacent in " \ @@ -184,7 +225,7 @@ def prepare(self): tile *= g.num m = {"repeat":repeat, "tile":tile, "mask":mask} dim["masks"].append(m) - # end for excluder in self.excluders + # end for excluder in excluders ##### tile = 1 diff --git a/scanpointgenerator/generators/linegenerator.py b/scanpointgenerator/generators/linegenerator.py index 28f9563..34e262b 100644 --- a/scanpointgenerator/generators/linegenerator.py +++ b/scanpointgenerator/generators/linegenerator.py @@ -35,6 +35,7 @@ def __init__(self, name, units, start, stop, num, alternate_direction=False): self.points = None self.points_lower = None self.points_upper = None + self.units = units if len(self.name) != len(set(self.name)): raise ValueError("Axis names cannot be duplicated; given %s" % @@ -115,7 +116,7 @@ def to_dict(self): d = dict() d['typeid'] = self.typeid d['name'] = self.name - d['units'] = list(self.position_units.values())[0] + d['units'] = self.units d['start'] = self.start d['stop'] = self.stop d['num'] = self.num diff --git a/tests/test_core/test_compoundgenerator.py b/tests/test_core/test_compoundgenerator.py index f11c038..6804884 100644 --- a/tests/test_core/test_compoundgenerator.py +++ b/tests/test_core/test_compoundgenerator.py @@ -150,7 +150,7 @@ def test_alternating_three_axis(self): def test_alternating_with_region(self): y = LineGenerator("y", "mm", 1, 5, 5, True) x = LineGenerator("x", "mm", 1, 5, 5, True) - r1 = RectangularROI([2, 2], 2, 2) + r1 = CircularROI([3, 3], 1.5) e1 = Excluder(r1, ["y", "x"]) g = CompoundGenerator([y, x], [e1], []) g.prepare() @@ -162,7 +162,7 @@ def test_alternating_with_region(self): for x in r: expected.append({"y":float(y), "x":float(x)}) expected = [p for p in expected if - (p["y"] >= 2 and p["y"] < 4 and p["x"] >= 2 and p["x"] < 4)] + ((p["x"]-3)**2 + (p["y"]-3)**2 <= 1.5**2)] expected_idx = [[xy] for xy in range_(len(expected))] points = list(g.iterator()) self.assertEqual(expected, [p.positions for p in points]) @@ -172,7 +172,7 @@ def test_inner_alternating(self): z = LineGenerator("z", "mm", 1, 5, 5) y = LineGenerator("y", "mm", 1, 5, 5, alternate_direction=True) x = LineGenerator("x", "mm", 1, 5, 5, alternate_direction=True) - r1 = RectangularROI([2, 2], 2, 2) + r1 = CircularROI([3, 3], 1.5) e1 = Excluder(r1, ["x", "y"]) g = CompoundGenerator([z, y, x], [e1], []) g.prepare() @@ -181,7 +181,7 @@ def test_inner_alternating(self): x_f = True for y in range_(1, 6): for x in (range_(1, 6) if x_f else range(5, 0, -1)): - if x >= 2 and x < 4 and y >= 2 and y < 4: + if (x-3)**2 + (y-3)**2 <= 1.5**2: xy_expected.append((x, y)) x_f = not x_f xy_f = True @@ -291,7 +291,7 @@ def test_alternating_regions_2(self): z = LineGenerator("z", "mm", 1, 5, 5) y = LineGenerator("y", "mm", 1, 5, 5, True) x = LineGenerator("x", "mm", 1, 5, 5, True) - r1 = RectangularROI([2, 2], 2, 2) + r1 = CircularROI([3, 3], 1.5) e1 = Excluder(r1, ["x", "y"]) e2 = Excluder(r1, ["z", "y"]) g = CompoundGenerator([z, y, x], [e1, e2], []) #20 points @@ -310,8 +310,8 @@ def test_alternating_regions_2(self): expected.append({"x":float(x), "y":float(y), "z":float(z)}) expected = [p for p in expected - if p["x"] >= 2 and p["x"] < 4 and p["y"] >= 2 and p["y"] < 4 - and p["z"] >= 2 and p["z"] < 4] + if (p["x"]-3)**2 + (p["y"]-3)**2 <= 1.5**2 + and (p["z"]-3)**2 + (p["y"]-3)**2 <= 1.5**2] self.assertEqual(expected, actual) def test_alternating_complex(self): @@ -507,6 +507,24 @@ def test_mutators(self): x_pos += 1 self.assertEqual(6, x_pos) + def test_grid_rect_region(self): + xg = LineGenerator("x", "mm", 1, 10, 10) + yg = LineGenerator("y", "mm", 1, 10, 10) + r = RectangularROI([3, 3], 6, 6) + e = Excluder(r, ["x", "y"]) + g = CompoundGenerator([yg, xg], [e], []) + g.prepare() + self.assertEqual(49, g.num) + p = g.get_point(8) + self.assertEqual([1, 1], p.indexes) + self.assertEqual((4, 4), (p.positions['y'], p.positions['x'])) + p = g.get_point(48) + self.assertEqual([6, 6], p.indexes) + self.assertEqual((9, 9), (p.positions['y'], p.positions['x'])) + p = g.get_point(14) + self.assertEqual([2, 0], p.indexes) + self.assertEqual((5, 3), (p.positions['y'], p.positions['x'])) + class CompoundGeneratorInternalDataTests(ScanPointGeneratorTest): """Tests on datastructures internal to CompoundGenerator""" @@ -640,7 +658,7 @@ def test_complex_masks(self): zg = LineGenerator("z", "mm", 0, 4, 5, alternate_direction=True) yg = LineGenerator("y", "mm", 1, 5, 5, alternate_direction=True) xg = LineGenerator("x", "mm", 2, 6, 5, alternate_direction=True) - r1 = RectangularROI([3., 3.], 2., 2.) + r1 = CircularROI([4., 4.], 1.5) e1 = Excluder(r1, ["y", "x"]) e2 = Excluder(r1, ["z", "y"]) g = CompoundGenerator([tg, zg, yg, xg], [e1, e2], []) @@ -654,8 +672,8 @@ def test_complex_masks(self): for z in range_(0, 5): for y in (range_(1, 6) if iy % 2 == 0 else range_(5, 0, -1)): for x in (range_(2, 7) if ix % 2 == 0 else range_(6, 1, -1)): - xyz_mask.append( x >= 3 and x < 5 and y >= 3 and y < 5 - and z >= 3 and z < 5 ) + xyz_mask.append( (x-4)**2 + (y-4)**2 <= 1.5**2 \ + and (y-4)**2 + (z-4)**2 <= 1.5**2) xyz.append((x, y, z)) ix += 1 iy += 1 From 831dbd7dcabba2d311208c61ad0764421af025ad Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Mon, 21 Nov 2016 16:43:22 +0000 Subject: [PATCH 19/47] Handle single point case in LineGenerator --- .../generators/linegenerator.py | 29 +++++++++++-------- tests/test_generators/test_linegenerator.py | 7 +++++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/scanpointgenerator/generators/linegenerator.py b/scanpointgenerator/generators/linegenerator.py index 34e262b..ae54ff9 100644 --- a/scanpointgenerator/generators/linegenerator.py +++ b/scanpointgenerator/generators/linegenerator.py @@ -81,18 +81,23 @@ def produce_points(self): start = self.start[axis] stop = self.stop[axis] d = stop - start - n = self.num - 1. - s = d / n - upper_start = start + 0.5 * d / n - upper_stop = stop + 0.5 * d / n - lower_start = start - 0.5 * d / n - lower_stop = stop - 0.5 * d / n - self.points[axis_name] = np.linspace( - float(start), float(stop), self.num) - self.points_upper[axis_name] = np.linspace( - float(upper_start), float(upper_stop), self.num) - self.points_lower[axis_name] = np.linspace( - float(lower_start), float(lower_stop), self.num) + if self.num == 1: + self.points[axis_name] = np.array([start]) + self.points_upper[axis_name] = np.array([start + 0.5 * d]) + self.points_lower[axis_name] = np.array([start - 0.5 * d]) + else: + n = self.num - 1. + s = d / n + upper_start = start + 0.5 * d / n + upper_stop = stop + 0.5 * d / n + lower_start = start - 0.5 * d / n + lower_stop = stop - 0.5 * d / n + self.points[axis_name] = np.linspace( + float(start), float(stop), self.num) + self.points_upper[axis_name] = np.linspace( + float(upper_start), float(upper_stop), self.num) + self.points_lower[axis_name] = np.linspace( + float(lower_start), float(lower_stop), self.num) def iterator(self): for i in range_(self.num): diff --git a/tests/test_generators/test_linegenerator.py b/tests/test_generators/test_linegenerator.py index d4efc1e..7c94fb5 100644 --- a/tests/test_generators/test_linegenerator.py +++ b/tests/test_generators/test_linegenerator.py @@ -28,6 +28,13 @@ def test_array_positions(self): self.assertEqual(lower, self.g.points_lower['x'].tolist()) self.assertEqual(upper, self.g.points_upper['x'].tolist()) + def test_single_point(self): + g = LineGenerator("x", "mm", 1.0, 4.0, 1) + g.produce_points() + self.assertEqual([1.0], g.points["x"].tolist()) + self.assertEqual([2.5], g.points_upper["x"].tolist()) + self.assertEqual([-0.5], g.points_lower["x"].tolist()) + def test_iterator(self): positions = [1.0, 3.0, 5.0, 7.0, 9.0] lower = [0.0, 2.0, 4.0, 6.0, 8.0] From ec085a0e7a6cfefed80a0323e5631f2891e9e4d4 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Tue, 22 Nov 2016 17:53:52 +0000 Subject: [PATCH 20/47] Fix point bounds in compound generator and add tests. Did not correctly handle reverse directions. Tests only check for "inner-most" generator as that is only generator that requires them. Currently bounds are provided for all generators and that should be fixed. The logic in get_point is getting convoluted and needs some love. --- scanpointgenerator/core/compoundgenerator.py | 14 ++++++- .../generators/linegenerator.py | 2 +- tests/test_core/test_compoundgenerator.py | 39 ++++++++++++++----- tests/test_generators/test_linegenerator.py | 10 +++++ 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index a2fa441..90cf76d 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -305,12 +305,22 @@ def get_point(self, n): j = k // self.generator_dim_scaling[g]["repeat"] gr = j // g.num j %= g.num + bounds_reverse = False if dim["alternate"] and g is not dim["generators"][0] and gr % 2 == 1: + # the top level generator's direction is handled by + # the fact that the reverse direction was appended j = g.num - j - 1 + bounds_reverse = True + # seriously messy logic now + bounds_reverse |= dim_reverse and g is dim["generators"][0] for axis in g.axes: p.positions[axis] = g.points[axis][j] - p.lower[axis] = g.points_lower[axis][j] - p.upper[axis] = g.points_upper[axis][j] + lower = g.points_upper[axis] if bounds_reverse \ + else g.points_lower[axis] + upper = g.points_lower[axis] if bounds_reverse \ + else g.points_upper[axis] + p.lower[axis] = lower[j] + p.upper[axis] = upper[j] return p def to_dict(self): diff --git a/scanpointgenerator/generators/linegenerator.py b/scanpointgenerator/generators/linegenerator.py index ae54ff9..8c6f3c1 100644 --- a/scanpointgenerator/generators/linegenerator.py +++ b/scanpointgenerator/generators/linegenerator.py @@ -21,7 +21,7 @@ def __init__(self, name, units, start, stop, num, alternate_direction=False): units (str): The scannable units. E.g. "mm" start (float/list(float)): The first position to be generated. e.g. 1.0 or [1.0, 2.0] - stop (float or list(float)): The first position to be generated. + stop (float or list(float)): The final position to be generated. e.g. 5.0 or [5.0, 10.0] num (int): The number of points to generate. E.g. 5 alternate_direction(bool): Specifier to reverse direction if diff --git a/tests/test_core/test_compoundgenerator.py b/tests/test_core/test_compoundgenerator.py index 6804884..5f19f44 100644 --- a/tests/test_core/test_compoundgenerator.py +++ b/tests/test_core/test_compoundgenerator.py @@ -115,37 +115,50 @@ def test_alternating_simple(self): g.prepare() expected = [] expected_idx = [] + expected_lower = [] + expected_upper = [] for y in range_(1, 6): - r = range_(1, 6) if y % 2 == 1 else range_(5, 0, -1) + x_f = y % 2 == 1 + r = range_(1, 6) if x_f else range_(5, 0, -1) for x in r: expected.append({"y":float(y), "x":float(x)}) expected_idx.append([y - 1, x - 1]) + expected_lower.append(x + (-0.5 if x_f else 0.5)) + expected_upper.append(x + (0.5 if x_f else -0.5)) points = list(g.iterator()) self.assertEqual(expected, [p.positions for p in points]) self.assertEqual(expected_idx, [p.indexes for p in points]) + self.assertEqual(expected_lower, [p.lower["x"] for p in points]) + self.assertEqual(expected_upper, [p.upper["x"] for p in points]) def test_alternating_three_axis(self): z = LineGenerator("z", "mm", 1, 2, 2) y = LineGenerator("y", "mm", 1, 2, 2, True) - x = LineGenerator("x", "mm", 1, 3, 3, True) + x = LineGenerator("x", "mm", 3, 1, 3, True) g = CompoundGenerator([z, y, x], [], []) g.prepare() expected = [] expected_idx = [] + expected_lower = [] + expected_upper = [] y_f = True x_f = True for z in range_(1, 3): y_r = range_(1, 3) if y_f else range_(2, 0, -1) y_f = not y_f for y in y_r: - x_r = range_(1, 4) if x_f else range_(3, 0, -1) - x_f = not x_f + x_r = range_(3, 0, -1) if x_f else range_(1, 4) for x in x_r: expected.append({"x":float(x), "y":float(y), "z":float(z)}) - expected_idx.append([z-1, y-1, x-1]) + expected_idx.append([z-1, y-1, 3-x]) + expected_lower.append(x + (0.5 if x_f else -0.5)) + expected_upper.append(x + (-0.5 if x_f else 0.5)) + x_f = not x_f points = list(g.iterator()) self.assertEqual(expected, [p.positions for p in points]) self.assertEqual(expected_idx, [p.indexes for p in points]) + self.assertEqual(expected_lower, [p.lower["x"] for p in points]) + self.assertEqual(expected_upper, [p.upper["x"] for p in points]) def test_alternating_with_region(self): y = LineGenerator("y", "mm", 1, 5, 5, True) @@ -468,19 +481,22 @@ def test_double_spiral_scan(self): l2_f = True for (t1, t2) in zip(spiral_t.points['t1'], spiral_t.points['t2']): l2p = line2.points['l2'] if l2_f else line2.points['l2'][::-1] + l2pu = line2.points_upper['l2'] if l2_f else line2.points_lower['l2'][::-1] + l2pl = line2.points_lower['l2'] if l2_f else line2.points_upper['l2'][::-1] l2_f = not l2_f - tl2 += [(l2, t1, t2) for l2 in l2p if l2*l2 + t1*t1 <= 1] + tl2 += [(l2, l2u, l2l, t1, t2) for (l2, l2u, l2l) in + zip(l2p, l2pu, l2pl) if l2*l2 + t1*t1 <= 1] t_f = True for (s1, s2, l1) in l1s: inner = tl2 if t_f else tl2[::-1] t_f = not t_f - points += [(l2, t1, t2, s1, s2, l1) for (l2, t1, t2) in inner - if s1*s1 + l1*l1 <= 1] + points += [(l2, l2u, l2l, t1, t2, s1, s2, l1) + for (l2, l2u, l2l, t1, t2) in inner if s1*s1 + l1*l1 <= 1] l1s_original = l1s l1s = [(s1, s2, l1) for (s1, s2, l1) in l1s if s1*s1 + l1*l1 <= 1] expected = [{"l2":l2, "t1":t1, "t2":t2, "s1":s1, "s2":s2, "l1":l1} - for (l2, t1, t2, s1, s2, l1) in points] + for (l2, l2u, l2l, t1, t2, s1, s2, l1) in points] expected_idx = [] t_f = (l1s_original.index(l1s[0])) % 2 == 0 # t_f is False @@ -489,9 +505,14 @@ def test_double_spiral_scan(self): range_(len(tl2) - 1, -1, -1))] t_f = not t_f + expected_l2_lower = [l2l for (l2, l2u, l2l, t1, t2, s1, s2, l1) in points] + expected_l2_upper = [l2u for (l2, l2u, l2l, t1, t2, s1, s2, l1) in points] + gpoints = list(g.iterator()) self.assertEqual(expected, [p.positions for p in gpoints]) self.assertEqual(expected_idx, [p.indexes for p in gpoints]) + self.assertEqual(expected_l2_lower, [p.lower["l2"] for p in gpoints]) + self.assertEqual(expected_l2_upper, [p.upper["l2"] for p in gpoints]) def test_mutators(self): mutator_1 = FixedDurationMutator(0.2) diff --git a/tests/test_generators/test_linegenerator.py b/tests/test_generators/test_linegenerator.py index 7c94fb5..c16d421 100644 --- a/tests/test_generators/test_linegenerator.py +++ b/tests/test_generators/test_linegenerator.py @@ -28,6 +28,16 @@ def test_array_positions(self): self.assertEqual(lower, self.g.points_lower['x'].tolist()) self.assertEqual(upper, self.g.points_upper['x'].tolist()) + def test_negative_direction(self): + g = LineGenerator("x", "mm", 2, -2, 5) + positions = [2., 1., 0., -1., -2.] + lower = [2.5, 1.5, 0.5, -0.5, -1.5] + upper = [1.5, 0.5, -0.5, -1.5, -2.5] + g.produce_points() + self.assertEqual(positions, g.points['x'].tolist()) + self.assertEqual(lower, g.points_lower['x'].tolist()) + self.assertEqual(upper, g.points_upper['x'].tolist()) + def test_single_point(self): g = LineGenerator("x", "mm", 1.0, 4.0, 1) g.produce_points() From 550ebcb24ffe0150a5d0a9aa9ac0fef3b01b94fb Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Tue, 22 Nov 2016 15:50:34 +0000 Subject: [PATCH 21/47] Change generator bounds to be a single array Each element is now the "end" of the previous point and the start of the "next" point (and hence is one longer than the positions array) --- scanpointgenerator/core/compoundgenerator.py | 25 ++++++++--------- .../generators/linegenerator.py | 19 +++++-------- .../generators/lissajousgenerator.py | 28 +++++++++---------- .../generators/spiralgenerator.py | 27 ++++++++++-------- tests/test_core/test_compoundgenerator.py | 12 ++++---- tests/test_generators/test_linegenerator.py | 15 ++++------ .../test_lissajousgenerator.py | 22 ++++----------- tests/test_generators/test_spiralgenerator.py | 14 ++-------- 8 files changed, 66 insertions(+), 96 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index 90cf76d..e7d4a20 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -28,8 +28,7 @@ def __init__(self, generators, excluders, mutators): self.axes = [] self.position_units = {} self.axes_points = {} - self.axes_points_lower = {} - self.axes_points_upper = {} + self.axes_bounds = {} self.dimensions = [] self.alternate_direction = [g.alternate_direction for g in generators] for generator in generators: @@ -91,8 +90,7 @@ def prepare(self): for generator in generators: generator.produce_points() self.axes_points.update(generator.points) - self.axes_points_lower.update(generator.points_lower) - self.axes_points_upper.update(generator.points_upper) + self.axes_bounds.update(generator.bounds) self.num *= generator.num dim = {"size":generator.num, @@ -305,22 +303,21 @@ def get_point(self, n): j = k // self.generator_dim_scaling[g]["repeat"] gr = j // g.num j %= g.num - bounds_reverse = False + j_lower = j + j_upper = j + 1 if dim["alternate"] and g is not dim["generators"][0] and gr % 2 == 1: # the top level generator's direction is handled by # the fact that the reverse direction was appended j = g.num - j - 1 - bounds_reverse = True - # seriously messy logic now - bounds_reverse |= dim_reverse and g is dim["generators"][0] + j_lower = j + 1 + j_upper = j + elif dim_reverse and g is dim["generators"][0]: + # top level generator is running in reverse + j_lower, j_upper = j_upper, j_lower for axis in g.axes: p.positions[axis] = g.points[axis][j] - lower = g.points_upper[axis] if bounds_reverse \ - else g.points_lower[axis] - upper = g.points_lower[axis] if bounds_reverse \ - else g.points_upper[axis] - p.lower[axis] = lower[j] - p.upper[axis] = upper[j] + p.lower[axis] = g.bounds[axis][j_lower] + p.upper[axis] = g.bounds[axis][j_upper] return p def to_dict(self): diff --git a/scanpointgenerator/generators/linegenerator.py b/scanpointgenerator/generators/linegenerator.py index 8c6f3c1..0ac46c9 100644 --- a/scanpointgenerator/generators/linegenerator.py +++ b/scanpointgenerator/generators/linegenerator.py @@ -74,8 +74,7 @@ def __init__(self, name, units, start, stop, num, alternate_direction=False): def produce_points(self): self.points = {} - self.points_lower = {} - self.points_upper = {} + self.bounds = {} for axis in range_(self.num_axes): axis_name = self.name[axis] start = self.start[axis] @@ -83,21 +82,17 @@ def produce_points(self): d = stop - start if self.num == 1: self.points[axis_name] = np.array([start]) - self.points_upper[axis_name] = np.array([start + 0.5 * d]) - self.points_lower[axis_name] = np.array([start - 0.5 * d]) + self.bounds[axis_name] = np.array( + [start - 0.5 * d, start + 0.5 * d]) else: n = self.num - 1. s = d / n - upper_start = start + 0.5 * d / n - upper_stop = stop + 0.5 * d / n - lower_start = start - 0.5 * d / n - lower_stop = stop - 0.5 * d / n + bound_stop = stop + 0.5 * s + bound_start = start - 0.5 * s self.points[axis_name] = np.linspace( float(start), float(stop), self.num) - self.points_upper[axis_name] = np.linspace( - float(upper_start), float(upper_stop), self.num) - self.points_lower[axis_name] = np.linspace( - float(lower_start), float(lower_stop), self.num) + self.bounds[axis_name] = np.linspace( + float(bound_start), float(bound_stop), self.num + 1) def iterator(self): for i in range_(self.num): diff --git a/scanpointgenerator/generators/lissajousgenerator.py b/scanpointgenerator/generators/lissajousgenerator.py index 594aad9..fc96ff0 100644 --- a/scanpointgenerator/generators/lissajousgenerator.py +++ b/scanpointgenerator/generators/lissajousgenerator.py @@ -59,27 +59,27 @@ def __init__(self, names, units, box, num_lobes, self.axes = self.names # For GDA - def _calc_arrays(self, offset): + def produce_points(self): + self.points = {} + self.bounds = {} + x0, y0 = self.centre[0], self.centre[1] A, B = self.x_max, self.y_max a, b = self.x_freq, self.y_freq d = self.phase_diff - f = lambda t: y0 + A * np.sin(a * 2 * m.pi * (t+offset)/self.num + d) + f = lambda t: y0 + A * np.sin(a * 2 * m.pi * t/self.num + d) x = f(np.arange(self.num)) - f = lambda t: B * np.sin(b * 2 * m.pi * (t+offset)/self.num) + f = lambda t: y0 + A * np.sin(a * 2 * m.pi * (t-0.5)/self.num + d) + bx = f(np.arange(self.num + 1)) + f = lambda t: B * np.sin(b * 2 * m.pi * t/self.num) y = f(np.arange(self.num)) - return x, y + f = lambda t: B * np.sin(b * 2 * m.pi * (t-0.5)/self.num) + by = f(np.arange(self.num + 1)) - def produce_points(self): - self.points = {} - self.points_lower = {} - self.points_upper = {} - - x = self.names[0] - y = self.names[1] - self.points[x], self.points[y] = self._calc_arrays(0) - self.points_upper[x], self.points_upper[y] = self._calc_arrays(0.5) - self.points_lower[x], self.points_lower[y] = self._calc_arrays(-0.5) + self.points[self.names[0]] = x + self.points[self.names[1]] = y + self.bounds[self.names[0]] = bx + self.bounds[self.names[1]] = by def _calc(self, i): """Calculate the coordinate for a given index""" diff --git a/scanpointgenerator/generators/spiralgenerator.py b/scanpointgenerator/generators/spiralgenerator.py index 1d76370..34ff3a5 100644 --- a/scanpointgenerator/generators/spiralgenerator.py +++ b/scanpointgenerator/generators/spiralgenerator.py @@ -50,7 +50,10 @@ def __init__(self, names, units, centre, radius, scale=1.0, self.axes = self.names # For GDA - def _calc_arrays(self, offset): + def produce_points(self): + self.points = {} + self.bounds = {} + # spiral equation : r = b * phi # scale = 2 * pi * b # parameterise phi with approximation: @@ -61,21 +64,21 @@ def _calc_arrays(self, offset): size = (self.radius) / (b * k) size *= size size = int(size) + 1 # TODO: Why the +1 ??? - phi_t = lambda t: k * np.sqrt(t + offset) + + phi_t = lambda t: k * np.sqrt(t + 0.5) phi = phi_t(np.arange(size)) x = self.centre[0] + b * phi * np.sin(phi) y = self.centre[1] + b * phi * np.cos(phi) - return x, y + self.points[self.names[0]] = x + self.points[self.names[1]] = y - def produce_points(self): - self.points = {} - self.points_lower = {} - self.points_upper = {} - x = self.names[0] - y = self.names[1] - self.points_lower[x], self.points_lower[y] = self._calc_arrays(0) - self.points[x], self.points[y] = self._calc_arrays(0.5) - self.points_upper[x], self.points_upper[y] = self._calc_arrays(1.) + size += 1 + phi_t = lambda t: k * np.sqrt(t) + phi = phi_t(np.arange(size)) + bx = self.centre[0] + b * phi * np.sin(phi) + by = self.centre[1] + b * phi * np.cos(phi) + self.bounds[self.names[0]] = bx + self.bounds[self.names[1]] = by def _calc(self, i): """Calculate the coordinate for a given index""" diff --git a/tests/test_core/test_compoundgenerator.py b/tests/test_core/test_compoundgenerator.py index 5f19f44..0327735 100644 --- a/tests/test_core/test_compoundgenerator.py +++ b/tests/test_core/test_compoundgenerator.py @@ -481,8 +481,10 @@ def test_double_spiral_scan(self): l2_f = True for (t1, t2) in zip(spiral_t.points['t1'], spiral_t.points['t2']): l2p = line2.points['l2'] if l2_f else line2.points['l2'][::-1] - l2pu = line2.points_upper['l2'] if l2_f else line2.points_lower['l2'][::-1] - l2pl = line2.points_lower['l2'] if l2_f else line2.points_upper['l2'][::-1] + l2pu = line2.bounds['l2'][1:len(line2.points['l2'])+1] + l2pl = line2.bounds['l2'][0:len(line2.points['l2'])] + if not l2_f: + l2pu, l2pl = l2pl[::-1], l2pu[::-1] l2_f = not l2_f tl2 += [(l2, l2u, l2l, t1, t2) for (l2, l2u, l2l) in zip(l2p, l2pu, l2pl) if l2*l2 + t1*t1 <= 1] @@ -555,11 +557,9 @@ def test_post_prepare(self): g = CompoundGenerator([y, x], [], []) g.prepare() self.assertListAlmostEqual([1.0, 1.1, 1.2], g.axes_points["x"].tolist()) - self.assertListAlmostEqual([0.95, 1.05, 1.15], g.axes_points_lower["x"].tolist()) - self.assertListAlmostEqual([1.05, 1.15, 1.25], g.axes_points_upper["x"].tolist()) + self.assertListAlmostEqual([0.95, 1.05, 1.15, 1.25], g.axes_bounds["x"].tolist()) self.assertListAlmostEqual([2.0, 2.1], g.axes_points["y"].tolist()) - self.assertListAlmostEqual([1.95, 2.05], g.axes_points_lower["y"].tolist()) - self.assertListAlmostEqual([2.05, 2.15], g.axes_points_upper["y"].tolist()) + self.assertListAlmostEqual([1.95, 2.05, 2.15], g.axes_bounds["y"].tolist()) self.assertEqual(g.num, 6) self.assertEqual(2, len(g.dimensions)) diff --git a/tests/test_generators/test_linegenerator.py b/tests/test_generators/test_linegenerator.py index c16d421..a13c243 100644 --- a/tests/test_generators/test_linegenerator.py +++ b/tests/test_generators/test_linegenerator.py @@ -20,30 +20,25 @@ def test_init(self): def test_array_positions(self): positions = [1.0, 3.0, 5.0, 7.0, 9.0] - lower = [0.0, 2.0, 4.0, 6.0, 8.0] - upper = [2.0, 4.0, 6.0, 8.0, 10.0] + bounds = [0.0, 2.0, 4.0, 6.0, 8.0, 10.0] indexes = [0, 1, 2, 3, 4] self.g.produce_points() self.assertEqual(positions, self.g.points['x'].tolist()) - self.assertEqual(lower, self.g.points_lower['x'].tolist()) - self.assertEqual(upper, self.g.points_upper['x'].tolist()) + self.assertEqual(bounds, self.g.bounds['x'].tolist()) def test_negative_direction(self): g = LineGenerator("x", "mm", 2, -2, 5) positions = [2., 1., 0., -1., -2.] - lower = [2.5, 1.5, 0.5, -0.5, -1.5] - upper = [1.5, 0.5, -0.5, -1.5, -2.5] + bounds = [2.5, 1.5, 0.5, -0.5, -1.5, -2.5] g.produce_points() self.assertEqual(positions, g.points['x'].tolist()) - self.assertEqual(lower, g.points_lower['x'].tolist()) - self.assertEqual(upper, g.points_upper['x'].tolist()) + self.assertEqual(bounds, g.bounds['x'].tolist()) def test_single_point(self): g = LineGenerator("x", "mm", 1.0, 4.0, 1) g.produce_points() self.assertEqual([1.0], g.points["x"].tolist()) - self.assertEqual([2.5], g.points_upper["x"].tolist()) - self.assertEqual([-0.5], g.points_lower["x"].tolist()) + self.assertEqual([-0.5, 2.5], g.bounds["x"].tolist()) def test_iterator(self): positions = [1.0, 3.0, 5.0, 7.0, 9.0] diff --git a/tests/test_generators/test_lissajousgenerator.py b/tests/test_generators/test_lissajousgenerator.py index 38e32f1..fb3a2c4 100644 --- a/tests/test_generators/test_lissajousgenerator.py +++ b/tests/test_generators/test_lissajousgenerator.py @@ -80,17 +80,8 @@ def test_array_positions(self): {'y': 0.2938926261462367, 'x': -0.1545084971874738}, {'y': -0.2938926261462364, 'x': 0.1545084971874736}, {'y': -0.4755282581475769, 'x': 0.4045084971874736}] - lower = [{'y': -0.29389262614623657, 'x': 0.47552825814757677}, + bounds = [{'y': -0.29389262614623657, 'x': 0.47552825814757677}, {'y': 0.29389262614623657, 'x': 0.4755282581475768}, - {'y': 0.4755282581475768, 'x': 0.2938926261462366}, - {'y': 6.123233995736766e-17, 'x': 6.123233995736766e-17}, - {'y': -0.47552825814757677, 'x': -0.2938926261462365}, - {'y': -0.2938926261462367, 'x': -0.47552825814757677}, - {'y': 0.29389262614623607, 'x': -0.47552825814757682}, - {'y': 0.4755282581475768, 'x': -0.2938926261462367}, - {'y': 1.8369701987210297e-16, 'x': -1.2246467991473532e-16}, - {'y': -0.4755282581475767, 'x': 0.29389262614623646}] - upper = [{'y': 0.29389262614623657, 'x': 0.4755282581475768}, {'y': 0.4755282581475768, 'x': 0.2938926261462366}, {'y': 6.123233995736766e-17, 'x': 6.123233995736766e-17}, {'y': -0.47552825814757677, 'x': -0.2938926261462365}, @@ -102,13 +93,10 @@ def test_array_positions(self): {'y': -0.29389262614623674, 'x': 0.47552825814757677}] g.produce_points() - points = [{'x':x, 'y':y} for (x, y) in zip(g.points['x'], g.points['y'])] - points_upper = [{'x':x, 'y':y} for (x, y) in zip(g.points_upper['x'], g.points_upper['y'])] - points_lower = [{'x':x, 'y':y} for (x, y) in zip(g.points_lower['x'], g.points_lower['y'])] - self.assertEqual(10, len(points)) - self.assertEquals(points, positions) - self.assertEquals(points_upper, upper) - self.assertEquals(points_lower, lower) + p = [{'x':x, 'y':y} for (x, y) in zip(g.points['x'], g.points['y'])] + b = [{'x':x, 'y':y} for (x, y) in zip(g.bounds['x'], g.bounds['y'])] + self.assertEqual(positions, p) + self.assertEqual(bounds, b) def test_to_dict(self): expected_dict = dict() diff --git a/tests/test_generators/test_spiralgenerator.py b/tests/test_generators/test_spiralgenerator.py index 24ee8c2..2012cbf 100644 --- a/tests/test_generators/test_spiralgenerator.py +++ b/tests/test_generators/test_spiralgenerator.py @@ -30,14 +30,8 @@ def test_array_positions(self): {'y': 0.3924587351155914, 'x': 1.130650533568409}, {'y': -0.5868891557832875, 'x': 1.18586065489788}, {'y': -1.332029488076613, 'x': 0.5428735608675326}] - lower = [{'y': 0.0, 'x': 0.0}, + bounds = [{'y':0.0, 'x':0.0}, {'y': -0.5189218293602549, 'x': -0.2214272368007088}, - {'y': 0.23645222432582483, 'x': -0.7620433832656455}, - {'y': 0.9671992383675001, 'x': -0.13948222773063082}, - {'y': 0.7807653675717078, 'x': 0.8146440851904461}, - {'y': -0.09160107657707395, 'x': 1.2582363345925418}, - {'y': -1.0190886264001306, 'x': 0.9334439933089926}] - upper = [{'y': -0.5189218293602549, 'x': -0.2214272368007088}, {'y': 0.23645222432582483, 'x': -0.7620433832656455}, {'y': 0.9671992383675001, 'x': -0.13948222773063082}, {'y': 0.7807653675717078, 'x': 0.8146440851904461}, @@ -46,11 +40,9 @@ def test_array_positions(self): {'y': -1.4911377166541206, 'x': 0.06839234794968006}] self.g.produce_points() p = [{"x":x, "y":y} for (x, y) in zip(self.g.points['x'], self.g.points['y'])] - l = [{"x":x, "y":y} for (x, y) in zip(self.g.points_lower['x'], self.g.points_lower['y'])] - u = [{"x":x, "y":y} for (x, y) in zip(self.g.points_upper['x'], self.g.points_upper['y'])] + b = [{"x":x, "y":y} for (x, y) in zip(self.g.bounds['x'], self.g.bounds['y'])] self.assertEqual(positions, p) - self.assertEqual(lower, l) - self.assertEqual(upper, u) + self.assertEqual(bounds, b) def test_iterator(self): positions = [{'y': -0.3211855677650875, 'x': 0.23663214944574582}, From 400929bfa106e6b5e967ddde70f38488eb50acb2 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Wed, 23 Nov 2016 14:35:38 +0000 Subject: [PATCH 22/47] Tidy CompoundGenerator a little bit Abstracts the Dimension structure into a separate class. Allows moving the "merging" part out of CompoundGenerator.prepare at the very least. The code is still complex in places with too much branching to be regarded tasteful. --- scanpointgenerator/core/compoundgenerator.py | 334 ++++++++++--------- tests/test_core/test_compoundgenerator.py | 46 ++- 2 files changed, 197 insertions(+), 183 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index e7d4a20..3201919 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -27,9 +27,9 @@ def __init__(self, generators, excluders, mutators): self.mutators = mutators self.axes = [] self.position_units = {} - self.axes_points = {} - self.axes_bounds = {} self.dimensions = [] + self.num = 1 + self.dim_meta = {} self.alternate_direction = [g.alternate_direction for g in generators] for generator in generators: logging.debug("Generator passed to Compound init") @@ -46,8 +46,10 @@ def __init__(self, generators, excluders, mutators): self.generator_dim_scaling = {} def prepare(self): - self.num = 1 - self.dimensions = [] + """ + Prepare data structures and masks required for point generation. + Must be called before get_point or iterator are called. + """ # we're going to mutate these structures excluders = list(self.excluders) generators = list(self.generators) @@ -89,22 +91,10 @@ def prepare(self): for generator in generators: generator.produce_points() - self.axes_points.update(generator.points) - self.axes_bounds.update(generator.bounds) - self.num *= generator.num - - dim = {"size":generator.num, - "axes":list(generator.axes), - "generators":[generator], - "masks":[], - "tile":1, - "repeat":1, - "alternate":generator.alternate_direction} - self.dimensions.append(dim) + self.dimensions.append(Dimension(generator)) for excluder in excluders: axis_1, axis_2 = excluder.scannables - # ensure axis_1 is "outer" axis (if separate generators) gen_1 = [g for g in generators if axis_1 in g.axes][0] gen_2 = [g for g in generators if axis_2 in g.axes][0] gen_diff = generators.index(gen_1) \ @@ -113,27 +103,16 @@ def prepare(self): raise ValueError( "Excluders must be defined on axes that are adjacent in " \ "generator order") - if gen_diff == 1: - gen_1, gen_2 = gen_2, gen_1 - axis_1, axis_2 = axis_2, axis_1 - gen_diff = -1 - - ##### - # first check if region spans two dimensions - merge if so - ##### - dim_1 = [i for i in self.dimensions if axis_1 in i["axes"]][0] - dim_2 = [i for i in self.dimensions if axis_2 in i["axes"]][0] + # merge dimensions if region spans two + dim_1 = [i for i in self.dimensions if axis_1 in i.axes][0] + dim_2 = [i for i in self.dimensions if axis_2 in i.axes][0] dim_diff = self.dimensions.index(dim_1) \ - self.dimensions.index(dim_2) - if dim_diff < -1 or dim_diff > 1: - raise ValueError( - "Excluders must be defined on axes that are adjacent in " \ - "generator order") if dim_diff == 1: dim_1, dim_2 = dim_2, dim_1 dim_diff = -1 - if dim_1["alternate"] != dim_2["alternate"] \ + if dim_1.alternate != dim_2.alternate \ and dim_1 is not self.dimensions[0]: raise ValueError( "Generators tied by regions must have the same " \ @@ -141,133 +120,53 @@ def prepare(self): # merge "inner" into "outer" if dim_diff == -1: # dim_1 is "outer" - preserves axis ordering - - # need to appropriately scale the existing masks - # masks are "tiled" by the size of generators "below" them - # and their elements are "repeated" by the size of generators - # above them, so: - # |mask| * duplicates * repeates == |generators in index| - scale = 1 - for g in dim_2["generators"]: - scale *= g.num - for m in dim_1["masks"]: - m["repeat"] *= scale - scale = 1 - for g in dim_1["generators"]: - scale *= g.num - for m in dim_2["masks"]: - m["tile"] *= scale - dim_1["masks"] += dim_2["masks"] - dim_1["axes"] += dim_2["axes"] - dim_1["generators"] += dim_2["generators"] - dim_1["size"] *= dim_2["size"] - dim_1["alternate"] |= dim_2["alternate"] + new_dim = Dimension.merge_dimensions(dim_1, dim_2) + self.dimensions[self.dimensions.index(dim_1)] = new_dim self.dimensions.remove(dim_2) - dim = dim_1 - - ##### - # generate the mask for this region - ##### - # if gen_1 and gen_2 are different then the outer axis will have to - # have its elements repeated and the inner axis will have to have - # itself repeated - gen_1 is always inner axis - - points_1 = self.axes_points[axis_1] - points_2 = self.axes_points[axis_2] - - doubled_mask = False # used for some cases of alternating generators - - if gen_1 is gen_2 and dim["alternate"]: - # run *both* axes backwards - # but our mask will be a factor of 2 too big - doubled_mask = True - points_1 = np.append(points_1, points_1[::-1]) - points_2 = np.append(points_2, points_2[::-1]) - elif dim["alternate"]: - doubled_mask = True - points_1 = np.append(points_1, points_1[::-1]) - points_2 = np.append(points_2, points_2[::-1]) - points_2 = np.tile(points_2, gen_1.num) - points_1 = np.repeat(points_1, gen_2.num) - elif gen_1 is not gen_2: - points_1 = np.repeat(points_1, gen_2.num) - points_2 = np.tile(points_2, gen_1.num) + dim = new_dim else: - # copy the points arrays anyway so the regions can - # safely perform any array operations in place - # this is advantageous in the cases above - points_1 = np.copy(points_1) - points_2 = np.copy(points_2) - + dim = dim_1 - if axis_1 == excluder.scannables[0]: - mask = excluder.create_mask(points_1, points_2) - else: - mask = excluder.create_mask(points_2, points_1) + dim.apply_excluder(excluder) - ##### - # Add new mask to index - ##### - tile = 0.5 if doubled_mask else 1 - repeat = 1 - found_axis = False - # tile by product of generators "before" - # repeat by product of generators "after" - for g in dim["generators"]: - if axis_1 in g.axes or axis_2 in g.axes: - found_axis = True - else: - if found_axis: - repeat *= g.num - else: - tile *= g.num - m = {"repeat":repeat, "tile":tile, "mask":mask} - dim["masks"].append(m) - # end for excluder in excluders - ##### - - tile = 1 - repeat = 1 - ##### - # Generate full index mask and "apply" - ##### + self.num = 1 for dim in self.dimensions: - mask = np.full(dim["size"], True, dtype=np.int8) - for m in dim["masks"]: - assert len(m["mask"]) * m["repeat"] * m["tile"] == len(mask), \ - "Mask lengths are not consistent" - expanded = np.repeat(m["mask"], m["repeat"]) - if m["tile"] % 1 != 0: - ex = np.tile(expanded, int(m["tile"])) - expanded = np.append(ex, expanded[:len(expanded)//2]) - else: - expanded = np.tile(expanded, int(m["tile"])) - mask &= expanded - dim["mask"] = mask - dim["indicies"] = np.nonzero(mask)[0] - if len(dim["indicies"]) == 0: + self.dim_meta[dim] = {} + mask = dim.create_dimension_mask() + indicies = np.nonzero(mask)[0] + if len(indicies) == 0: raise ValueError("Regions would exclude entire scan") - repeat *= len(dim["indicies"]) - self.num = repeat + self.num *= len(indicies) + self.dim_meta[dim]["mask"] = mask + self.dim_meta[dim]["indicies"] = indicies + + repeat = self.num + tile = 1 for dim in self.dimensions: - l = len(dim["indicies"]) - repeat /= l - dim["tile"] = tile - dim["repeat"] = repeat - tile *= l + dim_length = len(self.dim_meta[dim]["indicies"]) + repeat /= dim_length + self.dim_meta[dim]["tile"] = tile + self.dim_meta[dim]["repeat"] = repeat + tile *= dim_length for dim in self.dimensions: tile = 1 repeat = 1 - for g in dim["generators"]: + for g in dim.generators: repeat *= g.num - for g in dim["generators"]: + for g in dim.generators: repeat /= g.num d = {"tile":tile, "repeat":repeat} tile *= g.num self.generator_dim_scaling[g] = d def iterator(self): + """ + Iterator yielding generator positions at each scan point + + Yields: + Point: The next point + """ it = (self.get_point(n) for n in range_(self.num)) for m in self.mutators: it = m.mutate(it) @@ -275,50 +174,56 @@ def iterator(self): yield p def get_point(self, n): + """ + Retrieve the desired point from the generator + Args: + n (int): point to be generated + """ + if n >= self.num: raise IndexError("Requested point is out of range") - p = Point() + point = Point() - # need to know how far along each index we are + # need to know how far along each dimension we are # and, in the case of alternating indicies, how # many times we've run through them - kc = 0 # the "cumulative" k for each index + kc = 0 # the "cumulative" k for each dimension for dim in self.dimensions: - indicies = dim["indicies"] - i = n // dim["repeat"] - r = i // len(indicies) + indicies = self.dim_meta[dim]["indicies"] + i = n // self.dim_meta[dim]["repeat"] i %= len(indicies) k = indicies[i] dim_reverse = False - if dim["alternate"] and kc % 2 == 1: + if dim.alternate and kc % 2 == 1: i = len(indicies) - i - 1 dim_reverse = True kc *= len(indicies) kc += k k = indicies[i] - # need point k along each generator in index + # need point k along each generator in dimension # in alternating case, need to sometimes go backward - p.indexes.append(i) - for g in dim["generators"]: + point.indexes.append(i) + for g in dim.generators: j = k // self.generator_dim_scaling[g]["repeat"] - gr = j // g.num + r = j // g.num j %= g.num j_lower = j j_upper = j + 1 - if dim["alternate"] and g is not dim["generators"][0] and gr % 2 == 1: + if dim.alternate and g is not dim.generators[0] and r % 2 == 1: # the top level generator's direction is handled by # the fact that the reverse direction was appended j = g.num - j - 1 j_lower = j + 1 j_upper = j - elif dim_reverse and g is dim["generators"][0]: - # top level generator is running in reverse + elif dim_reverse and g is dim.generators[0]: + # top level generator is running in reverse, + # so bounds are swapped j_lower, j_upper = j_upper, j_lower for axis in g.axes: - p.positions[axis] = g.points[axis][j] - p.lower[axis] = g.bounds[axis][j_lower] - p.upper[axis] = g.bounds[axis][j_upper] - return p + point.positions[axis] = g.points[axis][j] + point.lower[axis] = g.bounds[axis][j_lower] + point.upper[axis] = g.bounds[axis][j_upper] + return point def to_dict(self): """Convert object attributes into a dictionary""" @@ -342,3 +247,114 @@ def from_dict(cls, d): excluders = [Excluder.from_dict(e) for e in d['excluders']] mutators = [Mutator.from_dict(m) for m in d['mutators']] return cls(generators, excluders, mutators) + +class Dimension(object): + """A collapsed set of generators joined by excluders""" + def __init__(self, generator): + self.axes = list(generator.axes) + self.generators = [generator] + self.size = generator.num + self.masks = [] + self.alternate = generator.alternate_direction + + def apply_excluder(self, excluder): + """Apply an excluder with axes matching some axes in the dimension to + produce an internal mask""" + axis_inner = excluder.scannables[0] + axis_outer = excluder.scannables[1] + gen_inner = [g for g in self.generators if axis_inner in g.axes][0] + gen_outer = [g for g in self.generators if axis_outer in g.axes][0] + points_x = gen_inner.points[axis_inner] + points_y = gen_outer.points[axis_outer] + if self.generators.index(gen_inner) > self.generators.index(gen_outer): + gen_inner, gen_outer = gen_outer, gen_inner + axis_inner, axis_outer = axis_outer, axis_inner + points_x, points_y = points_y, points_x + + if gen_inner is gen_outer and self.alternate: + points_x = np.append(points_x, points_x[::-1]) + points_y = np.append(points_y, points_y[::-1]) + elif self.alternate: + points_x = np.append(points_x, points_x[::-1]) + points_x = np.repeat(points_x, gen_outer.num) + points_y = np.append(points_y, points_y[::-1]) + points_y = np.tile(points_y, gen_inner.num) + elif gen_inner is not gen_outer: + points_x = np.repeat(points_x, gen_outer.num) + points_y = np.tile(points_y, gen_inner.num) + else: + # copy the point arrays so the excluders can perform + # array operations in place (advantageous in the other cases) + points_x = np.copy(points_x) + points_y = np.copy(points_y) + + if axis_inner == excluder.scannables[0]: + mask = excluder.create_mask(points_x, points_y) + else: + mask = excluder.create_mask(points_y, points_x) + tile = 0.5 if self.alternate else 1 + repeat = 1 + found_axis = False + for g in self.generators: + if axis_inner in g.axes or axis_outer in g.axes: + found_axis = True + else: + if found_axis: + repeat *= g.num + else: + tile *= g.num + + m = {"repeat":repeat, "tile":tile, "mask":mask} + self.masks.append(m) + + def create_dimension_mask(self): + """ + Create and return a mask for every point in the dimension + + e.g. (with [y1, y2, y3] and [x1, x2, x3] both alternating) + y: y1, y1, y1, y2, y2, y2, y3, y3, y3 + x: x1, x2, x3, x3, x2, x1, x1, x2, x3 + mask: m1, m2, m3, m4, m5, m6, m7, m8, m9 + + Returns: + np.array(int8): One dimensional mask array + """ + mask = np.full(self.size, True, dtype=np.int8) + for m in self.masks: + assert len(m["mask"]) * m["repeat"] * m["tile"] == len(mask), \ + "Mask lengths are not consistent" + expanded = np.repeat(m["mask"], m["repeat"]) + if m["tile"] % 1 != 0: + ex = np.tile(expanded, int(m["tile"])) + expanded = np.append(ex, expanded[:len(expanded)//2]) + else: + expanded = np.tile(expanded, int(m["tile"])) + mask &= expanded + return mask + + @staticmethod + def merge_dimensions(outer, inner): + """Collapse two dimensions into one, appropriate scaling structures""" + dim = Dimension(outer.generators[0]) + # masks in the inner generator are tiled by the size of + # outer generators and outer generators have their elements + # repeated by the size of inner generators + inner_masks = [m.copy() for m in inner.masks] + outer_masks = [m.copy() for m in outer.masks] + scale = 1 + for g in inner.generators: + scale *= g.num + for m in outer_masks: + m["repeat"] *= scale + scale = 1 + for g in outer.generators: + scale *= g.num + for m in inner_masks: + m["tile"] *= scale + dim.masks = outer_masks + inner_masks + dim.axes = outer.axes + inner.axes + dim.generators = outer.generators + inner.generators + dim.alternate = outer.alternate or inner.alternate + dim.size = outer.size * inner.size + return dim + diff --git a/tests/test_core/test_compoundgenerator.py b/tests/test_core/test_compoundgenerator.py index 0327735..347d16a 100644 --- a/tests/test_core/test_compoundgenerator.py +++ b/tests/test_core/test_compoundgenerator.py @@ -556,17 +556,15 @@ def test_post_prepare(self): y = LineGenerator("y", "mm", 2.0, 2.1, 2, False) g = CompoundGenerator([y, x], [], []) g.prepare() - self.assertListAlmostEqual([1.0, 1.1, 1.2], g.axes_points["x"].tolist()) - self.assertListAlmostEqual([0.95, 1.05, 1.15, 1.25], g.axes_bounds["x"].tolist()) - self.assertListAlmostEqual([2.0, 2.1], g.axes_points["y"].tolist()) - self.assertListAlmostEqual([1.95, 2.05, 2.15], g.axes_bounds["y"].tolist()) self.assertEqual(g.num, 6) self.assertEqual(2, len(g.dimensions)) - self.assertEqual(["y"], g.dimensions[0]["axes"]) - self.assertEqual([True] * 2, g.dimensions[0]["mask"].tolist()) - self.assertEqual(["x"], g.dimensions[1]["axes"]) - self.assertEqual([True] * 3, g.dimensions[1]["mask"].tolist()) + dim_0 = g.dimensions[0] + dim_1 = g.dimensions[1] + self.assertEqual(["y"], dim_0.axes) + self.assertEqual([True] * 2, g.dim_meta[dim_0]["mask"].tolist()) + self.assertEqual(["x"], dim_1.axes) + self.assertEqual([True] * 3, g.dim_meta[dim_1]["mask"].tolist()) def test_prepare_with_regions(self): x = LineGenerator("x", "mm", 0, 1, 5, False) @@ -576,10 +574,10 @@ def test_prepare_with_regions(self): g = CompoundGenerator([y, x], [excluder], []) g.prepare() self.assertEqual(1, len(g.dimensions)) - self.assertEqual(["y", "x"], g.dimensions[0]["axes"]) + self.assertEqual(["y", "x"], g.dimensions[0].axes) expected_mask = [(x/4.)**2 + (y/4.)**2 <= 1 for y in range(0, 5) for x in range(0, 5)] - self.assertEqual(expected_mask, g.dimensions[0]["mask"].tolist()) + self.assertEqual(expected_mask, g.dim_meta[g.dimensions[0]]["mask"].tolist()) def test_simple_mask(self): x = LineGenerator("x", "mm", -1.0, 1.0, 5, False) @@ -590,7 +588,7 @@ def test_simple_mask(self): g.prepare() p = [(x/2., y/2.) for y in range_(-2, 3) for x in range_(-2, 3)] expected_mask = [x*x + y*y <= 1 for (x, y) in p] - self.assertEqual(expected_mask, g.dimensions[0]["mask"].tolist()) + self.assertEqual(expected_mask, g.dim_meta[g.dimensions[0]]["mask"].tolist()) def test_simple_mask_alternating(self): x = LineGenerator("x", "mm", -1.0, 1.0, 5, alternate_direction=True) @@ -608,7 +606,7 @@ def test_simple_mask_alternating(self): p += [(x/2., y/2.) for x in range_(-2, 3)] reverse = not reverse expected_mask = [(x-0.5)**2 + y**2 <= 1**2 for (x, y) in p] - self.assertEqual(expected_mask, g.dimensions[0]["mask"].tolist()) + self.assertEqual(expected_mask, g.dim_meta[g.dimensions[0]]["mask"].tolist()) def test_double_mask_alternating_spiral(self): zgen = LineGenerator("z", "mm", 0.0, 4.0, 5, alternate_direction=True) @@ -619,13 +617,13 @@ def test_double_mask_alternating_spiral(self): e2 = Excluder(r2, ["y", "z"]) g = CompoundGenerator([zgen, spiral], [e1, e2], []) g.prepare() - xy = list(zip(g.axes_points['x'], g.axes_points['y'])) + xy = list(zip(spiral.points['x'], spiral.points['y'])) p = [] for z in range_(0, 5): p += [(x, y, z) for (x, y) in (xy if z % 2 == 0 else xy[::-1])] expected = [x >= -2 and x <= 1 and y >= -2 and y <= 2 and z >= 0 and z <= 3 for (x, y, z) in p] - actual = g.dimensions[0]["mask"].tolist() + actual = g.dim_meta[g.dimensions[0]]["mask"].tolist() self.assertEqual(expected, actual) def test_double_mask_spiral(self): @@ -637,11 +635,11 @@ def test_double_mask_spiral(self): e2 = Excluder(r2, ["y", "z"]) g = CompoundGenerator([zgen, spiral], [e1, e2], []) g.prepare() - p = list(zip(g.axes_points['x'], g.axes_points['y'])) + p = list(zip(spiral.points['x'], spiral.points['y'])) p = [(x, y, z) for z in range_(0, 5) for (x, y) in p] expected = [x >= -2 and x <= 1 and y >= -2 and y <= 2 and z >= 0 and z <= 3 for (x, y, z) in p] - actual = g.dimensions[0]["mask"].tolist() + actual = g.dim_meta[g.dimensions[0]]["mask"].tolist() self.assertEqual(expected, actual) def test_simple_mask_alternating_spiral(self): @@ -651,10 +649,10 @@ def test_simple_mask_alternating_spiral(self): e = Excluder(r, ["x", "y"]) g = CompoundGenerator([z, spiral], [e], []) g.prepare() - p = list(zip(g.axes_points['x'], g.axes_points['y'])) + p = list(zip(spiral.points['x'], spiral.points['y'])) expected = [x >= -2 and x < 1 and y >= -2 and y < 2 for (x, y) in p] expected_r = [x >= -2 and x < 1 and y >= -2 and y < 2 for (x, y) in p[::-1]] - actual = g.dimensions[1]["mask"].tolist() + actual = g.dim_meta[g.dimensions[1]]["mask"].tolist() self.assertEqual(expected, actual) def test_double_mask(self): @@ -672,7 +670,7 @@ def test_double_mask(self): m1 = [(x-0.1)**2 + (y-0.2)**2 <= 1 for (x, y, z) in p] m2 = [(y-0.1)**2 + (z-0.2)**2 <= 1 for (x, y, z) in p] expected_mask = [(b1 and b2) for (b1, b2) in zip(m1, m2)] - self.assertEqual(expected_mask, g.dimensions[0]["mask"].tolist()) + self.assertEqual(expected_mask, g.dim_meta[g.dimensions[0]]["mask"].tolist()) def test_complex_masks(self): tg = LineGenerator("t", "mm", 1, 5, 5) @@ -699,8 +697,8 @@ def test_complex_masks(self): ix += 1 iy += 1 - self.assertEqual(t_mask, g.dimensions[0]["mask"].tolist()) - self.assertEqual(xyz_mask, g.dimensions[1]["mask"].tolist()) + self.assertEqual(t_mask, g.dim_meta[g.dimensions[0]]["mask"].tolist()) + self.assertEqual(xyz_mask, g.dim_meta[g.dimensions[1]]["mask"].tolist()) def test_separate_indexes(self): x1 = LineGenerator("x1", "mm", -1.0, 1.0, 5, False) @@ -727,13 +725,13 @@ def test_separate_indexes(self): m1 = [x*x + y*y <= 1 for (x, y, z) in p] m2 = [y*y + z*z <= 1 for (x, y, z) in p] expected_mask = [(b1 and b2) for (b1, b2) in zip(m1, m2)] - self.assertEqual(expected_mask, g.dimensions[2]["mask"].tolist()) + self.assertEqual(expected_mask, g.dim_meta[g.dimensions[2]]["mask"].tolist()) p = [(x/2., y/2.) for y in range_(-2, 3) for x in range_(-2, 3)] expected_mask = [x*x + y*y <= 1 for (x, y) in p] - self.assertEqual(expected_mask, g.dimensions[1]["mask"].tolist()) + self.assertEqual(expected_mask, g.dim_meta[g.dimensions[1]]["mask"].tolist()) p = [(x/4., y/4.) for y in range_(0, 5) for x in range_(0, 5)] expected_mask = [x*x + y*y <= 1 for (x, y) in p] - self.assertEqual(expected_mask, g.dimensions[0]["mask"].tolist()) + self.assertEqual(expected_mask, g.dim_meta[g.dimensions[0]]["mask"].tolist()) class TestSerialisation(unittest.TestCase): From 467351c84d1c6ea04fb00e971880c59169e1f913 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Wed, 23 Nov 2016 16:37:18 +0000 Subject: [PATCH 23/47] Fix LissajousGenerator point generation to properly add offsets The centre attribute was being handled incorrectly after the vectorisation change. --- .../generators/lissajousgenerator.py | 8 +++---- .../test_lissajousgenerator.py | 23 ++++++++++++++++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/scanpointgenerator/generators/lissajousgenerator.py b/scanpointgenerator/generators/lissajousgenerator.py index fc96ff0..008e6ac 100644 --- a/scanpointgenerator/generators/lissajousgenerator.py +++ b/scanpointgenerator/generators/lissajousgenerator.py @@ -67,13 +67,13 @@ def produce_points(self): A, B = self.x_max, self.y_max a, b = self.x_freq, self.y_freq d = self.phase_diff - f = lambda t: y0 + A * np.sin(a * 2 * m.pi * t/self.num + d) + f = lambda t: x0 + A * np.sin(a * 2 * m.pi * t/self.num + d) x = f(np.arange(self.num)) - f = lambda t: y0 + A * np.sin(a * 2 * m.pi * (t-0.5)/self.num + d) + f = lambda t: x0 + A * np.sin(a * 2 * m.pi * (t-0.5)/self.num + d) bx = f(np.arange(self.num + 1)) - f = lambda t: B * np.sin(b * 2 * m.pi * t/self.num) + f = lambda t: y0 + B * np.sin(b * 2 * m.pi * t/self.num) y = f(np.arange(self.num)) - f = lambda t: B * np.sin(b * 2 * m.pi * (t-0.5)/self.num) + f = lambda t: y0 + B * np.sin(b * 2 * m.pi * (t-0.5)/self.num) by = f(np.arange(self.num + 1)) self.points[self.names[0]] = x diff --git a/tests/test_generators/test_lissajousgenerator.py b/tests/test_generators/test_lissajousgenerator.py index fb3a2c4..fb3e8da 100644 --- a/tests/test_generators/test_lissajousgenerator.py +++ b/tests/test_generators/test_lissajousgenerator.py @@ -7,7 +7,7 @@ from scanpointgenerator import LissajousGenerator -class LissajousGeneratorTest(unittest.TestCase): +class LissajousGeneratorTest(ScanPointGeneratorTest): def setUp(self): self.bounding_box = dict(centre=[0.0, 0.0], width=1.0, height=1.0) @@ -98,6 +98,27 @@ def test_array_positions(self): self.assertEqual(positions, p) self.assertEqual(bounds, b) + def test_array_positions_with_offset(self): + g = LissajousGenerator( + ['x', 'y'], 'mm', + {"centre":[1., 0.], "height":1., "width":2.}, + 1, num_points = 5) + g.produce_points() + expected_x = [2.0, 1.3090169943749475, 0.19098300532505266, + 0.19098300532505266, 1.3090169943749475] + expected_y = [0.0, 0.2938926261462366, -0.4755282581475768, + 0.47552825814757677, -0.2938926261462364] + expected_bx = [1.8090169943749475, 1.8090169943749475, + 0.6909830056250528, 0.0, + 0.6909830056250523, 1.8090169943749472] + expected_by = [-0.47552825814757677, 0.47552825814757677, + -0.2938926261462365, -1.2246467991473532e-16, + 0.2938926261462367, -0.4755282581475769] + self.assertListAlmostEqual(expected_x, g.points['x'].tolist()) + self.assertListAlmostEqual(expected_y, g.points['y'].tolist()) + self.assertListAlmostEqual(expected_bx, g.bounds['x'].tolist()) + self.assertListAlmostEqual(expected_by, g.bounds['y'].tolist()) + def test_to_dict(self): expected_dict = dict() box = dict() From c6fdd91435ba2e3293bab6825101575038822258 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Thu, 24 Nov 2016 14:19:26 +0000 Subject: [PATCH 24/47] Remove iterator from Generators (except CompoundGenerator) No longer used following the vectorisation changes. Also fixes plot_generator(...) to wrap non-compound generators in a CompoundGenerator to allow the usual iteration. --- scanpointgenerator/core/generator.py | 8 --- .../generators/linegenerator.py | 16 ----- .../generators/lissajousgenerator.py | 20 ------ .../generators/spiralgenerator.py | 52 ++++---------- scanpointgenerator/plotgenerator.py | 11 ++- tests/test_core/test_generator.py | 1 - tests/test_generators/test_linegenerator.py | 71 +++++++------------ .../test_lissajousgenerator.py | 45 ------------ tests/test_generators/test_spiralgenerator.py | 33 --------- .../test_mutators/test_randomoffsetmutator.py | 18 +++-- 10 files changed, 61 insertions(+), 214 deletions(-) diff --git a/scanpointgenerator/core/generator.py b/scanpointgenerator/core/generator.py index 6cdd52b..9b427a0 100644 --- a/scanpointgenerator/core/generator.py +++ b/scanpointgenerator/core/generator.py @@ -22,14 +22,6 @@ class Generator(object): _generator_lookup = {} axes = [] - def iterator(self): - """An iterator yielding positions at each scan point - - Yields: - Point: The next scan :class:`Point` - """ - raise NotImplementedError - def to_dict(self): """Abstract method to convert object attributes into a dictionary""" raise NotImplementedError diff --git a/scanpointgenerator/generators/linegenerator.py b/scanpointgenerator/generators/linegenerator.py index 0ac46c9..7f22c1c 100644 --- a/scanpointgenerator/generators/linegenerator.py +++ b/scanpointgenerator/generators/linegenerator.py @@ -94,22 +94,6 @@ def produce_points(self): self.bounds[axis_name] = np.linspace( float(bound_start), float(bound_stop), self.num + 1) - def iterator(self): - for i in range_(self.num): - point = Point() - - for axis_index in range_(self.num_axes): - axis_name = self.name[axis_index] - start = self.start[axis_index] - step = self.step[axis_index] - - point.positions[axis_name] = start + i * step - point.lower[axis_name] = start + (i - 0.5) * step - point.upper[axis_name] = start + (i + 0.5) * step - - point.indexes = [i] - yield point - def to_dict(self): """Convert object attributes into a dictionary""" diff --git a/scanpointgenerator/generators/lissajousgenerator.py b/scanpointgenerator/generators/lissajousgenerator.py index 008e6ac..f5f71d7 100644 --- a/scanpointgenerator/generators/lissajousgenerator.py +++ b/scanpointgenerator/generators/lissajousgenerator.py @@ -81,26 +81,6 @@ def produce_points(self): self.bounds[self.names[0]] = bx self.bounds[self.names[1]] = by - def _calc(self, i): - """Calculate the coordinate for a given index""" - x = self.centre[0] + \ - self.x_max * m.sin(self.x_freq * i * self.increment + - self.phase_diff) - y = self.centre[1] + \ - self.y_max * m.sin(self.y_freq * i * self.increment) - - return x, y - - def iterator(self): - for i in range_(self.num): - p = Point() - p.positions[self.names[0]], p.positions[self.names[1]] = self._calc(i) - p.lower[self.names[0]], p.lower[self.names[1]] = self._calc(i - 0.5) - p.upper[self.names[0]], p.upper[self.names[1]] = self._calc(i + 0.5) - p.indexes = [i] - - yield p - def to_dict(self): """Convert object attributes into a dictionary""" diff --git a/scanpointgenerator/generators/spiralgenerator.py b/scanpointgenerator/generators/spiralgenerator.py index 34ff3a5..a579ec9 100644 --- a/scanpointgenerator/generators/spiralgenerator.py +++ b/scanpointgenerator/generators/spiralgenerator.py @@ -37,12 +37,8 @@ def __init__(self, names, units, centre, radius, scale=1.0, raise ValueError("Axis names cannot be duplicated; given %s" % names) - self.alpha = m.sqrt(4 * m.pi) # Theta scale factor - self.beta = scale / (2 * m.pi) # Radius scale factor - self.num = self._end_point(self.radius) + 1 self.position_units = {names[0]: units, names[1]: units} - self.index_dims = [self._end_point(self.radius)] gen_name = "Spiral" for axis_name in self.names[::-1]: gen_name = axis_name + "_" + gen_name @@ -50,21 +46,25 @@ def __init__(self, names, units, centre, radius, scale=1.0, self.axes = self.names # For GDA - def produce_points(self): - self.points = {} - self.bounds = {} - # spiral equation : r = b * phi # scale = 2 * pi * b # parameterise phi with approximation: # phi(t) = k * sqrt(t) (for some k) # number of possible t is solved by sqrt(t) = max_r / b*k - b = self.scale / (2 * m.pi) - k = m.sqrt(4 * m.pi) # magic scaling factor for our angle steps - size = (self.radius) / (b * k) - size *= size - size = int(size) + 1 # TODO: Why the +1 ??? + self.alpha = m.sqrt(4 * m.pi) # Theta scale factor = k + self.beta = scale / (2 * m.pi) # Radius scale factor = b + self.num = int((self.radius / (self.alpha * self.beta)) ** 2) + 1 + + def produce_points(self): + self.points = {} + self.bounds = {} + + b = self.beta + k = self.alpha + size = self.num + # parameterise phi with approximation: + # phi(t) = k * sqrt(t) (for some k) phi_t = lambda t: k * np.sqrt(t + 0.5) phi = phi_t(np.arange(size)) x = self.centre[0] + b * phi * np.sin(phi) @@ -80,32 +80,6 @@ def produce_points(self): self.bounds[self.names[0]] = bx self.bounds[self.names[1]] = by - def _calc(self, i): - """Calculate the coordinate for a given index""" - theta = self.alpha * m.sqrt(i) - radius = self.beta * theta - x = self.centre[0] + radius * m.sin(theta) - y = self.centre[1] + radius * m.cos(theta) - - return x, y - - def _end_point(self, radius): - """Calculate the index of the final point contained by circle""" - return int((radius / (self.alpha * self.beta)) ** 2) - - def iterator(self): - for i in range_(0, self._end_point(self.radius) + 1): - p = Point() - p.indexes = [i] - - i += 0.5 # Offset so lower bound of first point is not less than 0 - - p.positions[self.names[0]], p.positions[self.names[1]] = self._calc(i) - p.upper[self.names[0]], p.upper[self.names[1]] = self._calc(i + 0.5) - p.lower[self.names[0]], p.lower[self.names[1]] = self._calc(i - 0.5) - - yield p - def to_dict(self): """Convert object attributes into a dictionary""" diff --git a/scanpointgenerator/plotgenerator.py b/scanpointgenerator/plotgenerator.py index f884b75..7f96c20 100644 --- a/scanpointgenerator/plotgenerator.py +++ b/scanpointgenerator/plotgenerator.py @@ -1,3 +1,5 @@ +from scanpointgenerator import CompoundGenerator + MARKER_SIZE = 10 @@ -16,6 +18,11 @@ def plot_generator(gen, excluder=None, show_indexes=True): if roi.name == "Circle": overlay.add_patch(Circle(roi.centre, roi.radius, fill=False)) + if not isinstance(gen, CompoundGenerator): + excluders = [] if excluder is None else [excluder] + gen = CompoundGenerator([gen], excluders, []) + gen.prepare() + # points for spline generation x, y = [], [] # capture points and indexes @@ -100,6 +107,6 @@ def plot_generator(gen, excluder=None, show_indexes=True): for i, x, y in zip(capi, capx, capy): plt.annotate(i, (x, y), xytext=(MARKER_SIZE/2, MARKER_SIZE/2), textcoords='offset points') - indexes = ["%s (size %d)" % z for z in zip(gen.index_names, gen.index_dims)] - plt.title("Dataset: [%s]" % (", ".join(indexes))) + #indexes = ["%s (size %d)" % z for z in zip(gen.index_names, gen.index_dims)] + #plt.title("Dataset: [%s]" % (", ".join(indexes))) plt.show() diff --git a/tests/test_core/test_generator.py b/tests/test_core/test_generator.py index bb9376a..5c4fe5b 100644 --- a/tests/test_core/test_generator.py +++ b/tests/test_core/test_generator.py @@ -20,7 +20,6 @@ def test_init(self): self.assertEqual(self.g.position_units, None) self.assertEqual(self.g.index_dims, None) self.assertEqual(self.g.index_names, None) - self.assertRaises(NotImplementedError, self.g.iterator) def test_to_dict_raises(self): with self.assertRaises(NotImplementedError): diff --git a/tests/test_generators/test_linegenerator.py b/tests/test_generators/test_linegenerator.py index a13c243..a8ef7c0 100644 --- a/tests/test_generators/test_linegenerator.py +++ b/tests/test_generators/test_linegenerator.py @@ -9,22 +9,21 @@ class LineGeneratorTest(ScanPointGeneratorTest): - def setUp(self): - self.g = LineGenerator("x", "mm", 1.0, 9.0, 5, alternate_direction=True) - def test_init(self): - self.assertEqual(self.g.position_units, dict(x="mm")) - self.assertEqual(self.g.index_dims, [5]) - self.assertEqual(self.g.index_names, ["x"]) - self.assertEqual(self.g.axes, ["x"]) + g = LineGenerator("x", "mm", 1.0, 9.0, 5, alternate_direction=True) + self.assertEqual(dict(x="mm"), g.position_units) + self.assertEqual([5], g.index_dims) + self.assertEqual(["x"], g.index_names) + self.assertEqual(["x"], g.axes) def test_array_positions(self): + g = LineGenerator("x", "mm", 1.0, 9.0, 5, alternate_direction=True) positions = [1.0, 3.0, 5.0, 7.0, 9.0] bounds = [0.0, 2.0, 4.0, 6.0, 8.0, 10.0] indexes = [0, 1, 2, 3, 4] - self.g.produce_points() - self.assertEqual(positions, self.g.points['x'].tolist()) - self.assertEqual(bounds, self.g.bounds['x'].tolist()) + g.produce_points() + self.assertEqual(positions, g.points['x'].tolist()) + self.assertEqual(bounds, g.bounds['x'].tolist()) def test_negative_direction(self): g = LineGenerator("x", "mm", 2, -2, 5) @@ -40,23 +39,12 @@ def test_single_point(self): self.assertEqual([1.0], g.points["x"].tolist()) self.assertEqual([-0.5, 2.5], g.bounds["x"].tolist()) - def test_iterator(self): - positions = [1.0, 3.0, 5.0, 7.0, 9.0] - lower = [0.0, 2.0, 4.0, 6.0, 8.0] - upper = [2.0, 4.0, 6.0, 8.0, 10.0] - indexes = [0, 1, 2, 3, 4] - for i, p in enumerate(self.g.iterator()): - self.assertEqual(p.positions, dict(x=positions[i])) - self.assertEqual(p.lower, dict(x=lower[i])) - self.assertEqual(p.upper, dict(x=upper[i])) - self.assertEqual(p.indexes, [indexes[i]]) - self.assertEqual(i, 4) - def test_duplicate_name_raises(self): with self.assertRaises(ValueError): LineGenerator(["x", "x"], "mm", 0.0, 1.0, 5) def test_to_dict(self): + g = LineGenerator("x", "mm", 1.0, 9.0, 5, alternate_direction=True) expected_dict = dict() expected_dict['typeid'] = "scanpointgenerator:generator/LineGenerator:1.0" expected_dict['name'] = ["x"] @@ -66,8 +54,7 @@ def test_to_dict(self): expected_dict['num'] = 5 expected_dict['alternate_direction'] = True - d = self.g.to_dict() - + d = g.to_dict() self.assertEqual(expected_dict, d) def test_from_dict(self): @@ -91,40 +78,34 @@ def test_from_dict(self): self.assertEqual(5, gen.num) self.assertTrue(gen.alternate_direction) - class LineGenerator2DTest(ScanPointGeneratorTest): - def setUp(self): - self.g = LineGenerator(["x", "y"], "mm", [1.0, 2.0], [5.0, 10.0], 5) - def test_init(self): - self.assertEqual(self.g.position_units, dict(x="mm", y="mm")) - self.assertEqual(self.g.index_dims, [5]) - self.assertEqual(self.g.index_names, ["x_y_Line"]) - self.assertEqual(self.g.axes, ["x", "y"]) + g = LineGenerator(["x", "y"], "mm", [1.0, 2.0], [5.0, 10.0], 5) + self.assertEqual(dict(x="mm", y="mm"), g.position_units) + self.assertEqual([5], g.index_dims) + self.assertEqual(["x_y_Line"], g.index_names) + self.assertEqual(["x", "y"], g.axes) def test_given_inconsistent_dims_then_raise_error(self): - with self.assertRaises(ValueError): LineGenerator("x", "mm", [1.0], [5.0, 10.0], 5) def test_give_one_point_then_step_zero(self): l = LineGenerator(["1", "2", "3", "4", "5"], "mm", [0.0]*5, [10.0]*5, 1) - self.assertEqual(l.step, [0]*5) + self.assertEqual([0]*5, l.step) - def test_iterator(self): + def test_array_positions(self): + g = LineGenerator(["x", "y"], "mm", [1.0, 2.0], [5.0, 10.0], 5) + g.produce_points() x_positions = [1.0, 2.0, 3.0, 4.0, 5.0] y_positions = [2.0, 4.0, 6.0, 8.0, 10.0] - lower = [0.5, 1.5, 2.5, 3.5, 4.5] - upper = [1.5, 2.5, 3.5, 4.5, 5.5] - indexes = [0, 1, 2, 3, 4] - for i, p in enumerate(self.g.iterator()): - self.assertEqual(p.positions, dict(x=x_positions[i], - y=y_positions[i])) - self.assertEqual(p.lower["x"], lower[i]) - self.assertEqual(p.upper["x"], upper[i]) - self.assertEqual(p.indexes, [indexes[i]]) - self.assertEqual(i, 4) + x_bounds = [0.5, 1.5, 2.5, 3.5, 4.5, 5.5] + y_bounds = [1, 3, 5, 7, 9, 11] + self.assertEqual(x_positions, g.points['x'].tolist()) + self.assertEqual(y_positions, g.points['y'].tolist()) + self.assertEqual(x_bounds, g.bounds['x'].tolist()) + self.assertEqual(y_bounds, g.bounds['y'].tolist()) if __name__ == "__main__": unittest.main() diff --git a/tests/test_generators/test_lissajousgenerator.py b/tests/test_generators/test_lissajousgenerator.py index fb3e8da..5077d8a 100644 --- a/tests/test_generators/test_lissajousgenerator.py +++ b/tests/test_generators/test_lissajousgenerator.py @@ -23,51 +23,6 @@ def test_duplicate_name_raises(self): with self.assertRaises(ValueError): LissajousGenerator(["x", "x"], "mm", dict(), 1) - def test_iterator(self): - g = LissajousGenerator(['x', 'y'], "mm", self.bounding_box, 1, num_points=10) - positions = [{'y': 0.0, 'x': 0.5}, - {'y': 0.47552825814757677, 'x': 0.4045084971874737}, - {'y': 0.2938926261462366, 'x': 0.15450849718747375}, - {'y': -0.2938926261462365, 'x': -0.15450849718747364}, - {'y': -0.4755282581475768, 'x': -0.40450849718747367}, - {'y': -1.2246467991473532e-16, 'x': -0.5}, - {'y': 0.47552825814757677, 'x': -0.4045084971874738}, - {'y': 0.2938926261462367, 'x': -0.1545084971874738}, - {'y': -0.2938926261462364, 'x': 0.1545084971874736}, - {'y': -0.4755282581475769, 'x': 0.4045084971874736}] - lower = [{'y': -0.29389262614623657, 'x': 0.47552825814757677}, - {'y': 0.29389262614623657, 'x': 0.4755282581475768}, - {'y': 0.4755282581475768, 'x': 0.2938926261462366}, - {'y': 6.123233995736766e-17, 'x': 6.123233995736766e-17}, - {'y': -0.47552825814757677, 'x': -0.2938926261462365}, - {'y': -0.2938926261462367, 'x': -0.47552825814757677}, - {'y': 0.29389262614623646, 'x': -0.4755282581475768}, - {'y': 0.4755282581475768, 'x': -0.2938926261462367}, - {'y': 1.8369701987210297e-16, 'x': -1.2246467991473532e-16}, - {'y': -0.4755282581475767, 'x': 0.29389262614623646}] - upper = [{'y': 0.29389262614623657, 'x': 0.4755282581475768}, - {'y': 0.4755282581475768, 'x': 0.2938926261462366}, - {'y': 6.123233995736766e-17, 'x': 6.123233995736766e-17}, - {'y': -0.47552825814757677, 'x': -0.2938926261462365}, - {'y': -0.2938926261462367, 'x': -0.47552825814757677}, - {'y': 0.29389262614623646, 'x': -0.4755282581475768}, - {'y': 0.4755282581475768, 'x': -0.2938926261462367}, - {'y': 1.8369701987210297e-16, 'x': -1.2246467991473532e-16}, - {'y': -0.4755282581475767, 'x': 0.29389262614623646}, - {'y': -0.29389262614623674, 'x': 0.47552825814757677}] - indexes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - - points = list(g.iterator()) - self.assertEqual(10, len(points)) - result = [{'x':p.positions['x'], 'y':p.positions['y']} for p in points] - self.assertEquals(result, positions) - result = [{'x':p.upper['x'], 'y':p.upper['y']} for p in points] - self.assertEquals(result, upper) - result = [{'x':p.lower['x'], 'y':p.lower['y']} for p in points] - self.assertEquals(result, lower) - result = [p.indexes[0] for p in points] - self.assertEquals(result, indexes) - def test_array_positions(self): g = LissajousGenerator(['x', 'y'], "mm", self.bounding_box, 1, num_points=10) positions = [{'y': 0.0, 'x': 0.5}, diff --git a/tests/test_generators/test_spiralgenerator.py b/tests/test_generators/test_spiralgenerator.py index 2012cbf..0f6112d 100644 --- a/tests/test_generators/test_spiralgenerator.py +++ b/tests/test_generators/test_spiralgenerator.py @@ -14,8 +14,6 @@ def setUp(self): def test_init(self): self.assertEqual(self.g.position_units, dict(x="mm", y="mm")) - self.assertEqual(self.g.index_dims, [6]) - self.assertEqual(self.g.index_names, ["x_y_Spiral"]) self.assertEqual(self.g.axes, ["x", "y"]) def test_duplicate_name_raises(self): @@ -44,37 +42,6 @@ def test_array_positions(self): self.assertEqual(positions, p) self.assertEqual(bounds, b) - def test_iterator(self): - positions = [{'y': -0.3211855677650875, 'x': 0.23663214944574582}, - {'y': -0.25037538922751695, 'x': -0.6440318266552169}, - {'y': 0.6946549630820702, 'x': -0.5596688286164636}, - {'y': 0.9919687803189761, 'x': 0.36066957248394327}, - {'y': 0.3924587351155914, 'x': 1.130650533568409}, - {'y': -0.5868891557832875, 'x': 1.18586065489788}, - {'y': -1.332029488076613, 'x': 0.5428735608675326}] - lower = [{'y': 0.0, 'x': 0.0}, - {'y': -0.5189218293602549, 'x': -0.2214272368007088}, - {'y': 0.23645222432582483, 'x': -0.7620433832656455}, - {'y': 0.9671992383675001, 'x': -0.13948222773063082}, - {'y': 0.7807653675717078, 'x': 0.8146440851904461}, - {'y': -0.09160107657707395, 'x': 1.2582363345925418}, - {'y': -1.0190886264001306, 'x': 0.9334439933089926}] - upper = [{'y': -0.5189218293602549, 'x': -0.2214272368007088}, - {'y': 0.23645222432582483, 'x': -0.7620433832656455}, - {'y': 0.9671992383675001, 'x': -0.13948222773063082}, - {'y': 0.7807653675717078, 'x': 0.8146440851904461}, - {'y': -0.09160107657707395, 'x': 1.2582363345925418}, - {'y': -1.0190886264001306, 'x': 0.9334439933089926}, - {'y': -1.4911377166541206, 'x': 0.06839234794968006}] - indexes = [0, 1, 2, 3, 4, 5, 6] - - for i, p in enumerate(self.g.iterator()): - self.assertEqual(positions[i], p.positions) - self.assertEqual(lower[i], p.lower) - self.assertEqual(upper[i], p.upper) - self.assertEqual(p.indexes, [indexes[i]]) - self.assertEqual(i, 6) - def test_to_dict(self): expected_dict = dict() expected_dict['typeid'] = "scanpointgenerator:generator/SpiralGenerator:1.0" diff --git a/tests/test_mutators/test_randomoffsetmutator.py b/tests/test_mutators/test_randomoffsetmutator.py index 949f787..2e4e986 100644 --- a/tests/test_mutators/test_randomoffsetmutator.py +++ b/tests/test_mutators/test_randomoffsetmutator.py @@ -4,10 +4,10 @@ import unittest from test_util import ScanPointGeneratorTest -from scanpointgenerator.mutators import RandomOffsetMutator +from scanpointgenerator.compat import range_ from scanpointgenerator.generators import LineGenerator -from scanpointgenerator import CompoundGenerator -from scanpointgenerator import Point +from scanpointgenerator.mutators import RandomOffsetMutator +from scanpointgenerator import Point, CompoundGenerator from pkg_resources import require require("mock") @@ -17,7 +17,6 @@ class RandomOffsetMutatorTest(ScanPointGeneratorTest): def setUp(self): - self.line_gen = LineGenerator("x", "mm", 1.0, 5.0, 5) self.m = RandomOffsetMutator(1, ["x"], dict(x=0.25)) def test_init(self): @@ -76,7 +75,16 @@ def test_mutate(self): 3.355476113375, 4.572636740625, 5.5] indexes = [0, 1, 2, 3, 4] - for i, p in enumerate(self.m.mutate(self.line_gen.iterator())): + def test_iterator(): + for x in range_(1, 6): + p = Point() + p.positions['x'] = x + p.lower['x'] = x - 0.5 + p.upper['x'] = x + 0.5 + p.indexes = [x - 1] + yield p + + for i, p in enumerate(self.m.mutate(test_iterator())): self.assertAlmostEqual(p.positions['x'], positions[i], places=10) self.assertAlmostEqual(p.lower['x'], lower[i], places=10) self.assertAlmostEqual(p.upper['x'], upper[i], places=10) From 3aade7114c0ec3c682096e6371131e247e64aaa3 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Thu, 24 Nov 2016 14:20:42 +0000 Subject: [PATCH 25/47] Restore index_dims attribute to CompoundGenerator --- scanpointgenerator/core/compoundgenerator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index 3201919..c2e44f1 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -27,6 +27,7 @@ def __init__(self, generators, excluders, mutators): self.mutators = mutators self.axes = [] self.position_units = {} + self.index_dims = [] self.dimensions = [] self.num = 1 self.dim_meta = {} @@ -139,6 +140,7 @@ def prepare(self): self.num *= len(indicies) self.dim_meta[dim]["mask"] = mask self.dim_meta[dim]["indicies"] = indicies + self.index_dims.append(len(indicies)) repeat = self.num tile = 1 From a321c4f5ee427a592170c9df92b6b89b63721037 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Fri, 25 Nov 2016 16:30:03 +0000 Subject: [PATCH 26/47] Tidy-up Lissajous point generation code --- .../generators/lissajousgenerator.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/scanpointgenerator/generators/lissajousgenerator.py b/scanpointgenerator/generators/lissajousgenerator.py index f5f71d7..7d75b7f 100644 --- a/scanpointgenerator/generators/lissajousgenerator.py +++ b/scanpointgenerator/generators/lissajousgenerator.py @@ -67,14 +67,12 @@ def produce_points(self): A, B = self.x_max, self.y_max a, b = self.x_freq, self.y_freq d = self.phase_diff - f = lambda t: x0 + A * np.sin(a * 2 * m.pi * t/self.num + d) - x = f(np.arange(self.num)) - f = lambda t: x0 + A * np.sin(a * 2 * m.pi * (t-0.5)/self.num + d) - bx = f(np.arange(self.num + 1)) - f = lambda t: y0 + B * np.sin(b * 2 * m.pi * t/self.num) - y = f(np.arange(self.num)) - f = lambda t: y0 + B * np.sin(b * 2 * m.pi * (t-0.5)/self.num) - by = f(np.arange(self.num + 1)) + fx = lambda t: x0 + A * np.sin(a * 2*m.pi * t/self.num + d) + fy = lambda t: y0 + B * np.sin(b * 2*m.pi * t/self.num) + x = fx(np.arange(self.num)) + bx = fx(np.arange(self.num + 1) - 0.5) + y = fy(np.arange(self.num)) + by = fy(np.arange(self.num + 1) - 0.5) self.points[self.names[0]] = x self.points[self.names[1]] = y From ca35058a42e1145a133dc172e8133854b6bcdd9c Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Fri, 25 Nov 2016 16:33:04 +0000 Subject: [PATCH 27/47] Remove unused Lock import in CompoundGenerator --- scanpointgenerator/core/compoundgenerator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index c2e44f1..bca2f33 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -1,5 +1,4 @@ import logging -from threading import Lock from scanpointgenerator.compat import range_, np from scanpointgenerator.core.generator import Generator From 5de5594f79e590d28d17bf55ee9a0d8e967eff55 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Mon, 5 Dec 2016 14:34:37 +0000 Subject: [PATCH 28/47] Fix LineGenerator point calculation to ensure float division --- scanpointgenerator/generators/linegenerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanpointgenerator/generators/linegenerator.py b/scanpointgenerator/generators/linegenerator.py index 7f22c1c..a6ad95e 100644 --- a/scanpointgenerator/generators/linegenerator.py +++ b/scanpointgenerator/generators/linegenerator.py @@ -86,7 +86,7 @@ def produce_points(self): [start - 0.5 * d, start + 0.5 * d]) else: n = self.num - 1. - s = d / n + s = float(d) / n bound_stop = stop + 0.5 * s bound_start = start - 0.5 * s self.points[axis_name] = np.linspace( From 8ed40636dce802215db6d3cafba60159032afdfa Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Mon, 5 Dec 2016 16:11:54 +0000 Subject: [PATCH 29/47] Add produce_points to Generator and stop CompoundGenerator deriving it. CompoundGenerator is now very different to other Generators. The name "Generator" may now be inappropriate for "regular" generators. --- scanpointgenerator/core/compoundgenerator.py | 5 +++-- scanpointgenerator/core/generator.py | 9 +++++++++ tests/test_core/test_generator.py | 4 ++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index bca2f33..66def8b 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -9,11 +9,12 @@ from scanpointgenerator.generators import LineGenerator -@Generator.register_subclass("scanpointgenerator:generator/CompoundGenerator:1.0") -class CompoundGenerator(Generator): +class CompoundGenerator(object): """Nest N generators, apply exclusion regions to relevant generator pairs and apply any mutators before yielding points""" + typeid = "scanpointgenerator:generator/CompoundGenerator:1.0" + def __init__(self, generators, excluders, mutators): """ Args: diff --git a/scanpointgenerator/core/generator.py b/scanpointgenerator/core/generator.py index 9b427a0..213aa23 100644 --- a/scanpointgenerator/core/generator.py +++ b/scanpointgenerator/core/generator.py @@ -22,6 +22,15 @@ class Generator(object): _generator_lookup = {} axes = [] + def produce_points(self): + """ + Abstract method to create and cache position and bound arrays + + Point arrays should be stored in self.points[axis] and bounds in + self.bounds[axis] + """ + raise NotImplementedError + def to_dict(self): """Abstract method to convert object attributes into a dictionary""" raise NotImplementedError diff --git a/tests/test_core/test_generator.py b/tests/test_core/test_generator.py index 5c4fe5b..5361991 100644 --- a/tests/test_core/test_generator.py +++ b/tests/test_core/test_generator.py @@ -21,6 +21,10 @@ def test_init(self): self.assertEqual(self.g.index_dims, None) self.assertEqual(self.g.index_names, None) + def test_produce_points_raises(self): + with self.assertRaises(NotImplementedError): + self.g.produce_points() + def test_to_dict_raises(self): with self.assertRaises(NotImplementedError): self.g.to_dict() From 903d2adb25bcbabfb2c96b85d138d64c34b4d16a Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Tue, 6 Dec 2016 17:04:14 +0000 Subject: [PATCH 30/47] Remove ArrayGenerator --- docs/arraygenerator.rst | 34 ---- docs/index.rst | 1 - scanpointgenerator/generators/__init__.py | 1 - .../generators/arraygenerator.py | 161 ------------------ tests/test_generators/test_arraygenerator.py | 140 --------------- 5 files changed, 337 deletions(-) delete mode 100644 docs/arraygenerator.rst delete mode 100644 scanpointgenerator/generators/arraygenerator.py delete mode 100644 tests/test_generators/test_arraygenerator.py diff --git a/docs/arraygenerator.rst b/docs/arraygenerator.rst deleted file mode 100644 index fcc28bf..0000000 --- a/docs/arraygenerator.rst +++ /dev/null @@ -1,34 +0,0 @@ -Array Generator -=============== - -.. module:: scanpointgenerator - -.. autoclass:: ArrayGenerator - :members: - -Examples --------- - -The ArrayGenerator takes an N-Dimensional array of coordinates and creates -Points with calculated upper and lower bounds. You can also provide your -own bounds. - -.. plot:: - :include-source: - - from scanpointgenerator import ArrayGenerator, plot_generator - - points = [0.0, 2.0, 3.0, 5.0, 7.0, 8.0] - array = ArrayGenerator("x", "mm", points) - plot_generator(array) - -And a 2D scan. - -.. plot:: - :include-source: - - from scanpointgenerator import ArrayGenerator, plot_generator - - points = [[0.0, 2.0], [2.0, 3.0], [3.0, 5.0], [5.0, 6.0], [7.0, 7.0], [8.0, 9.0]] - array = ArrayGenerator(["x", "y"], "mm", points) - plot_generator(array) \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 7aa44a6..5882a76 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,7 +15,6 @@ Table of Contents linegenerator spiralgenerator lissajousgenerator - arraygenerator .. toctree:: :maxdepth: 1 diff --git a/scanpointgenerator/generators/__init__.py b/scanpointgenerator/generators/__init__.py index 8aafffb..7e5b29d 100644 --- a/scanpointgenerator/generators/__init__.py +++ b/scanpointgenerator/generators/__init__.py @@ -1,4 +1,3 @@ -from scanpointgenerator.generators.arraygenerator import ArrayGenerator from scanpointgenerator.generators.linegenerator import LineGenerator from scanpointgenerator.generators.lissajousgenerator import LissajousGenerator from scanpointgenerator.generators.spiralgenerator import SpiralGenerator diff --git a/scanpointgenerator/generators/arraygenerator.py b/scanpointgenerator/generators/arraygenerator.py deleted file mode 100644 index 587f02a..0000000 --- a/scanpointgenerator/generators/arraygenerator.py +++ /dev/null @@ -1,161 +0,0 @@ -from scanpointgenerator.compat import range_ -from scanpointgenerator.core import Generator -from scanpointgenerator.core import Point - - -@Generator.register_subclass("scanpointgenerator:generator/ArrayGenerator:1.0") -class ArrayGenerator(Generator): - """Generate a given n-dimensional array of points""" - - def __init__(self, name, units, points, lower_bounds=None, upper_bounds=None): - """ - Args: - name (str/list(str)): ND list of scannable names - e.g. "x" or ["x", "y"] - units (str): The scannable units. E.g. "mm" - points (list): List of ND lists of coordinates - e.g. [1.0, 2.0, 3.0] or [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]] - lower_bounds (list): List of ND lists of lower bound coordinates - upper_bounds (list): List of ND lists of upper bound coordinates - """ - - if not isinstance(name, list): - name = [name] - if not isinstance(points[0], list): - points = [[point] for point in points] - if upper_bounds is not None: - upper_bounds = [[point] for point in upper_bounds] - if lower_bounds is not None: - lower_bounds = [[point] for point in lower_bounds] - - self.name = name - self.points = points - self.upper_bounds = upper_bounds - self.lower_bounds = lower_bounds - - if len(self.name) != len(set(self.name)): - raise ValueError("Axis names cannot be duplicated; given %s" % - name) - - for point in self.points: - if len(point) != len(self.name): - raise ValueError( - "Dimensions of name, start and stop do not match") - if self.upper_bounds is not None: - for point in self.upper_bounds: - if len(point) != len(self.name): - raise ValueError( - "Dimensions of name, start and stop do not match") - if self.lower_bounds is not None: - for point in self.lower_bounds: - if len(point) != len(self.name): - raise ValueError( - "Dimensions of name, start and stop do not match") - - self.num = len(points) - - self.position_units = {} - for dimension in self.name: - self.position_units[dimension] = units - self.index_dims = [self.num] - self.index_names = self.name - - self.axes = self.name # For GDA - - def iterator(self): - - for i in range_(self.num): - - point = Point() - for axis, coordinate in enumerate(self.points[i]): - point.positions[self.name[axis]] = coordinate - - if self.upper_bounds is None: - upper = self._calculate_upper_bound(i, axis, coordinate) - else: - upper = self.upper_bounds[i][axis] - point.upper[self.name[axis]] = upper - - if self.lower_bounds is None: - lower = self._calculate_lower_bound(i, axis, coordinate) - else: - lower = self.lower_bounds[i][axis] - point.lower[self.name[axis]] = lower - - point.indexes = [i] - yield point - - def _calculate_upper_bound(self, index, axis, coordinate): - """ - Calculate upper bound for coordinate; if final coordinate then - calculate lower bound and extrapolate upper - - Args: - index(int): Index of coordinate in list - axis(int): Index of coordinate axis in list - coordinate(float): Coordinate to calculate bounds for - - Returns: - float: Upper bound of coordinate - """ - - if index == self.num - 1: - lower = (coordinate + self.points[index - 1][axis]) / 2 - upper = coordinate + (coordinate - lower) - else: - upper = (self.points[index + 1][axis] + coordinate) / 2 - return upper - - def _calculate_lower_bound(self, index, axis, coordinate): - """ - Calculate lower bound for coordinate; if first coordinate then - calculate upper bound and extrapolate lower - - Args: - index(int): Index of coordinate in list - axis(int): Index of coordinate axis in list - coordinate(float): Coordinate to calculate bounds for - - Returns: - float: Lower bound of coordinate - """ - - if index == 0: - upper = (self.points[index + 1][axis] + coordinate) / 2 - lower = coordinate - (upper - coordinate) - else: - lower = (coordinate + self.points[index - 1][axis]) / 2 - return lower - - def to_dict(self): - """Convert object attributes into a dictionary""" - - d = dict() - d['typeid'] = self.typeid - d['name'] = self.name - d['units'] = list(self.position_units.values())[0] - d['points'] = self.points - d['lower_bounds'] = self.lower_bounds - d['upper_bounds'] = self.upper_bounds - - return d - - @classmethod - def from_dict(cls, d): - """ - Create a ArrayGenerator instance from a serialised dictionary - - Args: - d(dict): Dictionary of attributes - - Returns: - ArrayGenerator: New ArrayGenerator instance - """ - - name = d['name'] - units = d['units'] - points = d['points'] - lower_bounds = d['lower_bounds'] - upper_bounds = d['upper_bounds'] - - return cls(name, units, points, lower_bounds, upper_bounds) diff --git a/tests/test_generators/test_arraygenerator.py b/tests/test_generators/test_arraygenerator.py deleted file mode 100644 index e0b7a34..0000000 --- a/tests/test_generators/test_arraygenerator.py +++ /dev/null @@ -1,140 +0,0 @@ -import os -import sys -sys.path.append(os.path.join(os.path.dirname(__file__), "..")) -import unittest - -from test_util import ScanPointGeneratorTest -from scanpointgenerator import ArrayGenerator - -from pkg_resources import require -require("mock") -from mock import patch - - -class LineGeneratorTest(ScanPointGeneratorTest): - - def setUp(self): - test_array = [[0.0, 0.0], [1.0, 5.0], [2.0, 10.0], [3.0, 15.0]] - self.g = ArrayGenerator(["x", "y"], "mm", test_array) - - def test_init(self): - self.assertEqual(self.g.position_units, dict(x="mm", y="mm")) - self.assertEqual(self.g.index_dims, [4]) - self.assertEqual(self.g.index_names, ["x", "y"]) - self.assertEqual(self.g.axes, ["x", "y"]) - - def test_duplicate_name_raises(self): - with self.assertRaises(ValueError): - ArrayGenerator(["x", "x"], "mm", [0.0, 1.0]) - - def test_init_with_simple_name_and_points(self): - g = ArrayGenerator("x", "mm", [1.0, 2.0, 3.0], - [0.5, 1.5, 2.5], - [1.5, 2.5, 3.5]) - self.assertEqual(g.position_units, dict(x="mm")) - self.assertEqual(g.index_dims, [3]) - self.assertEqual(g.index_names, ["x"]) - self.assertEqual(g.points, [[1.0], [2.0], [3.0]]) - self.assertEqual(g.lower_bounds, [[0.5], [1.5], [2.5]]) - self.assertEqual(g.upper_bounds, [[1.5], [2.5], [3.5]]) - - def test_inconsistent_dimensions_raises(self): - with self.assertRaises(ValueError): - ArrayGenerator(["x"], "mm", [[0.0, 1.0], [2.0, 3.0]]) - with self.assertRaises(ValueError): - ArrayGenerator(["x"], "mm", [[0.0], [1.0], [2.0]], - lower_bounds=[[0.0, 1.0], [2.0, 3.0]]) - with self.assertRaises(ValueError): - ArrayGenerator(["x"], "mm", [[0.0], [1.0], [2.0]], - upper_bounds=[[0.0, 1.0], [2.0, 3.0]]) - - def test_calculate_first_lower_bound(self): - new_bound = self.g._calculate_lower_bound(0, 0, 0.0) - self.assertEqual(-0.5, new_bound) - - def test_calculate_other_lower_bound(self): - new_bound = self.g._calculate_lower_bound(2, 0, 2.0) - self.assertEqual(1.5, new_bound) - - def test_calculate_last_upper_bound(self): - new_bound = self.g._calculate_upper_bound(3, 0, 3.0) - self.assertEqual(3.5, new_bound) - - def test_calculate_other_upper_bound(self): - new_bound = self.g._calculate_upper_bound(1, 0, 1.0) - self.assertEqual(1.5, new_bound) - - @patch('scanpointgenerator.arraygenerator.ArrayGenerator._calculate_lower_bound') - @patch('scanpointgenerator.arraygenerator.ArrayGenerator._calculate_upper_bound') - def test_iterator_without_bounds(self, upper_mock, lower_mock): - upper_mock.side_effect = [0.5, 2.5, 1.5, 7.5, 2.5, 12.5, 3.5, 17.5] - lower_mock.side_effect = [-0.5, -2.5, 0.5, 2.5, 1.5, 7.5, 2.5, 12.5] - - positions = [[0.0, 0.0], [1.0, 5.0], [2.0, 10.0], [3.0, 15.0]] - lower = [[-0.5, -2.5], [0.5, 2.5], [1.5, 7.5], [2.5, 12.5]] - upper = [[0.5, 2.5], [1.5, 7.5], [2.5, 12.5], [3.5, 17.5]] - indexes = [0, 1, 2, 3] - - for i, p in enumerate(self.g.iterator()): - self.assertEqual(p.positions, - dict(x=positions[i][0], y=positions[i][1])) - self.assertEqual(p.lower, dict(x=lower[i][0], y=lower[i][1])) - self.assertEqual(p.upper, dict(x=upper[i][0], y=upper[i][1])) - self.assertEqual(p.indexes, [indexes[i]]) - self.assertEqual(i, 3) - - def test_iterator_with_bounds(self): - self.g.lower_bounds = [[-0.5, -2.5], [0.5, 2.5], [1.5, 7.5], [2.5, 12.5]] - self.g.upper_bounds = [[0.5, 2.5], [1.5, 7.5], [2.5, 12.5], [3.5, 17.5]] - - positions = [[0.0, 0.0], [1.0, 5.0], [2.0, 10.0], [3.0, 15.0]] - lower = [[-0.5, -2.5], [0.5, 2.5], [1.5, 7.5], [2.5, 12.5]] - upper = [[0.5, 2.5], [1.5, 7.5], [2.5, 12.5], [3.5, 17.5]] - indexes = [0, 1, 2, 3] - - for i, p in enumerate(self.g.iterator()): - self.assertEqual(p.positions, - dict(x=positions[i][0], y=positions[i][1])) - self.assertEqual(p.lower, dict(x=lower[i][0], y=lower[i][1])) - self.assertEqual(p.upper, dict(x=upper[i][0], y=upper[i][1])) - self.assertEqual(p.indexes, [indexes[i]]) - - def test_to_dict(self): - expected_dict = dict() - expected_dict['typeid'] = "scanpointgenerator:generator/ArrayGenerator:1.0" - expected_dict['name'] = ['x', 'y'] - expected_dict['units'] = 'mm' - expected_dict['points'] = [[0.0, 0.0], [1.0, 5.0], [2.0, 10.0], [3.0, 15.0]] - expected_dict['lower_bounds'] = None - expected_dict['upper_bounds'] = None - - d = self.g.to_dict() - - self.assertEqual(expected_dict, d) - - def test_from_dict(self): - points = [[0.0, 0.0], [1.0, 5.0], [2.0, 10.0], [3.0, 15.0]] - _dict = dict() - _dict['name'] = ['x', 'y'] - _dict['units'] = 'mm' - _dict['points'] = points - _dict['lower_bounds'] = None - _dict['upper_bounds'] = None - - units_dict = dict() - units_dict['x'] = 'mm' - units_dict['y'] = 'mm' - - gen = ArrayGenerator.from_dict(_dict) - - self.assertEqual(['x', 'y'], gen.name) - self.assertEqual(units_dict, gen.position_units) - self.assertEqual(points, gen.points) - self.assertIsNone(gen.lower_bounds) - self.assertIsNone(gen.upper_bounds) - -if __name__ == "__main__": - unittest.main() - - - From 146060516c401453f54293ce5bcda6f00d740ca3 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Tue, 6 Dec 2016 17:06:40 +0000 Subject: [PATCH 31/47] Only apply bounds to innermost generator in CompoundGenerator --- scanpointgenerator/core/compoundgenerator.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index 66def8b..b8f2774 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -223,8 +223,12 @@ def get_point(self, n): j_lower, j_upper = j_upper, j_lower for axis in g.axes: point.positions[axis] = g.points[axis][j] - point.lower[axis] = g.bounds[axis][j_lower] - point.upper[axis] = g.bounds[axis][j_upper] + if g is self.generators[-1]: + point.lower[axis] = g.bounds[axis][j_lower] + point.upper[axis] = g.bounds[axis][j_upper] + else: + point.lower[axis] = g.points[axis][j] + point.upper[axis] = g.points[axis][j] return point def to_dict(self): From d16c61d9233356a16f5f78cafe174dbf1bad0be0 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Tue, 6 Dec 2016 17:08:32 +0000 Subject: [PATCH 32/47] Update documentation following vectorisation changes. The documentation structure needs to be reviewed, as it doesn't obviously follow. Some changes may be desirable (particularly wrt produce_points on Generators as it is side-effecting and doesn't return anything). The nature of the index attribute of points from CompoundGenerator is also not spelled out. --- docs/architecture.rst | 23 +++++---- docs/compoundgenerator.rst | 30 ++++++++++- docs/creating.rst | 18 +++---- docs/excluders.rst | 12 +++-- docs/linegenerator.rst | 6 ++- docs/lissajousgenerator.rst | 8 +-- docs/mutators.rst | 10 ++-- docs/serialisation.rst | 8 +-- docs/spiralgenerator.rst | 8 +-- docs/writing.rst | 51 ++++++++----------- scanpointgenerator/core/compoundgenerator.py | 4 ++ .../generators/linegenerator.py | 4 +- scanpointgenerator/plotgenerator.py | 9 ++-- 13 files changed, 111 insertions(+), 80 deletions(-) diff --git a/docs/architecture.rst b/docs/architecture.rst index dc81f27..013e235 100644 --- a/docs/architecture.rst +++ b/docs/architecture.rst @@ -1,8 +1,11 @@ Architecture ============ -Every scan point generator inherits from the ScanPointGenerator baseclass. This -baseclass provides the following API: +Scan points are produced by a :ref:`compoundgenerator` that wraps base generators, +:ref:`excluders` and :ref:`mutators`. + +All Generators inherit from the Generator baseclass, which provides the +following API: .. module:: scanpointgenerator @@ -18,13 +21,11 @@ API: Using the API ------------- -You would use a generator in a step scan like this:: - - >>> for point in generator.iterator(): - >>> for mname, mpos in point.positions(): - >>> motors[mname].move(mpos) - >>> det.write_data_to_index(point.indexes) - - - +A basic use case that uses two generators looks like this:: + cgen = CompoundGenerator([outer_generator, inner_generator], [], []) + cgen.prepare() + for point in cgen.iterator(): + for mname, mpos in point.positions(): + motors[mname].move(mpos) + det.write_data_to_index(point.indexes) diff --git a/docs/compoundgenerator.rst b/docs/compoundgenerator.rst index 8073c5d..1b31809 100644 --- a/docs/compoundgenerator.rst +++ b/docs/compoundgenerator.rst @@ -1,3 +1,5 @@ +.. _compoundgenerator: + Compound Generator ================== @@ -16,7 +18,8 @@ line scan inside it with 5 points. .. plot:: :include-source: - from scanpointgenerator import LineGenerator, CompoundGenerator, plot_generator + from scanpointgenerator import LineGenerator, CompoundGenerator + from scanpointgenerator.plotgenerator import plot_generator xs = LineGenerator("x", "mm", 0.0, 0.5, 5, alternate_direction=False) ys = LineGenerator("y", "mm", 0.0, 0.5, 4) @@ -34,9 +37,32 @@ be run in reverse to give a snake scan. .. plot:: :include-source: - from scanpointgenerator import LineGenerator, CompoundGenerator, plot_generator + from scanpointgenerator import LineGenerator, CompoundGenerator + from scanpointgenerator.plotgenerator import plot_generator xs = LineGenerator("x", "mm", 0.0, 0.5, 5, alternate_direction=True) ys = LineGenerator("y", "mm", 0.0, 0.5, 4) gen = CompoundGenerator([ys, xs], [], []) plot_generator(gen) + + +.. _compoundgenerator_restrictions: + +Restrictions +------------ + +:ref:`excluders` must be defined on axes that are given by consecutive +generators. Generators with axes filtered by an excluder must also have a +common ``alternate_direction`` setting. An exception is made for the outermost +generator as it is not repeated. + +The following is `not` legal:: + + from scanpointgenerator import LineGenerator, CompoundGenerator, \ + Excluder, CircularROI + + xs = LineGenerator("x", "mm", 0, 1, 2) + ys = LineGenerator("y", "mm", 0, 1, 2) + zs = LineGenerator("z", "mm", 0, 1, 2) + exc = Excluder(CircularROI([0, 0], 1), ["x", "z"]) + gen = CompoundGenerator([zs, ys, xs], [exc], []) # xs and zs are not consecutive diff --git a/docs/creating.rst b/docs/creating.rst index f5a6bff..00fa8b4 100644 --- a/docs/creating.rst +++ b/docs/creating.rst @@ -3,7 +3,8 @@ Creating a Generator The idea of CompoundGenerator is that you can combine generators, excluders and mutators arbitrarily. The following will show some more extensive examples -to show the capabilities of scanpointgenerator. +to show the capabilities of scanpointgenerator. Remember to account for the +restrictions specified in :ref:`compoundgenerator_restrictions`. A spiral scan with an offset rectangular roi overlay and randomly offset @@ -13,8 +14,8 @@ points in the y direction :include-source: from scanpointgenerator import LineGenerator, SpiralGenerator, \ - CompoundGenerator, Excluder, RandomOffsetMutator, plot_generator - from scanpointgenerator.rectangular_roi import RectangularROI + CompoundGenerator, Excluder, RandomOffsetMutator, RectangularROI + from scanpointgenerator.plotgenerator import plot_generator spiral = SpiralGenerator(["x", "y"], "mm", [0.0, 0.0], 10.0, alternate_direction=True) @@ -36,6 +37,7 @@ A spiral scan at each point of a line scan with alternating direction spiral = SpiralGenerator(["x", "y"], "mm", [0.0, 0.0], 1.2, alternate_direction=True) gen = CompoundGenerator([line, spiral], [], []) + gen.prepare() for point in gen.iterator(): for axis, value in point.positions.items(): @@ -57,21 +59,20 @@ A spiral scan at each point of a line scan with alternating direction {'y': 0.695, 'x': -0.56, 'z': 20.0} {'y': 0.992, 'x': 0.361, 'z': 20.0} -Three nested line scans with an excluder operating on the innermost and -outermost axes +Three nested line scans with an excluder operating on the two innermost axes .. plot:: :include-source: from scanpointgenerator import LineGenerator, CompoundGenerator, \ - Excluder - from scanpointgenerator.circular_roi import CircularROI + Excluder, CircularROI line1 = LineGenerator("x", "mm", 0.0, 2.0, 3) line2 = LineGenerator("y", "mm", 0.0, 1.0, 2) line3 = LineGenerator("z", "mm", 0.0, 1.0, 2) - circle = Excluder(CircularROI([1.0, 1.0], 1.0), ["x", "z"]) + circle = Excluder(CircularROI([1.0, 1.0], 1.0), ["x", "y"]) gen = CompoundGenerator([line3, line2, line1], [circle], []) + gen.prepare() for point in gen.iterator(): print(point.positions) @@ -86,4 +87,3 @@ outermost axes {'y': 1.0, 'x': 0.0, 'z': 1.0} {'y': 1.0, 'x': 1.0, 'z': 1.0} {'y': 1.0, 'x': 2.0, 'z': 1.0} - diff --git a/docs/excluders.rst b/docs/excluders.rst index 03d0c98..7def404 100644 --- a/docs/excluders.rst +++ b/docs/excluders.rst @@ -1,3 +1,5 @@ +.. _excluders: + Excluders ========= @@ -18,8 +20,8 @@ Here we use a CircularROI to filter the points of a snake scan :include-source: from scanpointgenerator import LineGenerator, CompoundGenerator, \ - Excluder, plot_generator - from scanpointgenerator.circular_roi import CircularROI + Excluder, CircularROI + from scanpointgenerator.plotgenerator import plot_generator x = LineGenerator("x", "mm", 0.0, 4.0, 5, alternate_direction=True) y = LineGenerator("y", "mm", 0.0, 3.0, 4) @@ -33,12 +35,12 @@ And with the excluder applied :include-source: from scanpointgenerator import LineGenerator, CompoundGenerator, \ - Excluder, plot_generator - from scanpointgenerator.circular_roi import CircularROI + Excluder, CircularROI + from scanpointgenerator.plotgenerator import plot_generator x = LineGenerator("x", "mm", 0.0, 4.0, 5, alternate_direction=True) y = LineGenerator("y", "mm", 0.0, 3.0, 4) circle = Excluder(CircularROI([2.0, 1.0], 2.0), ["x", "y"]) excluder = Excluder(circle, ['x', 'y']) gen = CompoundGenerator([y, x], [circle], []) - plot_generator(gen, circle) \ No newline at end of file + plot_generator(gen, circle) diff --git a/docs/linegenerator.rst b/docs/linegenerator.rst index d032ab9..c6d7aad 100644 --- a/docs/linegenerator.rst +++ b/docs/linegenerator.rst @@ -17,7 +17,8 @@ of each capture point. .. plot:: :include-source: - from scanpointgenerator import LineGenerator, plot_generator + from scanpointgenerator import LineGenerator + from scanpointgenerator.plotgenerator import plot_generator gen = LineGenerator("x", "mm", 0.0, 1.0, 5) plot_generator(gen) @@ -27,7 +28,8 @@ LineGenerator is N dimensional; just pass in ND lists for name, start and stop. .. plot:: :include-source: - from scanpointgenerator import LineGenerator, plot_generator + from scanpointgenerator import LineGenerator + from scanpointgenerator.plotgenerator import plot_generator gen = LineGenerator(["x", "y"], "mm", [1.0, 2.0], [5.0, 10.0], 5) plot_generator(gen) diff --git a/docs/lissajousgenerator.rst b/docs/lissajousgenerator.rst index 0fe0818..86c834a 100644 --- a/docs/lissajousgenerator.rst +++ b/docs/lissajousgenerator.rst @@ -15,7 +15,8 @@ will be scanned over a 3x4 lobe Lissajous curve with filling a 1x1mm rectangle. .. plot:: :include-source: - from scanpointgenerator import LissajousGenerator, plot_generator + from scanpointgenerator import LissajousGenerator + from scanpointgenerator.plotgenerator import plot_generator box = dict(centre=[0.0, 0.0], width=1.0, height=1.0) gen = LissajousGenerator(['x', 'y'], "mm", box=box, num_lobes=3, num_points=50) @@ -27,8 +28,9 @@ visible. The following plot is for 10x11 lobes with the default number of points .. plot:: :include-source: - from scanpointgenerator import LissajousGenerator, plot_generator + from scanpointgenerator import LissajousGenerator + from scanpointgenerator.plotgenerator import plot_generator box = dict(centre=[0.0, 0.0], width=1.0, height=1.0) gen = LissajousGenerator(['x', 'y'], "mm", box=box, num_lobes=20) - plot_generator(gen, show_indexes=False) \ No newline at end of file + plot_generator(gen, show_indexes=False) diff --git a/docs/mutators.rst b/docs/mutators.rst index f338e14..665f4f0 100644 --- a/docs/mutators.rst +++ b/docs/mutators.rst @@ -1,3 +1,5 @@ +.. _mutators: + Mutators ======== @@ -18,7 +20,8 @@ apply it to a snake scan .. plot:: :include-source: - from scanpointgenerator import LineGenerator, CompoundGenerator, plot_generator + from scanpointgenerator import LineGenerator, CompoundGenerator + from scanpointgenerator.plotgenerator import plot_generator xs = LineGenerator("x", "mm", 0.0, 0.5, 5, alternate_direction=True) ys = LineGenerator("y", "mm", 0.0, 0.5, 4) @@ -30,10 +33,11 @@ And with the random offset .. plot:: :include-source: - from scanpointgenerator import LineGenerator, CompoundGenerator, RandomOffsetMutator, plot_generator + from scanpointgenerator import LineGenerator, CompoundGenerator, RandomOffsetMutator + from scanpointgenerator.plotgenerator import plot_generator xs = LineGenerator("x", "mm", 0.0, 0.5, 5, alternate_direction=True) ys = LineGenerator("y", "mm", 0.0, 0.5, 4) random_offset = RandomOffsetMutator(seed=1, axes = ["x", "y"], max_offset=dict(x=0.05, y=0.05)) gen = CompoundGenerator([ys, xs], [], [random_offset]) - plot_generator(gen) \ No newline at end of file + plot_generator(gen) diff --git a/docs/serialisation.rst b/docs/serialisation.rst index 65e8be8..f87b9ae 100644 --- a/docs/serialisation.rst +++ b/docs/serialisation.rst @@ -123,8 +123,8 @@ As an example of serialising, here is a simple snake scan. .. plot:: :include-source: - from scanpointgenerator import LineGenerator, CompoundGenerator, \ - plot_generator + from scanpointgenerator import LineGenerator, CompoundGenerator + from scanpointgenerator.plotgenerator import plot_generator x = LineGenerator("x", "mm", 0.0, 4.0, 5, alternate_direction=True) y = LineGenerator("y", "mm", 0.0, 3.0, 4) @@ -137,8 +137,8 @@ It is the same after being serialised and deserialised. .. plot:: :include-source: - from scanpointgenerator import LineGenerator, CompoundGenerator, \ - plot_generator + from scanpointgenerator import LineGenerator, CompoundGenerator + from scanpointgenerator.plotgenerator import plot_generator x = LineGenerator("x", "mm", 0.0, 4.0, 5, alternate_direction=True) y = LineGenerator("y", "mm", 0.0, 3.0, 4) diff --git a/docs/spiralgenerator.rst b/docs/spiralgenerator.rst index e6100a8..1d9b9dd 100644 --- a/docs/spiralgenerator.rst +++ b/docs/spiralgenerator.rst @@ -15,7 +15,8 @@ will be scanned in a spiral filling a circle of radius 5mm. .. plot:: :include-source: - from scanpointgenerator import SpiralGenerator, plot_generator + from scanpointgenerator import SpiralGenerator + from scanpointgenerator.plotgenerator import plot_generator gen = SpiralGenerator(["x", "y"], "mm", [0.0, 0.0], 5.0) plot_generator(gen) @@ -25,7 +26,8 @@ In this example the spiral is scaled to be more sparse. .. plot:: :include-source: - from scanpointgenerator import SpiralGenerator, plot_generator + from scanpointgenerator import SpiralGenerator + from scanpointgenerator.plotgenerator import plot_generator gen = SpiralGenerator(["x", "y"], "mm", [0.0, 0.0], 5.0, scale=2.0) - plot_generator(gen) \ No newline at end of file + plot_generator(gen) diff --git a/docs/writing.rst b/docs/writing.rst index d3b85fa..e5e3427 100644 --- a/docs/writing.rst +++ b/docs/writing.rst @@ -6,18 +6,19 @@ Writing new scan point generators Let's walk through the simplest generator, :class:`LineGenerator`, and see how it is written. -.. literalinclude:: ../scanpointgenerator/linegenerator.py - :lines: 2-3 +.. literalinclude:: ../scanpointgenerator/generators/linegenerator.py + :lines: 1-2 -We import the baseclass :class:`Generator` and the :class:`Point` class -that we will be generating instances of. +We import the baseclass :class:`Generator` and the compatibility wrappers +around the Python :py:func:`range` function and the :py:mod:`numpy` module -.. literalinclude:: ../scanpointgenerator/linegenerator.py - :lines: 14-15 +.. literalinclude:: ../scanpointgenerator/generators/linegenerator.py + :lines: 12-14 Our new subclass includes a docstring giving a short explanation of what it does +and registers itself as a subclass of Generator for deserialization purposes. -.. literalinclude:: ../scanpointgenerator/linegenerator.py +.. literalinclude:: ../scanpointgenerator/generators/linegenerator.py :pyobject: LineGenerator.__init__ The initialiser stores the arguments given to it, then generates the three @@ -33,30 +34,20 @@ non grid based scans (like SpiralGenerator), index_dims will typically have less elements, because the last two or more dimensions will be unrolled into one long array. This avoids sparse datasets. -.. literalinclude:: ../scanpointgenerator/linegenerator.py - :pyobject: LineGenerator._calc +.. literalinclude:: ../scanpointgenerator/generators/linegenerator.py + :pyobject: LineGenerator.produce_points -We have a repeated bit of code here, so have pulled it out into a function. It -calculates the position of a point given an index +This is used by CompoundGenerator to create the points for this generator. +In here, we should create an array of points for each axis and store them in +a dictionary attribute, using the axis names for the key. The same should be +done for the boundaries between points. -.. literalinclude:: ../scanpointgenerator/linegenerator.py - :pyobject: LineGenerator.iterator +The dictionaries are {axis_name : numpy float array}: -This is the entry point for external code. It is expecting us to produce a -number of :class:`Point` instances, one for each point in the scan. We are -required to fill in the following dictionaries of str position_name -> float -position: +- self.points: The capture positions corresponding to the centre of + the scan frame +- self.bounds: The boundary between points for continuous scanning. -- positions: The capture position corresponding to the centre of the scan frame -- lower: The lower bound of the scan frame if the scan is to be used for - continuous scanning -- upper: The upper bound of the scan frame if the scan is to be used for - continuous scanning - -We also fill in the list of datapoint indexes: - -- indexes: The index into the dataset that the data frame should be stored in - -The `yield` keyword turns the python function into a generator, which can then -be used by the external program to iterate through points without evaluating -them all at the start. +As a rule, if the position of points can be parameterised by +``[f(t) for t in range(num_points)]`` then the bounds should be given by +``[f(t - 0.5) for t in range(num_points + 1)]`` diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index b8f2774..c4cbec7 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -178,8 +178,11 @@ def iterator(self): def get_point(self, n): """ Retrieve the desired point from the generator + Args: n (int): point to be generated + Returns: + Point: The requested point """ if n >= self.num: @@ -244,6 +247,7 @@ def to_dict(self): def from_dict(cls, d): """ Create a CompoundGenerator instance from a serialised dictionary + Args: d(dict): Dictionary of attributes Returns: diff --git a/scanpointgenerator/generators/linegenerator.py b/scanpointgenerator/generators/linegenerator.py index a6ad95e..bd3f03b 100644 --- a/scanpointgenerator/generators/linegenerator.py +++ b/scanpointgenerator/generators/linegenerator.py @@ -1,6 +1,5 @@ from scanpointgenerator.compat import range_, np from scanpointgenerator.core import Generator -from scanpointgenerator.core import Point def to_list(value): @@ -33,8 +32,7 @@ def __init__(self, name, units, start, stop, num, alternate_direction=False): self.stop = to_list(stop) self.alternate_direction = alternate_direction self.points = None - self.points_lower = None - self.points_upper = None + self.bounds = None self.units = units if len(self.name) != len(set(self.name)): diff --git a/scanpointgenerator/plotgenerator.py b/scanpointgenerator/plotgenerator.py index 7f96c20..06ae012 100644 --- a/scanpointgenerator/plotgenerator.py +++ b/scanpointgenerator/plotgenerator.py @@ -1,4 +1,4 @@ -from scanpointgenerator import CompoundGenerator +from scanpointgenerator import CompoundGenerator, RectangularROI, CircularROI MARKER_SIZE = 10 @@ -12,10 +12,9 @@ def plot_generator(gen, excluder=None, show_indexes=True): if excluder is not None: roi = excluder.roi overlay = plt.subplot(111, aspect='equal') - if roi.name == "Rectangle": - lower_left = (roi.centre[0] - roi.width/2, roi.centre[1] - roi.height/2) - overlay.add_patch(Rectangle(lower_left, roi.width, roi.height, fill=False)) - if roi.name == "Circle": + if isinstance(roi, RectangularROI): + overlay.add_patch(Rectangle(roi.start, roi.width, roi.height, fill=False)) + if isinstance(roi, CircularROI): overlay.add_patch(Circle(roi.centre, roi.radius, fill=False)) if not isinstance(gen, CompoundGenerator): From a142209ec1d345d0df7660886ec8f132fc9f6e17 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Thu, 15 Dec 2016 15:07:14 +0000 Subject: [PATCH 33/47] Rename generator.points to generator.positions --- scanpointgenerator/core/compoundgenerator.py | 27 +++++++-------- .../generators/linegenerator.py | 8 ++--- .../generators/lissajousgenerator.py | 11 +++--- .../generators/spiralgenerator.py | 11 +++--- tests/test_core/test_compoundgenerator.py | 34 +++++++++---------- tests/test_generators/test_linegenerator.py | 10 +++--- .../test_lissajousgenerator.py | 6 ++-- tests/test_generators/test_spiralgenerator.py | 2 +- 8 files changed, 53 insertions(+), 56 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index c4cbec7..94b3ec1 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -71,15 +71,15 @@ def prepare(self): gen_1.produce_points() gen_2.produce_points() valid = np.full(gen_1.num, True, dtype=np.int8) - valid &= \ - gen_1.points[axis_1] <= rect.roi.width + rect.roi.start[0] - valid &= gen_1.points[axis_1] >= rect.roi.start[0] - points_1 = gen_1.points[axis_1][valid.astype(np.bool)] + valid &= gen_1.positions[axis_1] \ + <= rect.roi.width + rect.roi.start[0] + valid &= gen_1.positions[axis_1] >= rect.roi.start[0] + points_1 = gen_1.positions[axis_1][valid.astype(np.bool)] valid = np.full(gen_2.num, True, dtype=np.int8) - valid &= \ - gen_2.points[axis_2] <= rect.roi.height + rect.roi.start[1] - valid &= gen_2.points[axis_2] >= rect.roi.start[1] - points_2 = gen_2.points[axis_2][valid.astype(np.bool)] + valid &= gen_2.positions[axis_2] \ + <= rect.roi.height + rect.roi.start[1] + valid &= gen_2.positions[axis_2] >= rect.roi.start[1] + points_2 = gen_2.positions[axis_2][valid.astype(np.bool)] new_gen1 = LineGenerator( gen_1.name, gen_1.units, points_1[0], points_1[-1], len(points_1), gen_1.alternate_direction) @@ -225,13 +225,13 @@ def get_point(self, n): # so bounds are swapped j_lower, j_upper = j_upper, j_lower for axis in g.axes: - point.positions[axis] = g.points[axis][j] + point.positions[axis] = g.positions[axis][j] if g is self.generators[-1]: point.lower[axis] = g.bounds[axis][j_lower] point.upper[axis] = g.bounds[axis][j_upper] else: - point.lower[axis] = g.points[axis][j] - point.upper[axis] = g.points[axis][j] + point.lower[axis] = g.positions[axis][j] + point.upper[axis] = g.positions[axis][j] return point def to_dict(self): @@ -274,8 +274,8 @@ def apply_excluder(self, excluder): axis_outer = excluder.scannables[1] gen_inner = [g for g in self.generators if axis_inner in g.axes][0] gen_outer = [g for g in self.generators if axis_outer in g.axes][0] - points_x = gen_inner.points[axis_inner] - points_y = gen_outer.points[axis_outer] + points_x = gen_inner.positions[axis_inner] + points_y = gen_outer.positions[axis_outer] if self.generators.index(gen_inner) > self.generators.index(gen_outer): gen_inner, gen_outer = gen_outer, gen_inner axis_inner, axis_outer = axis_outer, axis_inner @@ -367,4 +367,3 @@ def merge_dimensions(outer, inner): dim.alternate = outer.alternate or inner.alternate dim.size = outer.size * inner.size return dim - diff --git a/scanpointgenerator/generators/linegenerator.py b/scanpointgenerator/generators/linegenerator.py index bd3f03b..e20f850 100644 --- a/scanpointgenerator/generators/linegenerator.py +++ b/scanpointgenerator/generators/linegenerator.py @@ -31,7 +31,7 @@ def __init__(self, name, units, start, stop, num, alternate_direction=False): self.start = to_list(start) self.stop = to_list(stop) self.alternate_direction = alternate_direction - self.points = None + self.positions = None self.bounds = None self.units = units @@ -71,7 +71,7 @@ def __init__(self, name, units, start, stop, num, alternate_direction=False): self.axes = self.name # For GDA def produce_points(self): - self.points = {} + self.positions = {} self.bounds = {} for axis in range_(self.num_axes): axis_name = self.name[axis] @@ -79,7 +79,7 @@ def produce_points(self): stop = self.stop[axis] d = stop - start if self.num == 1: - self.points[axis_name] = np.array([start]) + self.positions[axis_name] = np.array([start]) self.bounds[axis_name] = np.array( [start - 0.5 * d, start + 0.5 * d]) else: @@ -87,7 +87,7 @@ def produce_points(self): s = float(d) / n bound_stop = stop + 0.5 * s bound_start = start - 0.5 * s - self.points[axis_name] = np.linspace( + self.positions[axis_name] = np.linspace( float(start), float(stop), self.num) self.bounds[axis_name] = np.linspace( float(bound_start), float(bound_stop), self.num + 1) diff --git a/scanpointgenerator/generators/lissajousgenerator.py b/scanpointgenerator/generators/lissajousgenerator.py index 7d75b7f..675b52e 100644 --- a/scanpointgenerator/generators/lissajousgenerator.py +++ b/scanpointgenerator/generators/lissajousgenerator.py @@ -25,9 +25,8 @@ def __init__(self, names, units, box, num_lobes, self.names = names self.units = units - self.points = None - self.points_lower = None - self.points_upper = None + self.positions = None + self.bounds = None self.alternate_direction = alternate_direction if len(self.names) != len(set(self.names)): @@ -60,7 +59,7 @@ def __init__(self, names, units, box, num_lobes, self.axes = self.names # For GDA def produce_points(self): - self.points = {} + self.positions = {} self.bounds = {} x0, y0 = self.centre[0], self.centre[1] @@ -74,8 +73,8 @@ def produce_points(self): y = fy(np.arange(self.num)) by = fy(np.arange(self.num + 1) - 0.5) - self.points[self.names[0]] = x - self.points[self.names[1]] = y + self.positions[self.names[0]] = x + self.positions[self.names[1]] = y self.bounds[self.names[0]] = bx self.bounds[self.names[1]] = by diff --git a/scanpointgenerator/generators/spiralgenerator.py b/scanpointgenerator/generators/spiralgenerator.py index a579ec9..dde6048 100644 --- a/scanpointgenerator/generators/spiralgenerator.py +++ b/scanpointgenerator/generators/spiralgenerator.py @@ -29,9 +29,8 @@ def __init__(self, names, units, centre, radius, scale=1.0, self.radius = radius self.scale = scale self.alternate_direction = alternate_direction - self.points = None - self.points_lower = None - self.points_upper = None + self.positions = None + self.bounds = None if len(self.names) != len(set(self.names)): raise ValueError("Axis names cannot be duplicated; given %s" % @@ -56,7 +55,7 @@ def __init__(self, names, units, centre, radius, scale=1.0, self.num = int((self.radius / (self.alpha * self.beta)) ** 2) + 1 def produce_points(self): - self.points = {} + self.positions = {} self.bounds = {} b = self.beta @@ -69,8 +68,8 @@ def produce_points(self): phi = phi_t(np.arange(size)) x = self.centre[0] + b * phi * np.sin(phi) y = self.centre[1] + b * phi * np.cos(phi) - self.points[self.names[0]] = x - self.points[self.names[1]] = y + self.positions[self.names[0]] = x + self.positions[self.names[1]] = y size += 1 phi_t = lambda t: k * np.sqrt(t) diff --git a/tests/test_core/test_compoundgenerator.py b/tests/test_core/test_compoundgenerator.py index 347d16a..e50f767 100644 --- a/tests/test_core/test_compoundgenerator.py +++ b/tests/test_core/test_compoundgenerator.py @@ -90,7 +90,7 @@ def test_get_point_large_scan(self): g = CompoundGenerator([t, w, z, s], [e1, e2, e3], []) g.prepare() - spiral = [(x, y) for (x, y) in zip(s.points["x"], s.points["y"])] + spiral = [(x, y) for (x, y) in zip(s.positions["x"], s.positions["y"])] zwt = [(z/99., w/4., t/4.) for t in range_(0, 5) for w in range_(0, 5) for z in range_(0, 100)] @@ -253,8 +253,8 @@ def test_three_dim_middle_alternates(self): points = [] for t in range_(1, 6): for z in (range_(-1, 4) if it % 2 == 0 else range_(3, -2, -1)): - s1p = spiral.points["s1"] if iz % 2 == 0 else spiral.points["s1"][::-1] - s2p = spiral.points["s2"] if iz % 2 == 0 else spiral.points["s2"][::-1] + s1p = spiral.positions["s1"] if iz % 2 == 0 else spiral.positions["s1"][::-1] + s2p = spiral.positions["s2"] if iz % 2 == 0 else spiral.positions["s2"][::-1] points += [(x, y, s1, s2, z, t) for (s1, s2) in zip(s1p, s2p) for y in range(0, 5) for x in range(0, 5) if s1*s1 + z*z <= 1 and y*y + x*x <= 1] @@ -429,15 +429,15 @@ def test_horrible_scan(self): l1_f = True s_f = True points = [] - for (j1, j2) in zip(lissajous.points["j1"], lissajous.points["j2"]): - l2p = line2.points["l2"] if l2_f else line2.points["l2"][::-1] + for (j1, j2) in zip(lissajous.positions["j1"], lissajous.positions["j2"]): + l2p = line2.positions["l2"] if l2_f else line2.positions["l2"][::-1] l2_f = not l2_f for l2 in l2p: - l1p = line1.points["l1"] if l1_f else line1.points["l1"][::-1] + l1p = line1.positions["l1"] if l1_f else line1.positions["l1"][::-1] l1_f = not l1_f for l1 in l1p: - sp = zip(spiral.points["s1"], spiral.points["s2"]) if s_f \ - else zip(spiral.points["s1"][::-1], spiral.points["s2"][::-1]) + sp = zip(spiral.positions["s1"], spiral.positions["s2"]) if s_f \ + else zip(spiral.positions["s1"][::-1], spiral.positions["s2"][::-1]) s_f = not s_f for (s1, s2) in sp: points.append((s1, s2, l1, l2, j1, j2)) @@ -473,16 +473,16 @@ def test_double_spiral_scan(self): tl2 = [] s_f = True - for l1 in line1.points["l1"]: - sp = zip(spiral_s.points['s1'], spiral_s.points['s2']) + for l1 in line1.positions["l1"]: + sp = zip(spiral_s.positions['s1'], spiral_s.positions['s2']) sp = sp if s_f else list(sp)[::-1] s_f = not s_f l1s += [(s1, s2, l1) for (s1, s2) in sp] l2_f = True - for (t1, t2) in zip(spiral_t.points['t1'], spiral_t.points['t2']): - l2p = line2.points['l2'] if l2_f else line2.points['l2'][::-1] - l2pu = line2.bounds['l2'][1:len(line2.points['l2'])+1] - l2pl = line2.bounds['l2'][0:len(line2.points['l2'])] + for (t1, t2) in zip(spiral_t.positions['t1'], spiral_t.positions['t2']): + l2p = line2.positions['l2'] if l2_f else line2.positions['l2'][::-1] + l2pu = line2.bounds['l2'][1:len(line2.positions['l2'])+1] + l2pl = line2.bounds['l2'][0:len(line2.positions['l2'])] if not l2_f: l2pu, l2pl = l2pl[::-1], l2pu[::-1] l2_f = not l2_f @@ -617,7 +617,7 @@ def test_double_mask_alternating_spiral(self): e2 = Excluder(r2, ["y", "z"]) g = CompoundGenerator([zgen, spiral], [e1, e2], []) g.prepare() - xy = list(zip(spiral.points['x'], spiral.points['y'])) + xy = list(zip(spiral.positions['x'], spiral.positions['y'])) p = [] for z in range_(0, 5): p += [(x, y, z) for (x, y) in (xy if z % 2 == 0 else xy[::-1])] @@ -635,7 +635,7 @@ def test_double_mask_spiral(self): e2 = Excluder(r2, ["y", "z"]) g = CompoundGenerator([zgen, spiral], [e1, e2], []) g.prepare() - p = list(zip(spiral.points['x'], spiral.points['y'])) + p = list(zip(spiral.positions['x'], spiral.positions['y'])) p = [(x, y, z) for z in range_(0, 5) for (x, y) in p] expected = [x >= -2 and x <= 1 and y >= -2 and y <= 2 and z >= 0 and z <= 3 for (x, y, z) in p] @@ -649,7 +649,7 @@ def test_simple_mask_alternating_spiral(self): e = Excluder(r, ["x", "y"]) g = CompoundGenerator([z, spiral], [e], []) g.prepare() - p = list(zip(spiral.points['x'], spiral.points['y'])) + p = list(zip(spiral.positions['x'], spiral.positions['y'])) expected = [x >= -2 and x < 1 and y >= -2 and y < 2 for (x, y) in p] expected_r = [x >= -2 and x < 1 and y >= -2 and y < 2 for (x, y) in p[::-1]] actual = g.dim_meta[g.dimensions[1]]["mask"].tolist() diff --git a/tests/test_generators/test_linegenerator.py b/tests/test_generators/test_linegenerator.py index a8ef7c0..b702294 100644 --- a/tests/test_generators/test_linegenerator.py +++ b/tests/test_generators/test_linegenerator.py @@ -22,7 +22,7 @@ def test_array_positions(self): bounds = [0.0, 2.0, 4.0, 6.0, 8.0, 10.0] indexes = [0, 1, 2, 3, 4] g.produce_points() - self.assertEqual(positions, g.points['x'].tolist()) + self.assertEqual(positions, g.positions['x'].tolist()) self.assertEqual(bounds, g.bounds['x'].tolist()) def test_negative_direction(self): @@ -30,13 +30,13 @@ def test_negative_direction(self): positions = [2., 1., 0., -1., -2.] bounds = [2.5, 1.5, 0.5, -0.5, -1.5, -2.5] g.produce_points() - self.assertEqual(positions, g.points['x'].tolist()) + self.assertEqual(positions, g.positions['x'].tolist()) self.assertEqual(bounds, g.bounds['x'].tolist()) def test_single_point(self): g = LineGenerator("x", "mm", 1.0, 4.0, 1) g.produce_points() - self.assertEqual([1.0], g.points["x"].tolist()) + self.assertEqual([1.0], g.positions["x"].tolist()) self.assertEqual([-0.5, 2.5], g.bounds["x"].tolist()) def test_duplicate_name_raises(self): @@ -102,8 +102,8 @@ def test_array_positions(self): y_positions = [2.0, 4.0, 6.0, 8.0, 10.0] x_bounds = [0.5, 1.5, 2.5, 3.5, 4.5, 5.5] y_bounds = [1, 3, 5, 7, 9, 11] - self.assertEqual(x_positions, g.points['x'].tolist()) - self.assertEqual(y_positions, g.points['y'].tolist()) + self.assertEqual(x_positions, g.positions['x'].tolist()) + self.assertEqual(y_positions, g.positions['y'].tolist()) self.assertEqual(x_bounds, g.bounds['x'].tolist()) self.assertEqual(y_bounds, g.bounds['y'].tolist()) diff --git a/tests/test_generators/test_lissajousgenerator.py b/tests/test_generators/test_lissajousgenerator.py index 5077d8a..5364fee 100644 --- a/tests/test_generators/test_lissajousgenerator.py +++ b/tests/test_generators/test_lissajousgenerator.py @@ -48,7 +48,7 @@ def test_array_positions(self): {'y': -0.29389262614623674, 'x': 0.47552825814757677}] g.produce_points() - p = [{'x':x, 'y':y} for (x, y) in zip(g.points['x'], g.points['y'])] + p = [{'x':x, 'y':y} for (x, y) in zip(g.positions['x'], g.positions['y'])] b = [{'x':x, 'y':y} for (x, y) in zip(g.bounds['x'], g.bounds['y'])] self.assertEqual(positions, p) self.assertEqual(bounds, b) @@ -69,8 +69,8 @@ def test_array_positions_with_offset(self): expected_by = [-0.47552825814757677, 0.47552825814757677, -0.2938926261462365, -1.2246467991473532e-16, 0.2938926261462367, -0.4755282581475769] - self.assertListAlmostEqual(expected_x, g.points['x'].tolist()) - self.assertListAlmostEqual(expected_y, g.points['y'].tolist()) + self.assertListAlmostEqual(expected_x, g.positions['x'].tolist()) + self.assertListAlmostEqual(expected_y, g.positions['y'].tolist()) self.assertListAlmostEqual(expected_bx, g.bounds['x'].tolist()) self.assertListAlmostEqual(expected_by, g.bounds['y'].tolist()) diff --git a/tests/test_generators/test_spiralgenerator.py b/tests/test_generators/test_spiralgenerator.py index 0f6112d..3c2d5ed 100644 --- a/tests/test_generators/test_spiralgenerator.py +++ b/tests/test_generators/test_spiralgenerator.py @@ -37,7 +37,7 @@ def test_array_positions(self): {'y': -1.0190886264001306, 'x': 0.9334439933089926}, {'y': -1.4911377166541206, 'x': 0.06839234794968006}] self.g.produce_points() - p = [{"x":x, "y":y} for (x, y) in zip(self.g.points['x'], self.g.points['y'])] + p = [{"x":x, "y":y} for (x, y) in zip(self.g.positions['x'], self.g.positions['y'])] b = [{"x":x, "y":y} for (x, y) in zip(self.g.bounds['x'], self.g.bounds['y'])] self.assertEqual(positions, p) self.assertEqual(bounds, b) From af631515813a3694cd08a34fec77b4552e3eda58 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Thu, 15 Dec 2016 16:18:03 +0000 Subject: [PATCH 34/47] Add parameterised generator.prepare_array for position/bounds generation Replaces generator.produce_points with generator.prepare_bounds and prepare_positions that call into the "virtual" method (implemented by classes deriving generator) prepare_array that accepts an index array used to produce points. --- scanpointgenerator/core/compoundgenerator.py | 8 +++-- scanpointgenerator/core/generator.py | 23 +++++++++++--- .../generators/linegenerator.py | 30 +++++++------------ .../generators/lissajousgenerator.py | 18 ++++------- .../generators/spiralgenerator.py | 22 ++++---------- tests/test_core/test_generator.py | 8 +++-- tests/test_generators/test_linegenerator.py | 12 +++++--- .../test_lissajousgenerator.py | 6 ++-- tests/test_generators/test_spiralgenerator.py | 3 +- 9 files changed, 65 insertions(+), 65 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index 94b3ec1..0d25bde 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -68,8 +68,8 @@ def prepare(self): continue if isinstance(gen_1, LineGenerator) \ and isinstance(gen_2, LineGenerator): - gen_1.produce_points() - gen_2.produce_points() + gen_1.prepare_positions() + gen_2.prepare_positions() valid = np.full(gen_1.num, True, dtype=np.int8) valid &= gen_1.positions[axis_1] \ <= rect.roi.width + rect.roi.start[0] @@ -91,8 +91,10 @@ def prepare(self): excluders.remove(rect) for generator in generators: - generator.produce_points() + generator.prepare_positions() self.dimensions.append(Dimension(generator)) + # only the inner-most generator needs to have bounds calculated + generators[-1].prepare_bounds() for excluder in excluders: axis_1, axis_2 = excluder.scannables diff --git a/scanpointgenerator/core/generator.py b/scanpointgenerator/core/generator.py index 213aa23..a28e52e 100644 --- a/scanpointgenerator/core/generator.py +++ b/scanpointgenerator/core/generator.py @@ -1,3 +1,4 @@ +from scanpointgenerator.compat import np class Generator(object): @@ -18,19 +19,33 @@ class Generator(object): position_units = None index_dims = None index_names = None + positions = None + bounds = None + num = 0 # Lookup table for generator subclasses _generator_lookup = {} axes = [] - def produce_points(self): + def prepare_arrays(self, index_array): """ - Abstract method to create and cache position and bound arrays + Abstract method to create position or bounds array from provided index + array. index_array will be np.arange(self.num) for positions and + np.arange(self.num + 1) - 0.5 for bounds. - Point arrays should be stored in self.points[axis] and bounds in - self.bounds[axis] + Args: + index_array (np.array): Index array to produce parameterised points + + Returns: + Positions: Dictionary of axis names to position/bounds arrays """ raise NotImplementedError + def prepare_positions(self): + self.positions = self.prepare_arrays(np.arange(self.num)) + + def prepare_bounds(self): + self.bounds = self.prepare_arrays(np.arange(self.num + 1) - 0.5) + def to_dict(self): """Abstract method to convert object attributes into a dictionary""" raise NotImplementedError diff --git a/scanpointgenerator/generators/linegenerator.py b/scanpointgenerator/generators/linegenerator.py index e20f850..a7c5f35 100644 --- a/scanpointgenerator/generators/linegenerator.py +++ b/scanpointgenerator/generators/linegenerator.py @@ -70,27 +70,17 @@ def __init__(self, name, units, start, stop, num, alternate_direction=False): self.axes = self.name # For GDA - def produce_points(self): - self.positions = {} - self.bounds = {} - for axis in range_(self.num_axes): - axis_name = self.name[axis] - start = self.start[axis] - stop = self.stop[axis] + def prepare_arrays(self, index_array): + arrays = {} + for axis, start, stop in zip(self.name, self.start, self.stop): d = stop - start - if self.num == 1: - self.positions[axis_name] = np.array([start]) - self.bounds[axis_name] = np.array( - [start - 0.5 * d, start + 0.5 * d]) - else: - n = self.num - 1. - s = float(d) / n - bound_stop = stop + 0.5 * s - bound_start = start - 0.5 * s - self.positions[axis_name] = np.linspace( - float(start), float(stop), self.num) - self.bounds[axis_name] = np.linspace( - float(bound_start), float(bound_stop), self.num + 1) + step = float(d) + # if self.num == 1 then single point case + if self.num > 1: + step /= (self.num - 1) + f = lambda t: (t * step) + start + arrays[axis] = f(index_array) + return arrays def to_dict(self): """Convert object attributes into a dictionary""" diff --git a/scanpointgenerator/generators/lissajousgenerator.py b/scanpointgenerator/generators/lissajousgenerator.py index 675b52e..5893f3e 100644 --- a/scanpointgenerator/generators/lissajousgenerator.py +++ b/scanpointgenerator/generators/lissajousgenerator.py @@ -58,25 +58,17 @@ def __init__(self, names, units, box, num_lobes, self.axes = self.names # For GDA - def produce_points(self): - self.positions = {} - self.bounds = {} - + def prepare_arrays(self, index_array): + arrays = {} x0, y0 = self.centre[0], self.centre[1] A, B = self.x_max, self.y_max a, b = self.x_freq, self.y_freq d = self.phase_diff fx = lambda t: x0 + A * np.sin(a * 2*m.pi * t/self.num + d) fy = lambda t: y0 + B * np.sin(b * 2*m.pi * t/self.num) - x = fx(np.arange(self.num)) - bx = fx(np.arange(self.num + 1) - 0.5) - y = fy(np.arange(self.num)) - by = fy(np.arange(self.num + 1) - 0.5) - - self.positions[self.names[0]] = x - self.positions[self.names[1]] = y - self.bounds[self.names[0]] = bx - self.bounds[self.names[1]] = by + arrays[self.names[0]] = fx(index_array) + arrays[self.names[1]] = fy(index_array) + return arrays def to_dict(self): """Convert object attributes into a dictionary""" diff --git a/scanpointgenerator/generators/spiralgenerator.py b/scanpointgenerator/generators/spiralgenerator.py index dde6048..bf87ae1 100644 --- a/scanpointgenerator/generators/spiralgenerator.py +++ b/scanpointgenerator/generators/spiralgenerator.py @@ -54,30 +54,20 @@ def __init__(self, names, units, centre, radius, scale=1.0, self.beta = scale / (2 * m.pi) # Radius scale factor = b self.num = int((self.radius / (self.alpha * self.beta)) ** 2) + 1 - def produce_points(self): - self.positions = {} - self.bounds = {} - + def prepare_arrays(self, index_array): + arrays = {} b = self.beta k = self.alpha size = self.num - # parameterise phi with approximation: # phi(t) = k * sqrt(t) (for some k) phi_t = lambda t: k * np.sqrt(t + 0.5) - phi = phi_t(np.arange(size)) + phi = phi_t(index_array) x = self.centre[0] + b * phi * np.sin(phi) y = self.centre[1] + b * phi * np.cos(phi) - self.positions[self.names[0]] = x - self.positions[self.names[1]] = y - - size += 1 - phi_t = lambda t: k * np.sqrt(t) - phi = phi_t(np.arange(size)) - bx = self.centre[0] + b * phi * np.sin(phi) - by = self.centre[1] + b * phi * np.cos(phi) - self.bounds[self.names[0]] = bx - self.bounds[self.names[1]] = by + arrays[self.names[0]] = x + arrays[self.names[1]] = y + return arrays def to_dict(self): """Convert object attributes into a dictionary""" diff --git a/tests/test_core/test_generator.py b/tests/test_core/test_generator.py index 5361991..8d54832 100644 --- a/tests/test_core/test_generator.py +++ b/tests/test_core/test_generator.py @@ -21,9 +21,13 @@ def test_init(self): self.assertEqual(self.g.index_dims, None) self.assertEqual(self.g.index_names, None) - def test_produce_points_raises(self): + def test_prepare_positions_raises(self): with self.assertRaises(NotImplementedError): - self.g.produce_points() + self.g.prepare_positions() + + def test_prepare_bounds_raises(self): + with self.assertRaises(NotImplementedError): + self.g.prepare_bounds() def test_to_dict_raises(self): with self.assertRaises(NotImplementedError): diff --git a/tests/test_generators/test_linegenerator.py b/tests/test_generators/test_linegenerator.py index b702294..8d411a4 100644 --- a/tests/test_generators/test_linegenerator.py +++ b/tests/test_generators/test_linegenerator.py @@ -21,7 +21,8 @@ def test_array_positions(self): positions = [1.0, 3.0, 5.0, 7.0, 9.0] bounds = [0.0, 2.0, 4.0, 6.0, 8.0, 10.0] indexes = [0, 1, 2, 3, 4] - g.produce_points() + g.prepare_positions() + g.prepare_bounds() self.assertEqual(positions, g.positions['x'].tolist()) self.assertEqual(bounds, g.bounds['x'].tolist()) @@ -29,13 +30,15 @@ def test_negative_direction(self): g = LineGenerator("x", "mm", 2, -2, 5) positions = [2., 1., 0., -1., -2.] bounds = [2.5, 1.5, 0.5, -0.5, -1.5, -2.5] - g.produce_points() + g.prepare_positions() + g.prepare_bounds() self.assertEqual(positions, g.positions['x'].tolist()) self.assertEqual(bounds, g.bounds['x'].tolist()) def test_single_point(self): g = LineGenerator("x", "mm", 1.0, 4.0, 1) - g.produce_points() + g.prepare_positions() + g.prepare_bounds() self.assertEqual([1.0], g.positions["x"].tolist()) self.assertEqual([-0.5, 2.5], g.bounds["x"].tolist()) @@ -97,7 +100,8 @@ def test_give_one_point_then_step_zero(self): def test_array_positions(self): g = LineGenerator(["x", "y"], "mm", [1.0, 2.0], [5.0, 10.0], 5) - g.produce_points() + g.prepare_positions() + g.prepare_bounds() x_positions = [1.0, 2.0, 3.0, 4.0, 5.0] y_positions = [2.0, 4.0, 6.0, 8.0, 10.0] x_bounds = [0.5, 1.5, 2.5, 3.5, 4.5, 5.5] diff --git a/tests/test_generators/test_lissajousgenerator.py b/tests/test_generators/test_lissajousgenerator.py index 5364fee..d531d02 100644 --- a/tests/test_generators/test_lissajousgenerator.py +++ b/tests/test_generators/test_lissajousgenerator.py @@ -47,7 +47,8 @@ def test_array_positions(self): {'y': -0.4755282581475767, 'x': 0.29389262614623646}, {'y': -0.29389262614623674, 'x': 0.47552825814757677}] - g.produce_points() + g.prepare_positions() + g.prepare_bounds() p = [{'x':x, 'y':y} for (x, y) in zip(g.positions['x'], g.positions['y'])] b = [{'x':x, 'y':y} for (x, y) in zip(g.bounds['x'], g.bounds['y'])] self.assertEqual(positions, p) @@ -58,7 +59,8 @@ def test_array_positions_with_offset(self): ['x', 'y'], 'mm', {"centre":[1., 0.], "height":1., "width":2.}, 1, num_points = 5) - g.produce_points() + g.prepare_positions() + g.prepare_bounds() expected_x = [2.0, 1.3090169943749475, 0.19098300532505266, 0.19098300532505266, 1.3090169943749475] expected_y = [0.0, 0.2938926261462366, -0.4755282581475768, diff --git a/tests/test_generators/test_spiralgenerator.py b/tests/test_generators/test_spiralgenerator.py index 3c2d5ed..22a5648 100644 --- a/tests/test_generators/test_spiralgenerator.py +++ b/tests/test_generators/test_spiralgenerator.py @@ -36,7 +36,8 @@ def test_array_positions(self): {'y': -0.09160107657707395, 'x': 1.2582363345925418}, {'y': -1.0190886264001306, 'x': 0.9334439933089926}, {'y': -1.4911377166541206, 'x': 0.06839234794968006}] - self.g.produce_points() + self.g.prepare_positions() + self.g.prepare_bounds() p = [{"x":x, "y":y} for (x, y) in zip(self.g.positions['x'], self.g.positions['y'])] b = [{"x":x, "y":y} for (x, y) in zip(self.g.bounds['x'], self.g.bounds['y'])] self.assertEqual(positions, p) From 6235893063bb85f47a893ad271b02a278d837936 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Wed, 21 Dec 2016 16:18:14 +0000 Subject: [PATCH 35/47] Reset internal state in CompoundGenerator.prepare Prevents weird inconsistencies when prepare is called multiple times (e.g. when passed repeatedly to plot_generator) --- scanpointgenerator/core/compoundgenerator.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index 0d25bde..56731ef 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -51,6 +51,11 @@ def prepare(self): Prepare data structures and masks required for point generation. Must be called before get_point or iterator are called. """ + self.dimensions = [] + self.index_dims = [] + self.dim_meta = {} + self.generator_dim_scaling = {} + # we're going to mutate these structures excluders = list(self.excluders) generators = list(self.generators) From e6aadbb33e859e9e30d980bdc88e2738d91f09a5 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Wed, 21 Dec 2016 16:42:23 +0000 Subject: [PATCH 36/47] Force integer divisions to actually return integers In Python3 3.0 // 2 returns 1.0, not 1 --- scanpointgenerator/core/compoundgenerator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index 56731ef..fff7c9c 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -202,7 +202,7 @@ def get_point(self, n): kc = 0 # the "cumulative" k for each dimension for dim in self.dimensions: indicies = self.dim_meta[dim]["indicies"] - i = n // self.dim_meta[dim]["repeat"] + i = int(n // self.dim_meta[dim]["repeat"]) i %= len(indicies) k = indicies[i] dim_reverse = False @@ -216,8 +216,8 @@ def get_point(self, n): # in alternating case, need to sometimes go backward point.indexes.append(i) for g in dim.generators: - j = k // self.generator_dim_scaling[g]["repeat"] - r = j // g.num + j = int(k // self.generator_dim_scaling[g]["repeat"]) + r = int(j // g.num) j %= g.num j_lower = j j_upper = j + 1 @@ -343,7 +343,7 @@ def create_dimension_mask(self): expanded = np.repeat(m["mask"], m["repeat"]) if m["tile"] % 1 != 0: ex = np.tile(expanded, int(m["tile"])) - expanded = np.append(ex, expanded[:len(expanded)//2]) + expanded = np.append(ex, expanded[:int(len(expanded)//2)]) else: expanded = np.tile(expanded, int(m["tile"])) mask &= expanded From 3d490e7cf2224f1fe9e7cf8f443e9d38af0bd2e6 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Wed, 21 Dec 2016 16:19:43 +0000 Subject: [PATCH 37/47] Change Mutators to act on individual points, not iterators Requires a rewrite of RandomOffsetMutator, which currently does not alter the bounds of a point. Doing so would require rethinking mutators a little bit so the bound information could be updated. The random offset generation is now required to be deterministic based on the points passed (since the points can now be generated in a random order) and must be fully consistent. Fixes CompoundGenerator to call mutators in get_point. --- scanpointgenerator/core/compoundgenerator.py | 4 +- scanpointgenerator/core/mutator.py | 12 +- .../mutators/fixeddurationmutator.py | 13 +- .../mutators/randomoffsetmutator.py | 93 ++---------- tests/test_core/test_compoundgenerator.py | 7 +- .../test_fixeddurationmutator.py | 2 +- .../test_mutators/test_randomoffsetmutator.py | 134 ++++++------------ 7 files changed, 74 insertions(+), 191 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index fff7c9c..7dddad8 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -177,8 +177,6 @@ def iterator(self): Point: The next point """ it = (self.get_point(n) for n in range_(self.num)) - for m in self.mutators: - it = m.mutate(it) for p in it: yield p @@ -239,6 +237,8 @@ def get_point(self, n): else: point.lower[axis] = g.positions[axis][j] point.upper[axis] = g.positions[axis][j] + for m in self.mutators: + point = m.mutate(point) return point def to_dict(self): diff --git a/scanpointgenerator/core/mutator.py b/scanpointgenerator/core/mutator.py index ccd9879..a27ad15 100644 --- a/scanpointgenerator/core/mutator.py +++ b/scanpointgenerator/core/mutator.py @@ -7,16 +7,16 @@ class Mutator(object): # Lookup table for mutator subclasses _mutator_lookup = {} - def mutate(self, iterator): + def mutate(self, point): """ - Abstract method to take each point from the given iterator, apply a - mutation and then yield the new point + Abstract method to take a point, apply a mutation and then return the + new point Args: - iterator(iter): Iterator to mutate + Point: point to mutate - Yields: - Point: Mutated points from generator + Returns: + Point: Mutated point """ raise NotImplementedError diff --git a/scanpointgenerator/mutators/fixeddurationmutator.py b/scanpointgenerator/mutators/fixeddurationmutator.py index dc062cf..cd68250 100644 --- a/scanpointgenerator/mutators/fixeddurationmutator.py +++ b/scanpointgenerator/mutators/fixeddurationmutator.py @@ -11,20 +11,19 @@ def __init__(self, duration): """ self.duration = duration - def mutate(self, iterator): + def mutate(self, point): """ Applies duration to points in the given iterator, yielding them Args: - iterator: Iterator to mutate + Point: Point to mutate - Yields: - Point: Mutated points + Returns: + Point: Mutated point """ - for p in iterator: - p.duration = self.duration - yield p + point.duration = self.duration + return point def to_dict(self): return {"typeid": self.typeid, "duration": self.duration} diff --git a/scanpointgenerator/mutators/randomoffsetmutator.py b/scanpointgenerator/mutators/randomoffsetmutator.py index d2bfb07..f5dea9c 100644 --- a/scanpointgenerator/mutators/randomoffsetmutator.py +++ b/scanpointgenerator/mutators/randomoffsetmutator.py @@ -18,92 +18,19 @@ def __init__(self, seed, axes, max_offset): """ self.seed = seed - self.RNG = random.Random(seed) self.axes = axes self.max_offset = max_offset - def get_random_number(self): - """ - Return a random number between -1.0 and 1.0 with Gaussian distribution - - Returns: - Float: Random number - """ - random_number = 2.0 - while abs(random_number) > 1.0: - random_number = self.RNG.random() - - return random_number - - def apply_offset(self, point): - """ - Apply a random offset to the Point - - Args: - point(Point): Point to apply random offset to - - Returns: - bool: Whether point was changed - """ - - changed = False - for axis in self.axes: - offset = self.max_offset[axis] - if offset == 0.0: - pass - else: - random_offset = self.get_random_number() * offset - point.positions[axis] += random_offset - changed = True - - return changed - - @staticmethod - def calculate_new_bounds(current_point, next_point): - """ - Take two adjacent points and recalculate their shared bound - - Args: - next_point(Point): Next point - current_point(Point): Current point - """ - - for axis in current_point.positions.keys(): - new_bound = (current_point.positions[axis] + - next_point.positions[axis]) / 2 - - current_point.upper[axis] = new_bound - next_point.lower[axis] = new_bound - - def mutate(self, iterator): - """ - An iterator that takes another iterator, applies a random offset to - each point and then yields it - - Args: - iterator: Iterator to mutate - - Yields: - Point: Mutated points - """ - - next_point = current_point = None - - for next_point in iterator: - changed = self.apply_offset(next_point) - - if current_point is not None: - if changed: - # If point wasn't changed don't update bounds - if next_point.lower == current_point.upper: - # If leaving and re-entering ROI don't update bounds - self.calculate_new_bounds(current_point, next_point) - - yield current_point - - current_point = next_point - - yield next_point + def mutate(self, point): + seed = self.seed + for idx in point.indexes: + seed = (seed << 4) ^ idx + for axis in sorted(self.axes): + m = self.max_offset[axis] + r = random.Random(seed).random() + point.positions[axis] += m * r + seed += 1 + return point def to_dict(self): """Convert object attributes into a dictionary""" diff --git a/tests/test_core/test_compoundgenerator.py b/tests/test_core/test_compoundgenerator.py index e50f767..298ea45 100644 --- a/tests/test_core/test_compoundgenerator.py +++ b/tests/test_core/test_compoundgenerator.py @@ -516,10 +516,13 @@ def test_double_spiral_scan(self): self.assertEqual(expected_l2_lower, [p.lower["l2"] for p in gpoints]) self.assertEqual(expected_l2_upper, [p.upper["l2"] for p in gpoints]) - def test_mutators(self): + @patch("scanpointgenerator.core.random.Random") + def test_mutators(self, random_patch): + random_mock = MagicMock() + random_mock.random.return_value = 0.1 + random_patch.return_value = random_mock mutator_1 = FixedDurationMutator(0.2) mutator_2 = RandomOffsetMutator(0, ["x"], {"x":1}) - mutator_2.get_random_number = MagicMock(return_value=0.1) x = LineGenerator('x', 'mm', 1, 5, 5) g = CompoundGenerator([x], [], [mutator_1, mutator_2]) g.prepare() diff --git a/tests/test_mutators/test_fixeddurationmutator.py b/tests/test_mutators/test_fixeddurationmutator.py index de8632d..8bc3f6d 100644 --- a/tests/test_mutators/test_fixeddurationmutator.py +++ b/tests/test_mutators/test_fixeddurationmutator.py @@ -38,7 +38,7 @@ def test_mutate(self): duration = Mock() m = FixedDurationMutator(duration) points = [Mock(), Mock(), Mock()] - mutated_points = list(m.mutate(iter(points))) + mutated_points = [m.mutate(p) for p in points] self.assertEqual(points, mutated_points) for p in mutated_points: self.assertIs(duration, p.duration) diff --git a/tests/test_mutators/test_randomoffsetmutator.py b/tests/test_mutators/test_randomoffsetmutator.py index 2e4e986..a164db7 100644 --- a/tests/test_mutators/test_randomoffsetmutator.py +++ b/tests/test_mutators/test_randomoffsetmutator.py @@ -16,100 +16,54 @@ class RandomOffsetMutatorTest(ScanPointGeneratorTest): - def setUp(self): - self.m = RandomOffsetMutator(1, ["x"], dict(x=0.25)) def test_init(self): - self.assertEqual(1, self.m.seed) - self.assertEqual(dict(x=0.25), self.m.max_offset) - - def test_get_random_number(self): - number = self.m.get_random_number() - self.assertEqual(0.48590197099999966, number) - number = self.m.get_random_number() - self.assertEqual(0.3167828240000006, number) - number = self.m.get_random_number() - self.assertEqual(-0.7892260970000002, number) - - @patch('scanpointgenerator.mutators.RandomOffsetMutator.get_random_number', - return_value=1.0) - def test_apply_offset(self, _): - point = MagicMock() - point.positions = dict(x=1.0) - - response = self.m.apply_offset(point) - - self.assertTrue(response) - self.assertEqual(dict(x=1.25), point.positions) - - @patch('scanpointgenerator.mutators.RandomOffsetMutator.get_random_number', - return_value=1.0) - def test_apply_offset_unchanged(self, _): - point = MagicMock() - point.positions = dict(x=1.0) - self.m.max_offset = dict(x=0.0) - - response = self.m.apply_offset(point) - - self.assertFalse(response) - self.assertEqual(dict(x=1.0), point.positions) - - def test_calculate_new_bounds(self): - current_point = Point() - current_point.positions = dict(x=1.0) - current_point.upper = dict(x=1.1) - next_point = Point() - next_point.positions = dict(x=2.0) - - self.m.calculate_new_bounds(current_point, next_point) - - self.assertEqual(dict(x=1.5), current_point.upper) - self.assertEqual(dict(x=1.5), next_point.lower) - - def test_mutate(self): - positions = [1.12147549275, 2.079195706, - 2.80269347575, 3.908258751, 5.23701473025] - lower = [0.5, 1.600335599375, - 2.440944590875, 3.355476113375, 4.572636740625] - upper = [1.600335599375, 2.440944590875, - 3.355476113375, 4.572636740625, 5.5] - indexes = [0, 1, 2, 3, 4] - - def test_iterator(): - for x in range_(1, 6): + m = RandomOffsetMutator(1, ["x"], dict(x=0.25)) + self.assertEqual(1, m.seed) + self.assertEqual(dict(x=0.25), m.max_offset) + + def test_mutate_simple(self): + def point_gen(): + for n in range_(10): p = Point() - p.positions['x'] = x - p.lower['x'] = x - 0.5 - p.upper['x'] = x + 0.5 - p.indexes = [x - 1] + p.indexes = [n] + p.positions = {"x":n/10.} + p.lower = {"x":(n-0.5)/10.} + p.upper = {"x":(n+0.5)/10.} yield p - - for i, p in enumerate(self.m.mutate(test_iterator())): - self.assertAlmostEqual(p.positions['x'], positions[i], places=10) - self.assertAlmostEqual(p.lower['x'], lower[i], places=10) - self.assertAlmostEqual(p.upper['x'], upper[i], places=10) - self.assertEqual(p.indexes, [indexes[i]]) - self.assertEqual(i, 4) - - def test_order_of_offsets(self): - line1 = LineGenerator("y", "mm", 2.0, 10.0, 5) - line2 = LineGenerator("x", "mm", 1.0, 5.0, 5) - - mutator = RandomOffsetMutator(1, ["y", "x"], dict(x=0.25, y=0.25)) - gen = CompoundGenerator([line1, line2], [], [mutator]) - gen.prepare() - p = next(gen.iterator()) - self.assertAlmostEqual(p.positions['x'], 1.0791957060000001, places=10) - self.assertAlmostEqual(p.positions['y'], 2.12147549275, places=10) - - # Swap order of axes in mutator; should swap offsets applied to x and y - mutator = RandomOffsetMutator(1, ["x", "y"], dict(x=0.25, y=0.25)) - gen = CompoundGenerator([line1, line2], [], [mutator]) - gen.prepare() - p = next(gen.iterator()) - self.assertAlmostEqual(p.positions['x'], 1.12147549275, places=10) - self.assertAlmostEqual(p.positions['y'], 2.0791957060000001, places=10) - + m = RandomOffsetMutator(1, ["x"], {"x":0.01}) + original = [p for p in point_gen()] + mutated = [m.mutate(p) for p in point_gen()] + for o, m in zip(original, mutated): + op, mp = o.positions["x"], m.positions["x"] + ou, mu = o.upper["x"], m.upper["x"] + ol, ml = o.lower["x"], m.lower["x"] + self.assertNotEqual(op, mp) + self.assertTrue(abs(mp - op) < 0.01) + self.assertEqual(ou, mu) + self.assertEqual(ol, ml) + + offsets = [m.positions["x"] - o.positions["x"] for m, o in zip(mutated, original)] + for o1, o2 in zip(offsets[:-1], offsets[1:]): + self.assertNotEqual(o1, o2) + + def test_random_access_consistency(self): + def point_gen(): + for n in range_(10): + p = Point() + p.indexes = [n] + p.positions = {"x":n/10.} + p.lower = {"x":(n-0.5)/10.} + p.upper = {"x":(n+0.5)/10.} + yield p + m = RandomOffsetMutator(1, ["x"], {"x":0.01}) + original = [p.positions['x'] for p in point_gen()] + mutated1 = [m.mutate(p).positions['x'] for p in point_gen()] + mutated2 = [m.mutate(p).positions['x'] for p in point_gen()] + mutated3 = [m.mutate(p).positions['x'] for p in list(point_gen())[::-1]][::-1] + self.assertNotEqual(original, mutated1) + self.assertEqual(mutated1, mutated2) + self.assertEqual(mutated1, mutated3) class TestSerialisation(unittest.TestCase): From 554f5188fd5552acce9d3ee97b8621559a2b06e5 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Mon, 9 Jan 2017 16:09:00 +0000 Subject: [PATCH 38/47] Pass linear index to mutator and rewrite RandomOffsetMutator RandomOffsetMutator can now adjust a points boundaries consistently (once again), but this requires passing the linear index for a point to the mutate method (due to the potential for random access). The Random class is not being used at the moment. --- scanpointgenerator/core/compoundgenerator.py | 2 +- scanpointgenerator/core/mutator.py | 3 +- .../mutators/fixeddurationmutator.py | 3 +- .../mutators/randomoffsetmutator.py | 49 ++++++++++++--- tests/test_core/test_compoundgenerator.py | 7 +-- tests/test_core/test_mutator.py | 2 +- .../test_fixeddurationmutator.py | 2 +- .../test_mutators/test_randomoffsetmutator.py | 62 ++++++++++++++++--- 8 files changed, 100 insertions(+), 30 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index 7dddad8..fcdef3c 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -238,7 +238,7 @@ def get_point(self, n): point.lower[axis] = g.positions[axis][j] point.upper[axis] = g.positions[axis][j] for m in self.mutators: - point = m.mutate(point) + point = m.mutate(point, n) return point def to_dict(self): diff --git a/scanpointgenerator/core/mutator.py b/scanpointgenerator/core/mutator.py index a27ad15..171906e 100644 --- a/scanpointgenerator/core/mutator.py +++ b/scanpointgenerator/core/mutator.py @@ -7,13 +7,14 @@ class Mutator(object): # Lookup table for mutator subclasses _mutator_lookup = {} - def mutate(self, point): + def mutate(self, point, index): """ Abstract method to take a point, apply a mutation and then return the new point Args: Point: point to mutate + Index: one-dimensional linear index of point Returns: Point: Mutated point diff --git a/scanpointgenerator/mutators/fixeddurationmutator.py b/scanpointgenerator/mutators/fixeddurationmutator.py index cd68250..20111f1 100644 --- a/scanpointgenerator/mutators/fixeddurationmutator.py +++ b/scanpointgenerator/mutators/fixeddurationmutator.py @@ -11,12 +11,13 @@ def __init__(self, duration): """ self.duration = duration - def mutate(self, point): + def mutate(self, point, index): """ Applies duration to points in the given iterator, yielding them Args: Point: Point to mutate + Index: one-dimensional index of point Returns: Point: Mutated point diff --git a/scanpointgenerator/mutators/randomoffsetmutator.py b/scanpointgenerator/mutators/randomoffsetmutator.py index f5dea9c..4c49008 100644 --- a/scanpointgenerator/mutators/randomoffsetmutator.py +++ b/scanpointgenerator/mutators/randomoffsetmutator.py @@ -1,5 +1,4 @@ from scanpointgenerator.core import Mutator -from scanpointgenerator.core import random @Mutator.register_subclass("scanpointgenerator:mutator/RandomOffsetMutator:1.0") @@ -21,15 +20,45 @@ def __init__(self, seed, axes, max_offset): self.axes = axes self.max_offset = max_offset - def mutate(self, point): - seed = self.seed - for idx in point.indexes: - seed = (seed << 4) ^ idx - for axis in sorted(self.axes): - m = self.max_offset[axis] - r = random.Random(seed).random() - point.positions[axis] += m * r - seed += 1 + def calc_offset(self, axis, idx): + m = self.max_offset[axis] + x = (idx << 4) + (0 if len(axis) == 0 else ord(axis[0])) + x ^= (self.seed << 12) + # Apply hash algorithm to x for pseudo-randomness + # Robert Jenkins 32 bit hash (avalanches well) + x = (x + 0x7ED55D16) + (x << 12) + x &= 0xFFFFFFFF # act as 32 bit unsigned before doing any right-shifts + x = (x ^ 0xC761C23C) ^ (x >> 19) + x = (x + 0x165667B1) + (x << 5) + x = (x + 0xD3A2646C) ^ (x << 9) + x = (x + 0xFD7046C5) + (x << 3) + x &= 0xFFFFFFFF + x = (x ^ 0xB55A4F09) ^ (x >> 16) + x &= 0xFFFFFFFF + r = float(x) / float(0xFFFFFFFF) # r in interval [0, 1] + r = r * 2 - 1 # r in [-1, 1] + return m * r + + def mutate(self, point, idx): + inner_meta = None + point_offset = None + for axis in self.axes: + offset = self.calc_offset(axis, idx) + point.positions[axis] += offset + if axis in point.lower and axis in point.upper: + inner_axis = axis + point_offset = offset + if inner_axis is not None: + # recalculate lower bounds + idx -= 1 + prev_offset = self.calc_offset(inner_axis, idx) + offset = (point_offset + prev_offset) / 2 + point.lower[inner_axis] += offset + # recalculate upper bounds + idx += 2 + next_offset = self.calc_offset(inner_axis, idx) + offset = (point_offset + next_offset) / 2 + point.upper[inner_axis] += offset return point def to_dict(self): diff --git a/tests/test_core/test_compoundgenerator.py b/tests/test_core/test_compoundgenerator.py index 298ea45..8affa5b 100644 --- a/tests/test_core/test_compoundgenerator.py +++ b/tests/test_core/test_compoundgenerator.py @@ -516,13 +516,10 @@ def test_double_spiral_scan(self): self.assertEqual(expected_l2_lower, [p.lower["l2"] for p in gpoints]) self.assertEqual(expected_l2_upper, [p.upper["l2"] for p in gpoints]) - @patch("scanpointgenerator.core.random.Random") - def test_mutators(self, random_patch): - random_mock = MagicMock() - random_mock.random.return_value = 0.1 - random_patch.return_value = random_mock + def test_mutators(self): mutator_1 = FixedDurationMutator(0.2) mutator_2 = RandomOffsetMutator(0, ["x"], {"x":1}) + mutator_2.calc_offset = MagicMock(return_value=0.1) x = LineGenerator('x', 'mm', 1, 5, 5) g = CompoundGenerator([x], [], [mutator_1, mutator_2]) g.prepare() diff --git a/tests/test_core/test_mutator.py b/tests/test_core/test_mutator.py index d573ebb..3973dbc 100644 --- a/tests/test_core/test_mutator.py +++ b/tests/test_core/test_mutator.py @@ -16,7 +16,7 @@ class MutatorTest(unittest.TestCase): def test_mutate_raises(self): m = Mutator() with self.assertRaises(NotImplementedError): - m.mutate(MagicMock()) + m.mutate(MagicMock(), MagicMock()) def test_register_subclass(self): diff --git a/tests/test_mutators/test_fixeddurationmutator.py b/tests/test_mutators/test_fixeddurationmutator.py index 8bc3f6d..38cb8bb 100644 --- a/tests/test_mutators/test_fixeddurationmutator.py +++ b/tests/test_mutators/test_fixeddurationmutator.py @@ -38,7 +38,7 @@ def test_mutate(self): duration = Mock() m = FixedDurationMutator(duration) points = [Mock(), Mock(), Mock()] - mutated_points = [m.mutate(p) for p in points] + mutated_points = [m.mutate(p, i) for i, p in enumerate(points)] self.assertEqual(points, mutated_points) for p in mutated_points: self.assertIs(duration, p.duration) diff --git a/tests/test_mutators/test_randomoffsetmutator.py b/tests/test_mutators/test_randomoffsetmutator.py index a164db7..a0b5b0a 100644 --- a/tests/test_mutators/test_randomoffsetmutator.py +++ b/tests/test_mutators/test_randomoffsetmutator.py @@ -5,7 +5,7 @@ from test_util import ScanPointGeneratorTest from scanpointgenerator.compat import range_ -from scanpointgenerator.generators import LineGenerator +from scanpointgenerator.generators import LineGenerator, LissajousGenerator from scanpointgenerator.mutators import RandomOffsetMutator from scanpointgenerator import Point, CompoundGenerator @@ -16,7 +16,6 @@ class RandomOffsetMutatorTest(ScanPointGeneratorTest): - def test_init(self): m = RandomOffsetMutator(1, ["x"], dict(x=0.25)) self.assertEqual(1, m.seed) @@ -33,15 +32,15 @@ def point_gen(): yield p m = RandomOffsetMutator(1, ["x"], {"x":0.01}) original = [p for p in point_gen()] - mutated = [m.mutate(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, mp = o.positions["x"], m.positions["x"] ou, mu = o.upper["x"], m.upper["x"] ol, ml = o.lower["x"], m.lower["x"] self.assertNotEqual(op, mp) self.assertTrue(abs(mp - op) < 0.01) - self.assertEqual(ou, mu) - self.assertEqual(ol, ml) + self.assertTrue(abs(mu - ou) < 0.01) + self.assertTrue(abs(ml - ol) < 0.01) offsets = [m.positions["x"] - o.positions["x"] for m, o in zip(mutated, original)] for o1, o2 in zip(offsets[:-1], offsets[1:]): @@ -56,15 +55,58 @@ def point_gen(): p.lower = {"x":(n-0.5)/10.} p.upper = {"x":(n+0.5)/10.} yield p - m = RandomOffsetMutator(1, ["x"], {"x":0.01}) + m = RandomOffsetMutator(5025, ["x"], {"x":0.01}) original = [p.positions['x'] for p in point_gen()] - mutated1 = [m.mutate(p).positions['x'] for p in point_gen()] - mutated2 = [m.mutate(p).positions['x'] for p in point_gen()] - mutated3 = [m.mutate(p).positions['x'] for p in list(point_gen())[::-1]][::-1] + mutated1 = [m.mutate(p, i).positions['x'] for i, p in enumerate(point_gen())] + mutated2 = [m.mutate(p, i).positions['x'] for i, p in enumerate(point_gen())] + mutated3 = [m.mutate(p, i).positions['x'] for i, p in list(enumerate(point_gen()))[::-1]][::-1] self.assertNotEqual(original, mutated1) self.assertEqual(mutated1, mutated2) self.assertEqual(mutated1, mutated3) + def test_bounds_consistency(self): + def point_gen(): + for n in range_(10): + p = Point() + p.indexes = [n] + p.positions = {"x":n/10.} + p.lower = {"x":(n-0.5)/10.} + p.upper = {"x":(n+0.5)/10.} + yield p + m = RandomOffsetMutator(1, ["x"], {"x":0.01}) + original = [p.positions["x"] for p in point_gen()] + mutated = [m.mutate(p, i) for i, p in enumerate(point_gen())] + for m1, m2 in zip(mutated[:-1], mutated[1:]): + self.assertEqual(m1.upper["x"], m2.lower["x"]) + + def test_double_line_consistency(self): + xg = LineGenerator("x", "mm", 0, 4, 5, True) + yg = LineGenerator("y", "mm", 0, 4, 3) + m = RandomOffsetMutator(1, ["x", "y"], {"x":0.1, "y":0.25}) + g = CompoundGenerator([yg, xg], [], []) + g.prepare() + points = list(g.iterator()) + ly = [l.upper["y"] for l in points[0:4] + points[5:9] + points[10:14]] + ry = [r.lower["y"] for r in points[1:5] + points[6:10] + points[11:15]] + self.assertEqual(ly, ry) + + def test_bounds_consistency_in_compound(self): + liss = LissajousGenerator(["x", "y"], ["mm", "mm"], + {"centre":[0, 0], "width":2, "height":2}, 4, 100, True) + line = LineGenerator("z", "mm", 0, 1, 3) + m = RandomOffsetMutator(1, ["x", "y"], {"x":0.1, "y":0.1}) + g = CompoundGenerator([line, liss], [], []) + gm = CompoundGenerator([line, liss], [], [m]) + g.prepare() + gm.prepare() + points = list(gm.iterator()) + lx = [l.upper["x"] for l in points[:-1]] + rx = [r.lower["x"] for r in points[1:]] + self.assertListAlmostEqual(lx, rx) + ly = [l.upper["y"] for l in points[:-1]] + ry = [r.lower["y"] for r in points[1:]] + self.assertListAlmostEqual(ly, ry) + class TestSerialisation(unittest.TestCase): def setUp(self): @@ -104,4 +146,4 @@ def test_from_dict(self): if __name__ == "__main__": - unittest.main() + unittest.main(verbosity=2) From 2501c059eb5c223e3391e2bac8d5a790e9d8d5b1 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Mon, 9 Jan 2017 16:11:17 +0000 Subject: [PATCH 39/47] Fix documentation following changes to generator point production. --- docs/mutators.rst | 16 +++++++++++++++- docs/writing.rst | 20 +++++++------------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/docs/mutators.rst b/docs/mutators.rst index 665f4f0..81d17a9 100644 --- a/docs/mutators.rst +++ b/docs/mutators.rst @@ -38,6 +38,20 @@ And with the random offset xs = LineGenerator("x", "mm", 0.0, 0.5, 5, alternate_direction=True) ys = LineGenerator("y", "mm", 0.0, 0.5, 4) - random_offset = RandomOffsetMutator(seed=1, axes = ["x", "y"], max_offset=dict(x=0.05, y=0.05)) + random_offset = RandomOffsetMutator(seed=12345, axes = ["x", "y"], max_offset=dict(x=0.05, y=0.05)) gen = CompoundGenerator([ys, xs], [], [random_offset]) plot_generator(gen) + + +Example with a spiral + +.. plot:: + :include-source: + + from scanpointgenerator import SpiralGenerator, CompoundGenerator, RandomOffsetMutator + from scanpointgenerator.plotgenerator import plot_generator + + spiral = SpiralGenerator(["x", "y"], ["mm", "mm"], [0., 0.], 5.0, 1.25) + random_offset = RandomOffsetMutator(seed=12345, axes = ["x", "y"], max_offset=dict(x=0.2, y=0.2)) + gen = CompoundGenerator([spiral], [], [random_offset]) + plot_generator(gen) diff --git a/docs/writing.rst b/docs/writing.rst index e5e3427..d314de0 100644 --- a/docs/writing.rst +++ b/docs/writing.rst @@ -35,19 +35,13 @@ less elements, because the last two or more dimensions will be unrolled into one long array. This avoids sparse datasets. .. literalinclude:: ../scanpointgenerator/generators/linegenerator.py - :pyobject: LineGenerator.produce_points + :pyobject: LineGenerator.prepare_arrays This is used by CompoundGenerator to create the points for this generator. -In here, we should create an array of points for each axis and store them in -a dictionary attribute, using the axis names for the key. The same should be -done for the boundaries between points. +This method should create, for each axis the generator defines, an array of +positions by transforming the input index array. +The index array will be the numpy array [0, 1, 2, ..., n-1, n] for normal +positions, and [-0.5, 0.5, 1.5, ..., n-0.5, n+0.5] when used to calculate +boundary positions. -The dictionaries are {axis_name : numpy float array}: - -- self.points: The capture positions corresponding to the centre of - the scan frame -- self.bounds: The boundary between points for continuous scanning. - -As a rule, if the position of points can be parameterised by -``[f(t) for t in range(num_points)]`` then the bounds should be given by -``[f(t - 0.5) for t in range(num_points + 1)]`` +The arrays are returned as a dictionary of {axis_name : numpy float array} From c4031a8a6e05affe4f4871f71a4de26c1d7d1e16 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Mon, 9 Jan 2017 16:11:45 +0000 Subject: [PATCH 40/47] Remove redundant attributes in generator __init__'s --- scanpointgenerator/generators/linegenerator.py | 2 -- scanpointgenerator/generators/lissajousgenerator.py | 2 -- scanpointgenerator/generators/spiralgenerator.py | 2 -- 3 files changed, 6 deletions(-) diff --git a/scanpointgenerator/generators/linegenerator.py b/scanpointgenerator/generators/linegenerator.py index a7c5f35..0a80e42 100644 --- a/scanpointgenerator/generators/linegenerator.py +++ b/scanpointgenerator/generators/linegenerator.py @@ -31,8 +31,6 @@ def __init__(self, name, units, start, stop, num, alternate_direction=False): self.start = to_list(start) self.stop = to_list(stop) self.alternate_direction = alternate_direction - self.positions = None - self.bounds = None self.units = units if len(self.name) != len(set(self.name)): diff --git a/scanpointgenerator/generators/lissajousgenerator.py b/scanpointgenerator/generators/lissajousgenerator.py index 5893f3e..dc5443a 100644 --- a/scanpointgenerator/generators/lissajousgenerator.py +++ b/scanpointgenerator/generators/lissajousgenerator.py @@ -25,8 +25,6 @@ def __init__(self, names, units, box, num_lobes, self.names = names self.units = units - self.positions = None - self.bounds = None self.alternate_direction = alternate_direction if len(self.names) != len(set(self.names)): diff --git a/scanpointgenerator/generators/spiralgenerator.py b/scanpointgenerator/generators/spiralgenerator.py index bf87ae1..7012f8a 100644 --- a/scanpointgenerator/generators/spiralgenerator.py +++ b/scanpointgenerator/generators/spiralgenerator.py @@ -29,8 +29,6 @@ def __init__(self, names, units, centre, radius, scale=1.0, self.radius = radius self.scale = scale self.alternate_direction = alternate_direction - self.positions = None - self.bounds = None if len(self.names) != len(set(self.names)): raise ValueError("Axis names cannot be duplicated; given %s" % From daf887f25a2788c5df52364557e95f0225d48e5e Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Mon, 20 Feb 2017 13:27:23 +0000 Subject: [PATCH 41/47] Move Dimension from compoundgenerator.py to new dimension.py It is now expected for this class to become public API (after some changes that are not part of this commit) --- scanpointgenerator/core/compoundgenerator.py | 111 +------------------ scanpointgenerator/core/dimension.py | 111 +++++++++++++++++++ 2 files changed, 112 insertions(+), 110 deletions(-) create mode 100644 scanpointgenerator/core/dimension.py diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index fcdef3c..3876a25 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -1,6 +1,7 @@ import logging from scanpointgenerator.compat import range_, np +from scanpointgenerator.core.dimension import Dimension from scanpointgenerator.core.generator import Generator from scanpointgenerator.core.point import Point from scanpointgenerator.core.excluder import Excluder @@ -264,113 +265,3 @@ def from_dict(cls, d): excluders = [Excluder.from_dict(e) for e in d['excluders']] mutators = [Mutator.from_dict(m) for m in d['mutators']] return cls(generators, excluders, mutators) - -class Dimension(object): - """A collapsed set of generators joined by excluders""" - def __init__(self, generator): - self.axes = list(generator.axes) - self.generators = [generator] - self.size = generator.num - self.masks = [] - self.alternate = generator.alternate_direction - - def apply_excluder(self, excluder): - """Apply an excluder with axes matching some axes in the dimension to - produce an internal mask""" - axis_inner = excluder.scannables[0] - axis_outer = excluder.scannables[1] - gen_inner = [g for g in self.generators if axis_inner in g.axes][0] - gen_outer = [g for g in self.generators if axis_outer in g.axes][0] - points_x = gen_inner.positions[axis_inner] - points_y = gen_outer.positions[axis_outer] - if self.generators.index(gen_inner) > self.generators.index(gen_outer): - gen_inner, gen_outer = gen_outer, gen_inner - axis_inner, axis_outer = axis_outer, axis_inner - points_x, points_y = points_y, points_x - - if gen_inner is gen_outer and self.alternate: - points_x = np.append(points_x, points_x[::-1]) - points_y = np.append(points_y, points_y[::-1]) - elif self.alternate: - points_x = np.append(points_x, points_x[::-1]) - points_x = np.repeat(points_x, gen_outer.num) - points_y = np.append(points_y, points_y[::-1]) - points_y = np.tile(points_y, gen_inner.num) - elif gen_inner is not gen_outer: - points_x = np.repeat(points_x, gen_outer.num) - points_y = np.tile(points_y, gen_inner.num) - else: - # copy the point arrays so the excluders can perform - # array operations in place (advantageous in the other cases) - points_x = np.copy(points_x) - points_y = np.copy(points_y) - - if axis_inner == excluder.scannables[0]: - mask = excluder.create_mask(points_x, points_y) - else: - mask = excluder.create_mask(points_y, points_x) - tile = 0.5 if self.alternate else 1 - repeat = 1 - found_axis = False - for g in self.generators: - if axis_inner in g.axes or axis_outer in g.axes: - found_axis = True - else: - if found_axis: - repeat *= g.num - else: - tile *= g.num - - m = {"repeat":repeat, "tile":tile, "mask":mask} - self.masks.append(m) - - def create_dimension_mask(self): - """ - Create and return a mask for every point in the dimension - - e.g. (with [y1, y2, y3] and [x1, x2, x3] both alternating) - y: y1, y1, y1, y2, y2, y2, y3, y3, y3 - x: x1, x2, x3, x3, x2, x1, x1, x2, x3 - mask: m1, m2, m3, m4, m5, m6, m7, m8, m9 - - Returns: - np.array(int8): One dimensional mask array - """ - mask = np.full(self.size, True, dtype=np.int8) - for m in self.masks: - assert len(m["mask"]) * m["repeat"] * m["tile"] == len(mask), \ - "Mask lengths are not consistent" - expanded = np.repeat(m["mask"], m["repeat"]) - if m["tile"] % 1 != 0: - ex = np.tile(expanded, int(m["tile"])) - expanded = np.append(ex, expanded[:int(len(expanded)//2)]) - else: - expanded = np.tile(expanded, int(m["tile"])) - mask &= expanded - return mask - - @staticmethod - def merge_dimensions(outer, inner): - """Collapse two dimensions into one, appropriate scaling structures""" - dim = Dimension(outer.generators[0]) - # masks in the inner generator are tiled by the size of - # outer generators and outer generators have their elements - # repeated by the size of inner generators - inner_masks = [m.copy() for m in inner.masks] - outer_masks = [m.copy() for m in outer.masks] - scale = 1 - for g in inner.generators: - scale *= g.num - for m in outer_masks: - m["repeat"] *= scale - scale = 1 - for g in outer.generators: - scale *= g.num - for m in inner_masks: - m["tile"] *= scale - dim.masks = outer_masks + inner_masks - dim.axes = outer.axes + inner.axes - dim.generators = outer.generators + inner.generators - dim.alternate = outer.alternate or inner.alternate - dim.size = outer.size * inner.size - return dim diff --git a/scanpointgenerator/core/dimension.py b/scanpointgenerator/core/dimension.py new file mode 100644 index 0000000..7934aea --- /dev/null +++ b/scanpointgenerator/core/dimension.py @@ -0,0 +1,111 @@ +from scanpointgenerator.compat import np + +class Dimension(object): + """A collapsed set of generators joined by excluders""" + def __init__(self, generator): + self.axes = list(generator.axes) + self.generators = [generator] + self.size = generator.num + self.masks = [] + self.alternate = generator.alternate_direction + + def apply_excluder(self, excluder): + """Apply an excluder with axes matching some axes in the dimension to + produce an internal mask""" + axis_inner = excluder.scannables[0] + axis_outer = excluder.scannables[1] + gen_inner = [g for g in self.generators if axis_inner in g.axes][0] + gen_outer = [g for g in self.generators if axis_outer in g.axes][0] + points_x = gen_inner.positions[axis_inner] + points_y = gen_outer.positions[axis_outer] + if self.generators.index(gen_inner) > self.generators.index(gen_outer): + gen_inner, gen_outer = gen_outer, gen_inner + axis_inner, axis_outer = axis_outer, axis_inner + points_x, points_y = points_y, points_x + + if gen_inner is gen_outer and self.alternate: + points_x = np.append(points_x, points_x[::-1]) + points_y = np.append(points_y, points_y[::-1]) + elif self.alternate: + points_x = np.append(points_x, points_x[::-1]) + points_x = np.repeat(points_x, gen_outer.num) + points_y = np.append(points_y, points_y[::-1]) + points_y = np.tile(points_y, gen_inner.num) + elif gen_inner is not gen_outer: + points_x = np.repeat(points_x, gen_outer.num) + points_y = np.tile(points_y, gen_inner.num) + else: + # copy the point arrays so the excluders can perform + # array operations in place (advantageous in the other cases) + points_x = np.copy(points_x) + points_y = np.copy(points_y) + + if axis_inner == excluder.scannables[0]: + mask = excluder.create_mask(points_x, points_y) + else: + mask = excluder.create_mask(points_y, points_x) + tile = 0.5 if self.alternate else 1 + repeat = 1 + found_axis = False + for g in self.generators: + if axis_inner in g.axes or axis_outer in g.axes: + found_axis = True + else: + if found_axis: + repeat *= g.num + else: + tile *= g.num + + m = {"repeat":repeat, "tile":tile, "mask":mask} + self.masks.append(m) + + def create_dimension_mask(self): + """ + Create and return a mask for every point in the dimension + + e.g. (with [y1, y2, y3] and [x1, x2, x3] both alternating) + y: y1, y1, y1, y2, y2, y2, y3, y3, y3 + x: x1, x2, x3, x3, x2, x1, x1, x2, x3 + mask: m1, m2, m3, m4, m5, m6, m7, m8, m9 + + Returns: + np.array(int8): One dimensional mask array + """ + mask = np.full(self.size, True, dtype=np.int8) + for m in self.masks: + assert len(m["mask"]) * m["repeat"] * m["tile"] == len(mask), \ + "Mask lengths are not consistent" + expanded = np.repeat(m["mask"], m["repeat"]) + if m["tile"] % 1 != 0: + ex = np.tile(expanded, int(m["tile"])) + expanded = np.append(ex, expanded[:int(len(expanded)//2)]) + else: + expanded = np.tile(expanded, int(m["tile"])) + mask &= expanded + return mask + + @staticmethod + def merge_dimensions(outer, inner): + """Collapse two dimensions into one, appropriate scaling structures""" + dim = Dimension(outer.generators[0]) + # masks in the inner generator are tiled by the size of + # outer generators and outer generators have their elements + # repeated by the size of inner generators + inner_masks = [m.copy() for m in inner.masks] + outer_masks = [m.copy() for m in outer.masks] + scale = 1 + for g in inner.generators: + scale *= g.num + for m in outer_masks: + m["repeat"] *= scale + scale = 1 + for g in outer.generators: + scale *= g.num + for m in inner_masks: + m["tile"] *= scale + dim.masks = outer_masks + inner_masks + dim.axes = outer.axes + inner.axes + dim.generators = outer.generators + inner.generators + dim.alternate = outer.alternate or inner.alternate + dim.size = outer.size * inner.size + return dim From 16b1d28ed6c9fa959c7db52cb377c1fde913f388 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Mon, 20 Feb 2017 14:05:56 +0000 Subject: [PATCH 42/47] Rename generator.num to generator.size --- scanpointgenerator/core/compoundgenerator.py | 28 +++++++++---------- scanpointgenerator/core/dimension.py | 18 ++++++------ scanpointgenerator/core/generator.py | 10 +++---- .../generators/linegenerator.py | 24 ++++++++-------- .../generators/lissajousgenerator.py | 14 +++++----- .../generators/spiralgenerator.py | 4 +-- tests/test_core/test_compoundgenerator.py | 12 ++++---- .../test_compoundgenerator_performance.py | 2 +- tests/test_generators/test_linegenerator.py | 6 ++-- .../test_lissajousgenerator.py | 2 +- 10 files changed, 60 insertions(+), 60 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index 3876a25..63c548e 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -30,7 +30,7 @@ def __init__(self, generators, excluders, mutators): self.position_units = {} self.index_dims = [] self.dimensions = [] - self.num = 1 + self.size = 1 self.dim_meta = {} self.alternate_direction = [g.alternate_direction for g in generators] for generator in generators: @@ -76,12 +76,12 @@ def prepare(self): and isinstance(gen_2, LineGenerator): gen_1.prepare_positions() gen_2.prepare_positions() - valid = np.full(gen_1.num, True, dtype=np.int8) + valid = np.full(gen_1.size, True, dtype=np.int8) valid &= gen_1.positions[axis_1] \ <= rect.roi.width + rect.roi.start[0] valid &= gen_1.positions[axis_1] >= rect.roi.start[0] points_1 = gen_1.positions[axis_1][valid.astype(np.bool)] - valid = np.full(gen_2.num, True, dtype=np.int8) + valid = np.full(gen_2.size, True, dtype=np.int8) valid &= gen_2.positions[axis_2] \ <= rect.roi.height + rect.roi.start[1] valid &= gen_2.positions[axis_2] >= rect.roi.start[1] @@ -138,19 +138,19 @@ def prepare(self): dim.apply_excluder(excluder) - self.num = 1 + self.size = 1 for dim in self.dimensions: self.dim_meta[dim] = {} mask = dim.create_dimension_mask() indicies = np.nonzero(mask)[0] if len(indicies) == 0: raise ValueError("Regions would exclude entire scan") - self.num *= len(indicies) + self.size *= len(indicies) self.dim_meta[dim]["mask"] = mask self.dim_meta[dim]["indicies"] = indicies self.index_dims.append(len(indicies)) - repeat = self.num + repeat = self.size tile = 1 for dim in self.dimensions: dim_length = len(self.dim_meta[dim]["indicies"]) @@ -163,11 +163,11 @@ def prepare(self): tile = 1 repeat = 1 for g in dim.generators: - repeat *= g.num + repeat *= g.size for g in dim.generators: - repeat /= g.num + repeat /= g.size d = {"tile":tile, "repeat":repeat} - tile *= g.num + tile *= g.size self.generator_dim_scaling[g] = d def iterator(self): @@ -177,7 +177,7 @@ def iterator(self): Yields: Point: The next point """ - it = (self.get_point(n) for n in range_(self.num)) + it = (self.get_point(n) for n in range_(self.size)) for p in it: yield p @@ -191,7 +191,7 @@ def get_point(self, n): Point: The requested point """ - if n >= self.num: + if n >= self.size: raise IndexError("Requested point is out of range") point = Point() @@ -216,14 +216,14 @@ def get_point(self, n): point.indexes.append(i) for g in dim.generators: j = int(k // self.generator_dim_scaling[g]["repeat"]) - r = int(j // g.num) - j %= g.num + r = int(j // g.size) + j %= g.size j_lower = j j_upper = j + 1 if dim.alternate and g is not dim.generators[0] and r % 2 == 1: # the top level generator's direction is handled by # the fact that the reverse direction was appended - j = g.num - j - 1 + j = g.size - j - 1 j_lower = j + 1 j_upper = j elif dim_reverse and g is dim.generators[0]: diff --git a/scanpointgenerator/core/dimension.py b/scanpointgenerator/core/dimension.py index 7934aea..82694d3 100644 --- a/scanpointgenerator/core/dimension.py +++ b/scanpointgenerator/core/dimension.py @@ -5,7 +5,7 @@ class Dimension(object): def __init__(self, generator): self.axes = list(generator.axes) self.generators = [generator] - self.size = generator.num + self.size = generator.size self.masks = [] self.alternate = generator.alternate_direction @@ -28,12 +28,12 @@ def apply_excluder(self, excluder): points_y = np.append(points_y, points_y[::-1]) elif self.alternate: points_x = np.append(points_x, points_x[::-1]) - points_x = np.repeat(points_x, gen_outer.num) + points_x = np.repeat(points_x, gen_outer.size) points_y = np.append(points_y, points_y[::-1]) - points_y = np.tile(points_y, gen_inner.num) + points_y = np.tile(points_y, gen_inner.size) elif gen_inner is not gen_outer: - points_x = np.repeat(points_x, gen_outer.num) - points_y = np.tile(points_y, gen_inner.num) + points_x = np.repeat(points_x, gen_outer.size) + points_y = np.tile(points_y, gen_inner.size) else: # copy the point arrays so the excluders can perform # array operations in place (advantageous in the other cases) @@ -52,9 +52,9 @@ def apply_excluder(self, excluder): found_axis = True else: if found_axis: - repeat *= g.num + repeat *= g.size else: - tile *= g.num + tile *= g.size m = {"repeat":repeat, "tile":tile, "mask":mask} self.masks.append(m) @@ -95,12 +95,12 @@ def merge_dimensions(outer, inner): outer_masks = [m.copy() for m in outer.masks] scale = 1 for g in inner.generators: - scale *= g.num + scale *= g.size for m in outer_masks: m["repeat"] *= scale scale = 1 for g in outer.generators: - scale *= g.num + scale *= g.size for m in inner_masks: m["tile"] *= scale dim.masks = outer_masks + inner_masks diff --git a/scanpointgenerator/core/generator.py b/scanpointgenerator/core/generator.py index a28e52e..09aa58d 100644 --- a/scanpointgenerator/core/generator.py +++ b/scanpointgenerator/core/generator.py @@ -21,7 +21,7 @@ class Generator(object): index_names = None positions = None bounds = None - num = 0 + size = 0 # Lookup table for generator subclasses _generator_lookup = {} axes = [] @@ -29,8 +29,8 @@ class Generator(object): def prepare_arrays(self, index_array): """ Abstract method to create position or bounds array from provided index - array. index_array will be np.arange(self.num) for positions and - np.arange(self.num + 1) - 0.5 for bounds. + array. index_array will be np.arange(self.size) for positions and + np.arange(self.size + 1) - 0.5 for bounds. Args: index_array (np.array): Index array to produce parameterised points @@ -41,10 +41,10 @@ def prepare_arrays(self, index_array): raise NotImplementedError def prepare_positions(self): - self.positions = self.prepare_arrays(np.arange(self.num)) + self.positions = self.prepare_arrays(np.arange(self.size)) def prepare_bounds(self): - self.bounds = self.prepare_arrays(np.arange(self.num + 1) - 0.5) + self.bounds = self.prepare_arrays(np.arange(self.size + 1) - 0.5) def to_dict(self): """Abstract method to convert object attributes into a dictionary""" diff --git a/scanpointgenerator/generators/linegenerator.py b/scanpointgenerator/generators/linegenerator.py index 0a80e42..4bd5705 100644 --- a/scanpointgenerator/generators/linegenerator.py +++ b/scanpointgenerator/generators/linegenerator.py @@ -13,7 +13,7 @@ def to_list(value): class LineGenerator(Generator): """Generate a line of equally spaced N-dimensional points""" - def __init__(self, name, units, start, stop, num, alternate_direction=False): + def __init__(self, name, units, start, stop, size, alternate_direction=False): """ Args: name (str/list(str)): The scannable name(s) E.g. "x" or ["x", "y"] @@ -22,7 +22,7 @@ def __init__(self, name, units, start, stop, num, alternate_direction=False): e.g. 1.0 or [1.0, 2.0] stop (float or list(float)): The final position to be generated. e.g. 5.0 or [5.0, 10.0] - num (int): The number of points to generate. E.g. 5 + size (int): The number of points to generate. E.g. 5 alternate_direction(bool): Specifier to reverse direction if generator is nested """ @@ -42,21 +42,21 @@ def __init__(self, name, units, start, stop, num, alternate_direction=False): raise ValueError( "Dimensions of name, start and stop do not match") - self.num = num + self.size = size self.num_axes = len(self.name) self.step = [] - if self.num < 2: + if self.size < 2: self.step = [0]*len(self.start) else: for axis in range_(len(self.start)): self.step.append( - (self.stop[axis] - self.start[axis])/(self.num - 1)) + (self.stop[axis] - self.start[axis])/(self.size - 1)) self.position_units = dict() for dimension in self.name: self.position_units[dimension] = units - self.index_dims = [self.num] + self.index_dims = [self.size] if len(self.name) > 1: gen_name = "Line" @@ -73,9 +73,9 @@ def prepare_arrays(self, index_array): for axis, start, stop in zip(self.name, self.start, self.stop): d = stop - start step = float(d) - # if self.num == 1 then single point case - if self.num > 1: - step /= (self.num - 1) + # if self.size == 1 then single point case + if self.size > 1: + step /= (self.size - 1) f = lambda t: (t * step) + start arrays[axis] = f(index_array) return arrays @@ -89,7 +89,7 @@ def to_dict(self): d['units'] = self.units d['start'] = self.start d['stop'] = self.stop - d['num'] = self.num + d['size'] = self.size d['alternate_direction'] = self.alternate_direction return d @@ -110,7 +110,7 @@ def from_dict(cls, d): units = d['units'] start = d['start'] stop = d['stop'] - num = d['num'] + size = d['size'] alternate_direction = d['alternate_direction'] - return cls(name, units, start, stop, num, alternate_direction) + return cls(name, units, start, stop, size, alternate_direction) diff --git a/scanpointgenerator/generators/lissajousgenerator.py b/scanpointgenerator/generators/lissajousgenerator.py index dc5443a..c70ee38 100644 --- a/scanpointgenerator/generators/lissajousgenerator.py +++ b/scanpointgenerator/generators/lissajousgenerator.py @@ -38,17 +38,17 @@ def __init__(self, names, units, box, num_lobes, self.x_max = box['width']/2 self.y_max = box['height']/2 self.centre = box['centre'] - self.num = num_points + self.size = num_points # Phase needs to be 0 for even lobes and pi/2 for odd lobes to start # at centre for odd and at right edge for even self.phase_diff = m.pi/2 * (num_lobes % 2) if num_points is None: - self.num = num_lobes * 250 - self.increment = 2*m.pi/self.num + self.size = num_lobes * 250 + self.increment = 2*m.pi/self.size self.position_units = {self.names[0]: units, self.names[1]: units} - self.index_dims = [self.num] + self.index_dims = [self.size] gen_name = "Lissajous" for axis_name in self.names[::-1]: gen_name = axis_name + "_" + gen_name @@ -62,8 +62,8 @@ def prepare_arrays(self, index_array): A, B = self.x_max, self.y_max a, b = self.x_freq, self.y_freq d = self.phase_diff - fx = lambda t: x0 + A * np.sin(a * 2*m.pi * t/self.num + d) - fy = lambda t: y0 + B * np.sin(b * 2*m.pi * t/self.num) + fx = lambda t: x0 + A * np.sin(a * 2*m.pi * t/self.size + d) + fy = lambda t: y0 + B * np.sin(b * 2*m.pi * t/self.size) arrays[self.names[0]] = fx(index_array) arrays[self.names[1]] = fy(index_array) return arrays @@ -82,7 +82,7 @@ def to_dict(self): d['units'] = list(self.position_units.values())[0] d['box'] = box d['num_lobes'] = self.x_freq - d['num_points'] = self.num + d['num_points'] = self.size return d diff --git a/scanpointgenerator/generators/spiralgenerator.py b/scanpointgenerator/generators/spiralgenerator.py index 7012f8a..f52a95e 100644 --- a/scanpointgenerator/generators/spiralgenerator.py +++ b/scanpointgenerator/generators/spiralgenerator.py @@ -50,13 +50,13 @@ def __init__(self, names, units, centre, radius, scale=1.0, # number of possible t is solved by sqrt(t) = max_r / b*k self.alpha = m.sqrt(4 * m.pi) # Theta scale factor = k self.beta = scale / (2 * m.pi) # Radius scale factor = b - self.num = int((self.radius / (self.alpha * self.beta)) ** 2) + 1 + self.size = int((self.radius / (self.alpha * self.beta)) ** 2) + 1 def prepare_arrays(self, index_array): arrays = {} b = self.beta k = self.alpha - size = self.num + size = self.size # parameterise phi with approximation: # phi(t) = k * sqrt(t) (for some k) phi_t = lambda t: k * np.sqrt(t + 0.5) diff --git a/tests/test_core/test_compoundgenerator.py b/tests/test_core/test_compoundgenerator.py index 8affa5b..c1c4727 100644 --- a/tests/test_core/test_compoundgenerator.py +++ b/tests/test_core/test_compoundgenerator.py @@ -59,7 +59,7 @@ def test_get_point(self): e = Excluder(r, ["x", "y"]) g = CompoundGenerator([z, y, x], [e], []) g.prepare() - points = [g.get_point(n) for n in range(0, g.num)] + points = [g.get_point(n) for n in range(0, g.size)] pos = [p.positions for p in points] idx = [p.indexes for p in points] xy_expected = [(x/2., y/2.) for y in range_(-2, 3) @@ -100,7 +100,7 @@ def test_get_point_large_scan(self): (x-1)*(x-1) + (y-1)*(y-1) <= rad1*rad1 and (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5) <= rad2*rad2 and (w-0.5)*(w-0.5) + (t-0.5)*(t-0.5) <= rad3*rad3] - points = [g.get_point(n) for n in range_(0, g.num)] + points = [g.get_point(n) for n in range_(0, g.size)] pos = [p.positions for p in points] # assertEqual on a sequence of dicts is *really* slow for (e, p) in zip(expected, pos): @@ -442,12 +442,12 @@ def test_horrible_scan(self): for (s1, s2) in sp: points.append((s1, s2, l1, l2, j1, j2)) - self.assertEqual(lissajous.num * line2.num * line1.num * spiral.num, len(points)) + self.assertEqual(lissajous.size * line2.size * line1.size * spiral.size, len(points)) points = [(s1, s2, l1, l2, j1, j2) for (s1, s2, l1, l2, j1, j2) in points if (j1-1)**2 + (l2-1)**2 <= 4 and (s2+1)**2 + (l1+1)**2 <= 16 and (s1-1)**2 + (s2-1)**2 <= 1] - self.assertEqual(len(points), g.num) + self.assertEqual(len(points), g.size) generated_points = list(g.iterator()) self.assertEqual(len(points), len(generated_points)) @@ -537,7 +537,7 @@ def test_grid_rect_region(self): e = Excluder(r, ["x", "y"]) g = CompoundGenerator([yg, xg], [e], []) g.prepare() - self.assertEqual(49, g.num) + self.assertEqual(49, g.size) p = g.get_point(8) self.assertEqual([1, 1], p.indexes) self.assertEqual((4, 4), (p.positions['y'], p.positions['x'])) @@ -556,7 +556,7 @@ def test_post_prepare(self): y = LineGenerator("y", "mm", 2.0, 2.1, 2, False) g = CompoundGenerator([y, x], [], []) g.prepare() - self.assertEqual(g.num, 6) + self.assertEqual(g.size, 6) self.assertEqual(2, len(g.dimensions)) dim_0 = g.dimensions[0] diff --git a/tests/test_core/test_compoundgenerator_performance.py b/tests/test_core/test_compoundgenerator_performance.py index 9a15105..c2b0335 100644 --- a/tests/test_core/test_compoundgenerator_performance.py +++ b/tests/test_core/test_compoundgenerator_performance.py @@ -29,7 +29,7 @@ def test_200_million_time_constraint(self): fm = FixedDurationMutator(0.1) om = RandomOffsetMutator(0, ["x", "y"], {"x":0.2, "y":0.2}) g = CompoundGenerator([w, z, s], [e1, e3, e2], [fm, om]) - g.prepare() # g.num ~3e5 + g.prepare() # g.size ~3e5 end_time = time.time() #self.assertLess(end_time - start_time, 5) diff --git a/tests/test_generators/test_linegenerator.py b/tests/test_generators/test_linegenerator.py index 8d411a4..6bd43f3 100644 --- a/tests/test_generators/test_linegenerator.py +++ b/tests/test_generators/test_linegenerator.py @@ -54,7 +54,7 @@ def test_to_dict(self): expected_dict['units'] = "mm" expected_dict['start'] = [1.0] expected_dict['stop'] = [9.0] - expected_dict['num'] = 5 + expected_dict['size'] = 5 expected_dict['alternate_direction'] = True d = g.to_dict() @@ -66,7 +66,7 @@ def test_from_dict(self): _dict['units'] = "mm" _dict['start'] = [1.0] _dict['stop'] = [9.0] - _dict['num'] = 5 + _dict['size'] = 5 _dict['alternate_direction'] = True units_dict = dict() @@ -78,7 +78,7 @@ def test_from_dict(self): self.assertEqual(units_dict, gen.position_units) self.assertEqual([1.0], gen.start) self.assertEqual([9.0], gen.stop) - self.assertEqual(5, gen.num) + self.assertEqual(5, gen.size) self.assertTrue(gen.alternate_direction) class LineGenerator2DTest(ScanPointGeneratorTest): diff --git a/tests/test_generators/test_lissajousgenerator.py b/tests/test_generators/test_lissajousgenerator.py index d531d02..fdc299e 100644 --- a/tests/test_generators/test_lissajousgenerator.py +++ b/tests/test_generators/test_lissajousgenerator.py @@ -120,7 +120,7 @@ def test_from_dict(self): self.assertEqual(0.5, gen.x_max) self.assertEqual(1.0, gen.y_max) self.assertEqual([0.0, 0.0], gen.centre) - self.assertEqual(250, gen.num) + self.assertEqual(250, gen.size) if __name__ == "__main__": unittest.main() From d1d846501ca6f70c82f72db37aa948e4d8ebeb42 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Tue, 21 Feb 2017 15:10:50 +0000 Subject: [PATCH 43/47] Fix spelling of indices in compoundgenerator.py --- scanpointgenerator/core/compoundgenerator.py | 26 ++++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index 63c548e..4f1e928 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -142,18 +142,18 @@ def prepare(self): for dim in self.dimensions: self.dim_meta[dim] = {} mask = dim.create_dimension_mask() - indicies = np.nonzero(mask)[0] - if len(indicies) == 0: + indices = np.nonzero(mask)[0] + if len(indices) == 0: raise ValueError("Regions would exclude entire scan") - self.size *= len(indicies) + self.size *= len(indices) self.dim_meta[dim]["mask"] = mask - self.dim_meta[dim]["indicies"] = indicies - self.index_dims.append(len(indicies)) + self.dim_meta[dim]["indices"] = indices + self.index_dims.append(len(indices)) repeat = self.size tile = 1 for dim in self.dimensions: - dim_length = len(self.dim_meta[dim]["indicies"]) + dim_length = len(self.dim_meta[dim]["indices"]) repeat /= dim_length self.dim_meta[dim]["tile"] = tile self.dim_meta[dim]["repeat"] = repeat @@ -196,21 +196,21 @@ def get_point(self, n): point = Point() # need to know how far along each dimension we are - # and, in the case of alternating indicies, how + # and, in the case of alternating indices, how # many times we've run through them kc = 0 # the "cumulative" k for each dimension for dim in self.dimensions: - indicies = self.dim_meta[dim]["indicies"] + indices = self.dim_meta[dim]["indices"] i = int(n // self.dim_meta[dim]["repeat"]) - i %= len(indicies) - k = indicies[i] + i %= len(indices) + k = indices[i] dim_reverse = False if dim.alternate and kc % 2 == 1: - i = len(indicies) - i - 1 + i = len(indices) - i - 1 dim_reverse = True - kc *= len(indicies) + kc *= len(indices) kc += k - k = indicies[i] + k = indices[i] # need point k along each generator in dimension # in alternating case, need to sometimes go backward point.indexes.append(i) From 58b441b276c3ec3daddea4753b8f9d46c7131a9b Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Wed, 22 Feb 2017 09:36:55 +0000 Subject: [PATCH 44/47] Make CompoundGenerator.prepare a no-op on successive calls --- scanpointgenerator/core/compoundgenerator.py | 10 +++++++++- tests/test_core/test_compoundgenerator.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index 4f1e928..d65cc36 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -32,7 +32,7 @@ def __init__(self, generators, excluders, mutators): self.dimensions = [] self.size = 1 self.dim_meta = {} - self.alternate_direction = [g.alternate_direction for g in generators] + self.prepared = False for generator in generators: logging.debug("Generator passed to Compound init") logging.debug(generator.to_dict()) @@ -52,6 +52,8 @@ def prepare(self): Prepare data structures and masks required for point generation. Must be called before get_point or iterator are called. """ + if self.prepared: + return self.dimensions = [] self.index_dims = [] self.dim_meta = {} @@ -170,6 +172,8 @@ def prepare(self): tile *= g.size self.generator_dim_scaling[g] = d + self.prepared = True + def iterator(self): """ Iterator yielding generator positions at each scan point @@ -177,6 +181,8 @@ def iterator(self): Yields: Point: The next point """ + if not self.prepared: + raise ValueError("CompoundGenerator has not been prepared") it = (self.get_point(n) for n in range_(self.size)) for p in it: yield p @@ -191,6 +197,8 @@ def get_point(self, n): Point: The requested point """ + if not self.prepared: + raise ValueError("CompoundGenerator has not been prepared") if n >= self.size: raise IndexError("Requested point is out of range") point = Point() diff --git a/tests/test_core/test_compoundgenerator.py b/tests/test_core/test_compoundgenerator.py index c1c4727..d6adb6d 100644 --- a/tests/test_core/test_compoundgenerator.py +++ b/tests/test_core/test_compoundgenerator.py @@ -39,6 +39,24 @@ def test_duplicate_name_raises(self): with self.assertRaises(ValueError): CompoundGenerator([y, x], [], []) + def test_raise_before_prepare(self): + x = LineGenerator("x", "mm", 1.0, 1.2, 3, True) + g = CompoundGenerator([x], [], []) + with self.assertRaises(ValueError): + g.get_point(0) + with self.assertRaises(ValueError): + for p in g.iterator(): pass + + def test_prepare_idempotent(self): + x = LineGenerator("x", "mm", 1.0, 1.2, 3, True) + g = CompoundGenerator([x], [], []) + x.prepare_positions = MagicMock(return_value=x.prepare_positions()) + g.prepare() + x.prepare_positions.assert_called_once_with() + x.prepare_positions.reset_mock() + g.prepare() + x.prepare_positions.assert_not_called() + def test_iterator(self): x = LineGenerator("x", "mm", 1.0, 2.0, 5, False) y = LineGenerator("y", "mm", 1.0, 2.0, 5, False) From c13849cfde614bc8e7c39499b9397a6cc53da35c Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Wed, 22 Feb 2017 15:11:57 +0000 Subject: [PATCH 45/47] Add test class for Dimension --- tests/test_core/test_dimension.py | 157 ++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 tests/test_core/test_dimension.py diff --git a/tests/test_core/test_dimension.py b/tests/test_core/test_dimension.py new file mode 100644 index 0000000..9fe0983 --- /dev/null +++ b/tests/test_core/test_dimension.py @@ -0,0 +1,157 @@ +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 np +from scanpointgenerator.core.dimension import Dimension + +from pkg_resources import require +require("mock") +from mock import Mock + + +class DimensionTests(ScanPointGeneratorTest): + + def test_init(self): + g = Mock() + g.axes = ["x", "y"] + d = Dimension(g) + self.assertEqual([g], d.generators) + self.assertEqual(["x", "y"], d.axes) + self.assertEqual([], d.masks) + self.assertEqual(g.alternate_direction, d.alternate) + self.assertEqual(g.size, d.size) + + def test_merge_dimensions(self): + g, h = Mock(), Mock() + g.axes, h.axes = ["gx", "gy"], ["hx", "hy"] + g.size, h.size = 16, 64 + outer, inner = Dimension(g), Dimension(h) + om1, om2 = Mock(), Mock() + im1, im2 = Mock(), Mock() + outer.masks = [{"repeat":2, "tile":3, "mask":om1}, + {"repeat":5, "tile":7, "mask":om2}] + inner.masks = [{"repeat":11, "tile":13, "mask":im1}, + {"repeat":17, "tile":19, "mask":im2}] + combined = Dimension.merge_dimensions(outer, inner) + + self.assertEqual(g.size * h.size, combined.size) + self.assertEqual(outer.alternate or inner.alternate, combined.alternate) + self.assertEqual(["gx", "gy", "hx", "hy"], combined.axes) + expected_masks = [ + {"repeat":128, "tile":3, "mask":om1}, + {"repeat":320, "tile":7, "mask":om2}, + {"repeat":11, "tile":13*16, "mask":im1}, + {"repeat":17, "tile":19*16, "mask":im2}] + self.assertEqual(expected_masks, combined.masks) + + def test_successive_merges(self): + g1, g2, h1, h2 = Mock(), Mock(), Mock(), Mock() + g1.axes, g2.axes = ["g1x"], ["g2x", "g2y"] + h1.axes, h2.axes = ["h1x", "h1y"], ["h2x"] + g1.size, g2.size = 5, 7 + h1.size, h2.size = 11, 13 + g2mask = Mock() + h1mask = Mock() + dg1, dg2 = Dimension(g1), Dimension(g2) + dh1, dh2 = Dimension(h1), Dimension(h2) + dg2.masks = [{"repeat":1, "tile":1, "mask":g2mask}] + dh1.masks = [{"repeat":1, "tile":1, "mask":h1mask}] + + outer = Dimension.merge_dimensions(dg1, dg2) + inner = Dimension.merge_dimensions(dh1, dh2) + self.assertEqual(5 * 7, outer.size) + self.assertEqual(11 * 13, inner.size) + self.assertEqual([{"repeat":1, "tile":5, "mask":g2mask}], outer.masks) + self.assertEqual([{"repeat":13, "tile":1, "mask":h1mask}], inner.masks) + combined = Dimension.merge_dimensions(outer, inner) + + expected_masks = [ + {"repeat":11*13, "tile":5, "mask":g2mask}, + {"repeat":13, "tile":5*7, "mask":h1mask}] + self.assertEqual(expected_masks, combined.masks) + self.assertEqual(5 * 7 * 11 * 13, combined.size) + self.assertEqual(["g1x", "g2x", "g2y", "h1x", "h1y", "h2x"], combined.axes) + + def test_create_dimension_mask(self): + d = Dimension(Mock(axes=["x", "y"])) + d.size = 30 + m1 = np.array([0, 1, 0, 1, 1, 0], dtype=np.int8) + m2 = np.array([1, 1, 0, 0, 1], dtype=np.int8) + d.masks = [ + {"repeat":2, "tile":2.5, "mask":m1}, + {"repeat":2, "tile":3, "mask":m2}] + e1 = np.array([0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0], dtype=np.int8) + e1 = np.append(np.tile(e1, 2), e1[:len(e1)//2]) + e2 = np.array([1, 1, 1, 1, 0, 0, 0, 0, 1, 1], dtype=np.int8) + e2 = np.tile(e2, 3) + expected = e1 & e2 + mask = d.create_dimension_mask() + self.assertEqual(expected.tolist(), mask.tolist()) + + def test_apply_excluder_over_single_gen(self): + x_pos = np.array([1, 2, 3, 4, 5]) + y_pos = np.array([10, 11, 12, 13, 14, 15]) + g = Mock(axes=["x", "y"], positions={"x":x_pos, "y":y_pos}) + g.alternate_direction = False + mask = np.array([1, 1, 0, 1, 0, 0], dtype=np.int8) + e = Mock(scannables=["x", "y"], create_mask=Mock(return_value=mask)) + d = Dimension(g) + d.apply_excluder(e) + d.masks[0]["mask"] = d.masks[0]["mask"].tolist() + self.assertEqual([{"repeat":1, "tile":1, "mask":mask.tolist()}], d.masks) + self.assertTrue((x_pos == e.create_mask.call_args[0][0]).all()) + self.assertTrue((y_pos == e.create_mask.call_args[0][1]).all()) + + def test_apply_excluders_over_multiple_gens(self): + gx_pos = np.array([1, 2, 3, 4, 5]) + hy_pos = np.array([-1, -2, -3]) + mask = np.full(15, 1, dtype=np.int8) + e = Mock(scannables=["gx", "hy"], create_mask=Mock(return_value=mask)) + g = Mock(axes=["gx", "gy"], positions={"gx":gx_pos}, size=len(gx_pos), alternate_direction=False) + h = Mock(axes=["hx", "hy"], positions={"hy":hy_pos}, size=len(hy_pos), alternate_direction=False) + d = Dimension(g) + d.generators = [g, h] + d.size = g.size * h.size + d.apply_excluder(e) + d.masks[0]["mask"] = d.masks[0]["mask"].tolist() + self.assertEqual([{"repeat":1, "tile":1, "mask":mask.tolist()}], d.masks) + self.assertTrue((np.repeat([1, 2, 3, 4, 5], 3) == e.create_mask.call_args[0][0]).all()) + self.assertTrue((np.tile([-1, -2, -3], 5) == e.create_mask.call_args[0][1]).all()) + + def test_apply_excluders_over_single_alternating(self): + x_pos = np.array([1, 2, 3, 4, 5]) + y_pos = np.array([10, 11, 12, 13, 14, 15]) + g = Mock(axes=["x", "y"], positions={"x":x_pos, "y":y_pos}) + g.alternate_direction = True + mask = np.array([1, 1, 0, 1, 0, 0], dtype=np.int8) + e = Mock(scannables=["x", "y"], create_mask=Mock(return_value=mask)) + d = Dimension(g) + d.apply_excluder(e) + d.masks[0]["mask"] = d.masks[0]["mask"].tolist() + self.assertEqual([{"repeat":1, "tile":0.5, "mask":mask.tolist()}], d.masks) + self.assertTrue((np.append(x_pos, x_pos[::-1]) == e.create_mask.call_args[0][0]).all()) + self.assertTrue((np.append(y_pos, y_pos[::-1]) == e.create_mask.call_args[0][1]).all()) + + def test_apply_excluders_with_scaling(self): + g1_pos = np.array([1, 2, 3]) + g2_pos = np.array([-1, -2]) + mask_func = lambda px, py: np.full(len(px), 1, dtype=np.int8) + g1 = Mock(axes=["g1"], positions={"g1":g1_pos}, size=len(g1_pos)) + g2 = Mock(axes=["g2"], positions={"g2":g2_pos}, size=len(g2_pos)) + e = Mock(scannables=["g1", "g2"], create_mask=Mock(side_effect=mask_func)) + d = Dimension(g1) + d.alternate = True + d.generators = [Mock(size=5, axes=[]), g1, g2, Mock(size=7, axes=[])] + d.size = 5 * len(g1_pos) * len(g2_pos) * 7 + d.apply_excluder(e) + d.masks[0]["mask"] = d.masks[0]["mask"].tolist() + expected_mask = [1] * 12 + self.assertEqual([{"repeat":7, "tile":2.5, "mask":expected_mask}], d.masks) + self.assertTrue((np.repeat(np.append(g1_pos, g1_pos[::-1]), 2) == e.create_mask.call_args[0][0]).all()) + self.assertTrue((np.tile(np.append(g2_pos, g2_pos[::-1]), 3) == e.create_mask.call_args[0][1]).all()) + +if __name__ == "__main__": + unittest.main(verbosity=2) From 3f3cd5086ec920a2c9d4e496adfb99b859898076 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Wed, 22 Feb 2017 15:12:36 +0000 Subject: [PATCH 46/47] Use Dimension size attribute instead of multiplying generator sizes --- scanpointgenerator/core/compoundgenerator.py | 4 +--- scanpointgenerator/core/dimension.py | 8 ++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/scanpointgenerator/core/compoundgenerator.py b/scanpointgenerator/core/compoundgenerator.py index d65cc36..ac01d70 100644 --- a/scanpointgenerator/core/compoundgenerator.py +++ b/scanpointgenerator/core/compoundgenerator.py @@ -163,9 +163,7 @@ def prepare(self): for dim in self.dimensions: tile = 1 - repeat = 1 - for g in dim.generators: - repeat *= g.size + repeat = dim.size for g in dim.generators: repeat /= g.size d = {"tile":tile, "repeat":repeat} diff --git a/scanpointgenerator/core/dimension.py b/scanpointgenerator/core/dimension.py index 82694d3..b5bb87d 100644 --- a/scanpointgenerator/core/dimension.py +++ b/scanpointgenerator/core/dimension.py @@ -93,14 +93,10 @@ def merge_dimensions(outer, inner): # repeated by the size of inner generators inner_masks = [m.copy() for m in inner.masks] outer_masks = [m.copy() for m in outer.masks] - scale = 1 - for g in inner.generators: - scale *= g.size + scale = inner.size for m in outer_masks: m["repeat"] *= scale - scale = 1 - for g in outer.generators: - scale *= g.size + scale = outer.size for m in inner_masks: m["tile"] *= scale dim.masks = outer_masks + inner_masks From a1775d7be186d3755d62eacfbdcf26cfe2dbc1d7 Mon Sep 17 00:00:00 2001 From: Charles Mita Date: Wed, 22 Feb 2017 15:13:11 +0000 Subject: [PATCH 47/47] Move comment on polygonal roi test The first line of the comment sometimes replaces the test name during large test runs, which is unhelpful in this case. --- tests/test_rois/test_polygonal_roi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_rois/test_polygonal_roi.py b/tests/test_rois/test_polygonal_roi.py index 882ffda..c70554b 100644 --- a/tests/test_rois/test_polygonal_roi.py +++ b/tests/test_rois/test_polygonal_roi.py @@ -33,6 +33,7 @@ def test_from_dict(self): self.assertEquals([[1, 1], [1, 2], [3, 1]], roi.points) def test_simple_point_contains(self): + vertices = [[0, 0], [1, 0], [2, -1], [2, 1], [-1, 1], [-1, -1]] """ Shape described looks like this: _____ @@ -40,7 +41,6 @@ def test_simple_point_contains(self): |/ \| """ - vertices = [[0, 0], [1, 0], [2, -1], [2, 1], [-1, 1], [-1, -1]] roi = PolygonalROI(vertices) p = [-0.9, -0.85] self.assertTrue(roi.contains_point(p)) @@ -78,4 +78,4 @@ def test_mask_points(self): self.assertEquals(expected, mask.tolist()) if __name__ == "__main__": - unittest.main() + unittest.main(verbosity=2)