diff --git a/org.eclipse.scanning.api/src/org/eclipse/scanning/api/points/Point.java b/org.eclipse.scanning.api/src/org/eclipse/scanning/api/points/Point.java index 3c5136b2e..8ed42286c 100644 --- a/org.eclipse.scanning.api/src/org/eclipse/scanning/api/points/Point.java +++ b/org.eclipse.scanning.api/src/org/eclipse/scanning/api/points/Point.java @@ -16,6 +16,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.eclipse.scanning.api.annotation.UiHidden; @@ -133,4 +135,21 @@ public Map getIndices() { } return indices; } + + private static final String VERTEX = "([a-zA-Z0-9_])+\\((\\d+)\\)=([-+]?[0-9]*\\.?[0-9]+)"; + private static final Pattern POSITION = Pattern.compile(VERTEX + ", " + VERTEX); + + /** + * Parse a point from the toString() method into an instance of Point + * @param asString + * @return + */ + public static Point parse(String asString) { + Matcher m = POSITION.matcher(asString); + if (m.matches()) { + return new Point(m.group(4), Integer.parseInt(m.group(5)), Double.parseDouble(m.group(6)), + m.group(1), Integer.parseInt(m.group(2)), Double.parseDouble(m.group(3))); + } + throw new RuntimeException("Unparsable string" + asString); + } } diff --git a/org.eclipse.scanning.points/scripts/jython_spg_interface.py b/org.eclipse.scanning.points/scripts/jython_spg_interface.py index 7da70a311..706885c3e 100644 --- a/org.eclipse.scanning.points/scripts/jython_spg_interface.py +++ b/org.eclipse.scanning.points/scripts/jython_spg_interface.py @@ -66,7 +66,7 @@ def toDict(self): return self.generator.to_dict() def size(self): - return self.generator.num + return self.generator.size class JLineGenerator1D(JavaIteratorWrapper): @@ -78,7 +78,9 @@ def __init__(self, name, units, start, stop, num_points, alternate_direction=Fal super(JLineGenerator1D, self).__init__() self.name = name - self.generator = LineGenerator(name, units, start, stop, num_points, alternate_direction) + line_gen = LineGenerator(name, units, start, stop, num_points, alternate_direction) + self.generator = CompoundGenerator([line_gen], [], []) + self.generator.prepare() logging.debug(self.generator.to_dict()) def _iterator(self): @@ -103,7 +105,9 @@ def __init__(self, names, units, start, stop, num_points, alternate_direction=Fa stop = stop.tolist() self.names = names - self.generator = LineGenerator(names, units, start, stop, num_points, alternate_direction) + line_gen = LineGenerator(names, units, start, stop, num_points, alternate_direction) + self.generator = CompoundGenerator([line_gen], [], []) + self.generator.prepare() logging.debug(self.generator.to_dict()) def _iterator(self): @@ -132,7 +136,9 @@ def __init__(self, name, units, points): points = points.tolist() # Convert from array to list self.name = name - self.generator = ArrayGenerator(name, units, points) + array_gen = ArrayGenerator(name, units, points) + self.generator = CompoundGenerator([array_gen], [], []) + self.generator.prepare() logging.debug(self.generator.to_dict()) def _iterator(self): @@ -155,11 +161,12 @@ def __init__(self, names, units, centre, radius, scale=1.0, alternate_direction= super(JSpiralGenerator, self).__init__() self.names = names - self.generator = SpiralGenerator(names, units, centre, radius, scale, alternate_direction) + spiral_gen = SpiralGenerator(names, units, centre, radius, scale, alternate_direction) + self.generator = CompoundGenerator([spiral_gen], [], []) + self.generator.prepare() logging.debug(self.generator.to_dict()) def _iterator(self): - x_name = self.names[0] y_name = self.names[1] @@ -183,7 +190,9 @@ def __init__(self, names, units, box, num_lobes, num_points): super(JLissajousGenerator, self).__init__() self.names = names - self.generator = LissajousGenerator(names, units, box, num_lobes, num_points) + liss_gen = LissajousGenerator(names, units, box, num_lobes, num_points) + self.generator = CompoundGenerator([liss_gen], [], []) + self.generator.prepare() logging.debug(self.generator.to_dict()) def _iterator(self): @@ -240,6 +249,15 @@ def __init__(self, iterators, excluders, mutators): scan_name.add(axis) self.dimension_names.add(scan_name) + for excluder in excluders: + # axes connected by excluders are "unrolled" + matched_axes = [a for a in self.axes_ordering if a in excluder.scannables] + if len(matched_axes) == 0: + continue + inner_axis = matched_axes[0] + inner_idx = self.axes_ordering.index(inner_axis) + for a in matched_axes[1:]: + self.index_locations[a] = inner_idx logging.debug("Index Locations:") logging.debug(self.index_locations) diff --git a/org.eclipse.scanning.points/scripts/scanpointgenerator/compat.py b/org.eclipse.scanning.points/scripts/scanpointgenerator/compat.py index fbe664331..95378c521 100644 --- a/org.eclipse.scanning.points/scripts/scanpointgenerator/compat.py +++ b/org.eclipse.scanning.points/scripts/scanpointgenerator/compat.py @@ -8,7 +8,7 @@ if os.name == 'java': - import scanpointgenerator.numjy as numpy + import numjy as numpy else: import numpy diff --git a/org.eclipse.scanning.points/scripts/scanpointgenerator/core/compoundgenerator.py b/org.eclipse.scanning.points/scripts/scanpointgenerator/core/compoundgenerator.py index 1e3c306d9..61486a618 100644 --- a/org.eclipse.scanning.points/scripts/scanpointgenerator/core/compoundgenerator.py +++ b/org.eclipse.scanning.points/scripts/scanpointgenerator/core/compoundgenerator.py @@ -1,12 +1,7 @@ import logging -import sys -if sys.platform.startswith('java'): - Lock = object # Workaround for GDA -else: - from threading import Lock - 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 @@ -15,11 +10,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: @@ -31,14 +27,11 @@ def __init__(self, generators, excluders, mutators): self.excluders = excluders self.mutators = mutators self.axes = [] - self.position_units = {} - self.axes_points = {} - self.axes_points_lower = {} - self.axes_points_upper = {} + self.units = {} self.dimensions = [] - self.alternate_direction = [g.alternate_direction for g in generators] - self.num = 1 - + self.size = 1 + self._dim_meta = {} + self._prepared = False for generator in generators: logging.debug("Generator passed to Compound init") logging.debug(generator.to_dict()) @@ -46,16 +39,24 @@ def __init__(self, generators, excluders, mutators): raise TypeError("CompoundGenerators cannot be nested, nest" "its constituent parts instead") self.axes += generator.axes - self.position_units.update(generator.position_units) + self.units.update(generator.units) if len(self.axes) != len(set(self.axes)): raise ValueError("Axis names cannot be duplicated") self.generators = generators - self.generator_dim_scaling = {} + self._generator_dim_scaling = {} def prepare(self): - self.num = 1 + """ + 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._dim_meta = {} + self._generator_dim_scaling = {} + # we're going to mutate these structures excluders = list(self.excluders) generators = list(self.generators) @@ -73,45 +74,36 @@ def prepare(self): 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)] + gen_1.prepare_positions() + gen_2.prepare_positions() + 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.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] + 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) + gen_1.axes, gen_1.units, points_1[0], points_1[-1], + len(points_1), gen_1.alternate) new_gen2 = LineGenerator( - gen_2.name, gen_2.units, points_2[0], points_2[-1], - len(points_2), gen_2.alternate_direction) + gen_2.axes, gen_2.units, points_2[0], points_2[-1], + len(points_2), gen_2.alternate) 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) - self.axes_points_upper.update(generator.points_upper) - 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) + 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 - # 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) \ @@ -120,203 +112,138 @@ 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 " \ - "alternate_direction setting") + "alternate setting") # 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.size = 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() + indices = np.nonzero(mask)[0] + if len(indices) == 0: raise ValueError("Regions would exclude entire scan") - repeat *= len(dim["indicies"]) - self.num = repeat + self.size *= len(indices) + self._dim_meta[dim]["mask"] = mask + self._dim_meta[dim]["indices"] = indices + + repeat = self.size + 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]["indices"]) + 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"]: - repeat *= g.num - for g in dim["generators"]: - repeat /= g.num + repeat = dim.size + for g in dim.generators: + repeat /= g.size d = {"tile":tile, "repeat":repeat} - tile *= g.num - self.generator_dim_scaling[g] = d + tile *= g.size + self._generator_dim_scaling[g] = d + + self._prepared = True def iterator(self): - it = (self.get_point(n) for n in range_(self.num)) - for m in self.mutators: - it = m.mutate(it) + """ + Iterator yielding generator positions at each scan point + + 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 def get_point(self, n): - if n >= self.num: + """ + Retrieve the desired point from the generator + + Args: + n (int): point to be generated + Returns: + 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") - p = Point() + point = Point() - # need to know how far along each index we are - # and, in the case of alternating indicies, how + # need to know how far along each dimension we are + # and, in the case of alternating indices, 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) - i %= len(indicies) - k = indicies[i] + indices = self._dim_meta[dim]["indices"] + i = int(n // self._dim_meta[dim]["repeat"]) + i %= len(indices) + k = indices[i] dim_reverse = False - if dim["alternate"] and kc % 2 == 1: - i = len(indicies) - i - 1 + if dim.alternate and kc % 2 == 1: + i = len(indices) - i - 1 dim_reverse = True - kc *= len(indicies) + kc *= len(indices) kc += k - k = indicies[i] - # need point k along each generator in index + k = indices[i] + # 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"]: - j = k // self.generator_dim_scaling[g]["repeat"] - gr = j // g.num - j %= g.num - if dim["alternate"] and g is not dim["generators"][0] and gr % 2 == 1: - j = g.num - j - 1 + point.indexes.append(i) + for g in dim.generators: + j = int(k // self._generator_dim_scaling[g]["repeat"]) + 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.size - 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, + # 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.points_lower[axis][j] - p.upper[axis] = g.points_upper[axis][j] - return p + 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.positions[axis][j] + point.upper[axis] = g.positions[axis][j] + for m in self.mutators: + point = m.mutate(point, n) + return point def to_dict(self): """Convert object attributes into a dictionary""" @@ -331,6 +258,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/org.eclipse.scanning.points/scripts/scanpointgenerator/core/dimension.py b/org.eclipse.scanning.points/scripts/scanpointgenerator/core/dimension.py new file mode 100644 index 000000000..086c91ee8 --- /dev/null +++ b/org.eclipse.scanning.points/scripts/scanpointgenerator/core/dimension.py @@ -0,0 +1,107 @@ +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.size + self.alternate = generator.alternate + self._masks = [] + + 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.size) + points_y = np.append(points_y, points_y[::-1]) + points_y = np.tile(points_y, gen_inner.size) + elif gen_inner is not gen_outer: + 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) + 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.size + else: + tile *= g.size + + 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 = inner.size + for m in outer_masks: + m["repeat"] *= scale + scale = outer.size + 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/org.eclipse.scanning.points/scripts/scanpointgenerator/core/generator.py b/org.eclipse.scanning.points/scripts/scanpointgenerator/core/generator.py index 6cdd52b96..29c72dd71 100644 --- a/org.eclipse.scanning.points/scripts/scanpointgenerator/core/generator.py +++ b/org.eclipse.scanning.points/scripts/scanpointgenerator/core/generator.py @@ -1,35 +1,51 @@ +from scanpointgenerator.compat import np class Generator(object): """Base class for all malcolm scan point generators Attributes: - position_units (dict): Dict of str position_name -> str position_unit + units (dict): Dict of str position_name -> str position_unit for each scannable dimension. E.g. {"x": "mm", "y": "mm"} index_dims (list): List of the int dimension sizes for the dataset. This - will have the same length as the position_units list for square + will have the same length as the units list for square scans but will be shorter for things like spiral scans. E.g. [15] index_names (list): List of the str dimension names for the dataset. This will have the same length as the index_dims. E.g. ["spiral_i"] axes (list): List of scannable names, used in GDA to reconstruct Point in CompoundGenerators """ - alternate_direction = False - position_units = None + alternate = False + units = None index_dims = None index_names = None + positions = None + bounds = None + size = 0 # Lookup table for generator subclasses _generator_lookup = {} axes = [] - def iterator(self): - """An iterator yielding positions at each scan point + 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.size) for positions and + np.arange(self.size + 1) - 0.5 for bounds. + + Args: + index_array (np.array): Index array to produce parameterised points - Yields: - Point: The next scan :class:`Point` + Returns: + Positions: Dictionary of axis names to position/bounds arrays """ raise NotImplementedError + def prepare_positions(self): + self.positions = self.prepare_arrays(np.arange(self.size)) + + def prepare_bounds(self): + 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""" raise NotImplementedError diff --git a/org.eclipse.scanning.points/scripts/scanpointgenerator/core/mutator.py b/org.eclipse.scanning.points/scripts/scanpointgenerator/core/mutator.py index ccd987947..171906e20 100644 --- a/org.eclipse.scanning.points/scripts/scanpointgenerator/core/mutator.py +++ b/org.eclipse.scanning.points/scripts/scanpointgenerator/core/mutator.py @@ -7,16 +7,17 @@ class Mutator(object): # Lookup table for mutator subclasses _mutator_lookup = {} - def mutate(self, iterator): + def mutate(self, point, index): """ - 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 + Index: one-dimensional linear index of point - Yields: - Point: Mutated points from generator + Returns: + Point: Mutated point """ raise NotImplementedError diff --git a/org.eclipse.scanning.points/scripts/scanpointgenerator/generators/arraygenerator.py b/org.eclipse.scanning.points/scripts/scanpointgenerator/generators/arraygenerator.py index 587f02a06..7f1fcccec 100644 --- a/org.eclipse.scanning.points/scripts/scanpointgenerator/generators/arraygenerator.py +++ b/org.eclipse.scanning.points/scripts/scanpointgenerator/generators/arraygenerator.py @@ -1,161 +1,74 @@ -from scanpointgenerator.compat import range_ +from scanpointgenerator.compat import np from scanpointgenerator.core import Generator -from scanpointgenerator.core import Point +def to_list(value): + if isinstance(value, list): + return value + else: + return [value] @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)): + """Generate points fron a given list of positions""" + + def __init__(self, axes, units, points, alternate_direction=False): + self.axes = to_list(axes) + self.units = {d:u for (d, u) in zip(self.axes, to_list(units))} + self.alternate_direction = alternate_direction + self.points = np.array(points, dtype=np.float64) + if self.points.shape == (len(self.points),): + self.points = self.points.reshape((len(self.points), 1)) + self.size = len(self.points) + if len(self.axes) != len(set(self.axes)): 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 + axes) + + gen_name = "Array" + for axis_name in self.axes[::-1]: + gen_name = axis_name + "_" + gen_name + self.index_names = [gen_name] + + def prepare_arrays(self, index_array): + points = self.points + # add linear extension to ends of points, representing t=-1 and t=N+1 + v_left = points[0] - (points[1] - points[0]) + v_right = points[-1] + (points[-1] - points[-2]) + shape = points.shape + shape = (shape[0] + 2,) + shape[1:] + extended = np.empty(shape, dtype=points.dtype) + extended[1:-1] = points + extended[0] = v_left + extended[-1] = v_right + points = extended + index_floor = np.floor(index_array).astype(np.int32) + epsilon = index_array - index_floor + epsilon = epsilon.reshape((-1, 1)) + + index_floor += 1 + + values = points[index_floor] + epsilon * (points[index_floor+1] - points[index_floor]) + values = values.T + arrays = {} + for (i, name) in enumerate(self.axes): + arrays[name] = values[i] + return arrays 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 - + d = { + "typeid":self.typeid, + "axes":self.axes, + "units":[self.units[a] for a in self.axes], + "points":self.points.ravel().tolist(), + "alternate_direction":self.alternate_direction, + } 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) + axes = d["axes"] + units = d["units"] + alternate_direction = d["alternate_direction"] + flat_points = d["points"] + arr_shape = (int(len(flat_points) // len(axes)), len(axes)) + points = np.array(flat_points).reshape(arr_shape) + return cls(axes, units, points, alternate_direction) diff --git a/org.eclipse.scanning.points/scripts/scanpointgenerator/generators/linegenerator.py b/org.eclipse.scanning.points/scripts/scanpointgenerator/generators/linegenerator.py index ae54ff9af..ae4e6835f 100644 --- a/org.eclipse.scanning.points/scripts/scanpointgenerator/generators/linegenerator.py +++ b/org.eclipse.scanning.points/scripts/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): @@ -14,118 +13,79 @@ 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, axes, units, start, stop, size, alternate=False): """ Args: - name (str/list(str)): The scannable name(s) E.g. "x" or ["x", "y"] - units (str): The scannable units. E.g. "mm" + axes (str/list(str)): The scannable axes E.g. "x" or ["x", "y"] + units (str/list(str)): The scannable units. E.g. "mm" or ["mm", "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 + size (int): The number of points to generate. E.g. 5 + alternate(bool): Specifier to reverse direction if generator is nested """ - - self.name = to_list(name) + self.axes = to_list(axes) 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 - self.units = units + self.alternate = alternate + self.units = {d:u for (d, u) in zip(self.axes, to_list(units))} - if len(self.name) != len(set(self.name)): + if len(self.axes) != len(set(self.axes)): raise ValueError("Axis names cannot be duplicated; given %s" % - name) + axes) - if len(self.name) != len(self.start) or \ - len(self.name) != len(self.stop): + if len(self.axes) != len(self.start) or \ + len(self.axes) != len(self.stop): raise ValueError( - "Dimensions of name, start and stop do not match") + "Dimensions of axes, start and stop do not match") + if len(self.axes) != len(self.units): + raise ValueError("Provided units do not match number of axes") - self.num = num - self.num_axes = len(self.name) + self.size = size 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: + if len(self.axes) > 1: gen_name = "Line" - for axis_name in self.name[::-1]: + for axis_name in self.axes[::-1]: gen_name = axis_name + "_" + gen_name self.index_names = [gen_name] else: - self.index_names = self.name - - self.axes = self.name # For GDA - - 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] + self.index_names = self.axes + + def prepare_arrays(self, index_array): + arrays = {} + for axis, start, stop in zip(self.axes, self.start, self.stop): 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]) - 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): - 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 + step = float(d) + # 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 def to_dict(self): """Convert object attributes into a dictionary""" d = dict() d['typeid'] = self.typeid - d['name'] = self.name - d['units'] = self.units + d['axes'] = self.axes + d['units'] = [self.units[a] for a in self.axes] d['start'] = self.start d['stop'] = self.stop - d['num'] = self.num - d['alternate_direction'] = self.alternate_direction + d['size'] = self.size + d['alternate'] = self.alternate return d @@ -141,11 +101,11 @@ def from_dict(cls, d): LineGenerator: New LineGenerator instance """ - name = d['name'] + axes = d['axes'] units = d['units'] start = d['start'] stop = d['stop'] - num = d['num'] - alternate_direction = d['alternate_direction'] + size = d['size'] + alternate = d['alternate'] - return cls(name, units, start, stop, num, alternate_direction) + return cls(axes, units, start, stop, size, alternate) diff --git a/org.eclipse.scanning.points/scripts/scanpointgenerator/generators/lissajousgenerator.py b/org.eclipse.scanning.points/scripts/scanpointgenerator/generators/lissajousgenerator.py index 594aad9d9..75f8d5a04 100644 --- a/org.eclipse.scanning.points/scripts/scanpointgenerator/generators/lissajousgenerator.py +++ b/org.eclipse.scanning.points/scripts/scanpointgenerator/generators/lissajousgenerator.py @@ -9,12 +9,12 @@ class LissajousGenerator(Generator): """Generate the points of a Lissajous curve""" - def __init__(self, names, units, box, num_lobes, - num_points=None, alternate_direction=False): + def __init__(self, axes, units, box, num_lobes, + num_points=None, alternate=False): """ Args: - names (list(str)): The scannable names e.g. ["x", "y"] - units (str): The scannable units e.g. "mm" + axes (list(str)): The scannable axes e.g. ["x", "y"] + units (list(str)): The scannable units e.g. ["mm", "mm"] box(dict): Dictionary of centre, width and height representing box to fill with points num_lobes(int): Number of x-direction lobes for curve; will @@ -23,17 +23,16 @@ def __init__(self, names, units, box, num_lobes, curve. Default is 250 * num_lobes """ - self.names = names - self.units = units - self.points = None - self.points_lower = None - self.points_upper = None - self.alternate_direction = alternate_direction - - if len(self.names) != len(set(self.names)): + if len(self.axes) != len(set(self.axes)): raise ValueError("Axis names cannot be duplicated; given %s" % - names) + axes) + + if len(units) != len(axes): + raise ValueError("Provided units do not match number of axes") + self.axes = axes + self.units = {d:u for d,u in zip(axes, units)} + self.alternate = alternate num_lobes = int(num_lobes) self.x_freq = num_lobes @@ -41,65 +40,32 @@ 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]: + for axis_name in self.axes[::-1]: gen_name = axis_name + "_" + gen_name self.index_names = [gen_name] - self.axes = self.names # For GDA - - def _calc_arrays(self, offset): + 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 - f = lambda t: y0 + A * np.sin(a * 2 * m.pi * (t+offset)/self.num + d) - x = f(np.arange(self.num)) - f = lambda t: B * np.sin(b * 2 * m.pi * (t+offset)/self.num) - y = f(np.arange(self.num)) - 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] + \ - 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 + 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.axes[0]] = fx(index_array) + arrays[self.axes[1]] = fy(index_array) + return arrays def to_dict(self): """Convert object attributes into a dictionary""" @@ -111,11 +77,11 @@ def to_dict(self): d = dict() d['typeid'] = self.typeid - d['names'] = self.names - d['units'] = list(self.position_units.values())[0] + d['axes'] = self.axes + d['units'] = [self.units[a] for a in self.axes] d['box'] = box d['num_lobes'] = self.x_freq - d['num_points'] = self.num + d['num_points'] = self.size return d @@ -131,10 +97,10 @@ def from_dict(cls, d): LissajousGenerator: New LissajousGenerator instance """ - names = d['names'] + axes = d['axes'] units = d['units'] box = d['box'] num_lobes = d['num_lobes'] num_points = d['num_points'] - return cls(names, units, box, num_lobes, num_points) + return cls(axes, units, box, num_lobes, num_points) diff --git a/org.eclipse.scanning.points/scripts/scanpointgenerator/generators/spiralgenerator.py b/org.eclipse.scanning.points/scripts/scanpointgenerator/generators/spiralgenerator.py index 1d763703d..d06d96b4c 100644 --- a/org.eclipse.scanning.points/scripts/scanpointgenerator/generators/spiralgenerator.py +++ b/org.eclipse.scanning.points/scripts/scanpointgenerator/generators/spiralgenerator.py @@ -9,111 +9,74 @@ class SpiralGenerator(Generator): """Generate the points of an Archimedean spiral""" - def __init__(self, names, units, centre, radius, scale=1.0, - alternate_direction=False): + def __init__(self, axes, units, centre, radius, scale=1.0, + alternate=False): """ Args: - names (list(str)): The scannable names e.g. ["x", "y"] - units (str): The scannable units e.g. "mm" + axes (list(str)): The scannable axes e.g. ["x", "y"] + units (list(str)): The scannable units e.g. ["mm", "mm"] centre(list): List of two coordinates of centre point of spiral 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 + alternate(bool): Specifier to reverse direction if generator is nested """ - self.names = names - self.units = units + if len(self.axes) != len(set(self.axes)): + raise ValueError("Axis names cannot be duplicated; given %s" % + axes) + self.axes = axes self.centre = centre self.radius = radius self.scale = scale - self.alternate_direction = alternate_direction - self.points = None - self.points_lower = None - self.points_upper = None + self.alternate = alternate + self.units = {d:u for d,u in zip(axes, units)} - if len(self.names) != len(set(self.names)): + if len(self.axes) != len(set(self.axes)): 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 + axes) - 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]: + for axis_name in self.axes[::-1]: gen_name = axis_name + "_" + gen_name self.index_names = [gen_name] - 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 = phi_t(np.arange(size)) + self.alpha = m.sqrt(4 * m.pi) # Theta scale factor = k + self.beta = scale / (2 * m.pi) # Radius scale factor = b + 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.size + # 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(index_array) 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) - 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 + arrays[self.axes[0]] = x + arrays[self.axes[1]] = y + return arrays def to_dict(self): """Convert object attributes into a dictionary""" d = dict() d['typeid'] = self.typeid - d['names'] = self.names - d['units'] = list(self.position_units.values())[0] + d['axes'] = self.axes + d['units'] = [self.units[a] for a in self.axes] d['centre'] = self.centre d['radius'] = self.radius d['scale'] = self.scale - d['alternate_direction'] = self.alternate_direction + d['alternate'] = self.alternate return d @@ -129,11 +92,11 @@ def from_dict(cls, d): SpiralGenerator: New SpiralGenerator instance """ - names = d['names'] + axes = d['axes'] units = d['units'] centre = d['centre'] radius = d['radius'] scale = d['scale'] - alternate_direction = d['alternate_direction'] + alternate = d['alternate'] - return cls(names, units, centre, radius, scale, alternate_direction) + return cls(axes, units, centre, radius, scale, alternate) diff --git a/org.eclipse.scanning.points/scripts/scanpointgenerator/mutators/fixeddurationmutator.py b/org.eclipse.scanning.points/scripts/scanpointgenerator/mutators/fixeddurationmutator.py index dc062cf3f..20111f154 100644 --- a/org.eclipse.scanning.points/scripts/scanpointgenerator/mutators/fixeddurationmutator.py +++ b/org.eclipse.scanning.points/scripts/scanpointgenerator/mutators/fixeddurationmutator.py @@ -11,20 +11,20 @@ def __init__(self, duration): """ self.duration = duration - def mutate(self, iterator): + def mutate(self, point, index): """ Applies duration to points in the given iterator, yielding them Args: - iterator: Iterator to mutate + Point: Point to mutate + Index: one-dimensional index of point - 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/org.eclipse.scanning.points/scripts/scanpointgenerator/mutators/randomoffsetmutator.py b/org.eclipse.scanning.points/scripts/scanpointgenerator/mutators/randomoffsetmutator.py index d2bfb07e0..4c4900820 100644 --- a/org.eclipse.scanning.points/scripts/scanpointgenerator/mutators/randomoffsetmutator.py +++ b/org.eclipse.scanning.points/scripts/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") @@ -18,92 +17,49 @@ 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 + 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.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 + 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): """Convert object attributes into a dictionary""" diff --git a/org.eclipse.scanning.points/scripts/scanpointgenerator/plotgenerator.py b/org.eclipse.scanning.points/scripts/scanpointgenerator/plotgenerator.py index f884b75a5..39684ea9b 100644 --- a/org.eclipse.scanning.points/scripts/scanpointgenerator/plotgenerator.py +++ b/org.eclipse.scanning.points/scripts/scanpointgenerator/plotgenerator.py @@ -1,3 +1,5 @@ +from scanpointgenerator import CompoundGenerator, RectangularROI, CircularROI + MARKER_SIZE = 10 @@ -10,12 +12,16 @@ 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): + excluders = [] if excluder is None else [excluder] + gen = CompoundGenerator([gen], excluders, []) + gen.prepare() + # points for spline generation x, y = [], [] # capture points and indexes @@ -61,9 +67,9 @@ def plot_generator(gen, excluder=None, show_indexes=True): y.append(point.upper.get("y", 0)) # # Plot labels - plt.xlabel("X (%s)" % gen.position_units["x"]) - if "y" in gen.position_units: - plt.ylabel("Y (%s)" % gen.position_units["y"]) + plt.xlabel("X (%s)" % gen.units["x"]) + if "y" in gen.units: + plt.ylabel("Y (%s)" % gen.units["y"]) else: plt.tick_params(left='off', labelleft='off') @@ -100,6 +106,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/org.eclipse.scanning.points/src/org/eclipse/scanning/points/CompoundSpgIterator.java b/org.eclipse.scanning.points/src/org/eclipse/scanning/points/CompoundSpgIterator.java index 92800740d..ff3cc3ac6 100644 --- a/org.eclipse.scanning.points/src/org/eclipse/scanning/points/CompoundSpgIterator.java +++ b/org.eclipse.scanning.points/src/org/eclipse/scanning/points/CompoundSpgIterator.java @@ -14,7 +14,6 @@ import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; -import java.util.List; import org.eclipse.dawnsci.analysis.dataset.roi.CircularROI; import org.eclipse.dawnsci.analysis.dataset.roi.EllipticalROI; @@ -70,7 +69,7 @@ public CompoundSpgIterator(CompoundGenerator gen) throws GeneratorException { JythonObjectFactory compoundGeneratorFactory = ScanPointGeneratorFactory.JCompoundGeneratorFactory(); - Object[] excluders = {}; //getExcluders(gen.getModel().getRegions()); TODO put back in when excluders are fixed in Python + Object[] excluders = getExcluders(gen.getModel().getRegions()); Object[] mutators = getMutators(gen.getModel().getMutators()); @SuppressWarnings("unchecked") diff --git a/org.eclipse.scanning.points/src/org/eclipse/scanning/points/LineIterator.java b/org.eclipse.scanning.points/src/org/eclipse/scanning/points/LineIterator.java index c95582807..a1acb3e9a 100644 --- a/org.eclipse.scanning.points/src/org/eclipse/scanning/points/LineIterator.java +++ b/org.eclipse.scanning.points/src/org/eclipse/scanning/points/LineIterator.java @@ -58,12 +58,13 @@ public LineIterator(OneDEqualSpacingGenerator gen) { double yStep = step * Math.sin(line.getAngle()); PyList names = new PyList(Arrays.asList(new String[] {model.getFastAxisName(), model.getSlowAxisName()})); + PyList units = new PyList(Arrays.asList(new String[] {"mm", "mm"})); double[] start = {line.getxStart() + xStep/2, line.getyStart() + yStep/2}; double[] stop = {line.getxStart() + xStep * (numPoints - 0.5), line.getyStart() + yStep * (numPoints - 0.5)}; @SuppressWarnings("unchecked") Iterator iterator = (Iterator) lineGeneratorFactory.createObject( - names, "mm", start, stop, numPoints); + names, units, start, stop, numPoints); pyIterator = iterator; } @@ -78,12 +79,13 @@ public LineIterator(OneDStepGenerator gen) { double yStep = model.getStep() * Math.sin(line.getAngle()); PyList names = new PyList(Arrays.asList(new String[] {model.getFastAxisName(), model.getSlowAxisName()})); + PyList units = new PyList(Arrays.asList(new String[] {"mm", "mm"})); double[] start = {line.getxStart(), line.getyStart()}; double[] stop = {line.getxStart() + xStep * numPoints, line.getyStart() + yStep * numPoints}; @SuppressWarnings("unchecked") Iterator iterator = (Iterator) lineGeneratorFactory.createObject( - names, "mm", start, stop, numPoints); + names, units, start, stop, numPoints); pyIterator = iterator; } diff --git a/org.eclipse.scanning.test/src/org/eclipse/scanning/test/command/MScanServletTest.java b/org.eclipse.scanning.test/src/org/eclipse/scanning/test/command/MScanServletTest.java index 84214e655..81d732cbb 100644 --- a/org.eclipse.scanning.test/src/org/eclipse/scanning/test/command/MScanServletTest.java +++ b/org.eclipse.scanning.test/src/org/eclipse/scanning/test/command/MScanServletTest.java @@ -39,7 +39,7 @@ public void testGridScanNoDetector() throws Exception { @Test public void testGridWithROIScan() throws Exception { - pi.exec("sr = scan_request(grid(axes=('xNex', 'yNex'), start=(0.0, 1.0), stop=(10.0, 12.0), count=(3, 4), snake=False, roi=[circ(origin=(0.0, 0.0), radius=1.0)]), det=detector('mandelbrot', 0.1))"); + pi.exec("sr = scan_request(grid(axes=('xNex', 'yNex'), start=(0.0, 1.0), stop=(10.0, 12.0), count=(3, 4), snake=False, roi=[circ(origin=(0.0, 0.0), radius=2.0)]), det=detector('mandelbrot', 0.1))"); runAndCheck("sr", false, 10); } diff --git a/org.eclipse.scanning.test/src/org/eclipse/scanning/test/points/CompoundTest.java b/org.eclipse.scanning.test/src/org/eclipse/scanning/test/points/CompoundTest.java index f1ed3cf8d..d6407439c 100644 --- a/org.eclipse.scanning.test/src/org/eclipse/scanning/test/points/CompoundTest.java +++ b/org.eclipse.scanning.test/src/org/eclipse/scanning/test/points/CompoundTest.java @@ -11,6 +11,7 @@ *******************************************************************************/ package org.eclipse.scanning.test.points; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -146,11 +147,11 @@ public void testSimpleToDict() throws Exception { PyList gens = (PyList) dict.get("generators"); PyDictionary line1 = (PyDictionary) gens.get(0); - assertEquals("Temperature", (String) ((PyList) line1.get("name")).get(0)); - assertEquals("mm", line1.get("units")); + assertEquals("Temperature", (String) ((PyList) line1.get("axes")).get(0)); + assertArrayEquals(new String[] {"mm"}, ((PyList) line1.get("units")).toArray()); assertEquals(290.0, (double) ((PyList) line1.get("start")).get(0), 1E-10); assertEquals(295.0, (double) ((PyList) line1.get("stop")).get(0), 1E-10); - assertEquals(6, (int) line1.get("num")); + assertEquals(6, (int) line1.get("size")); PyList excluders = (PyList) dict.get("excluders"); PyList mutators = (PyList) dict.get("mutators"); @@ -171,17 +172,17 @@ public void testNestedToDict() throws Exception { PyDictionary line1 = (PyDictionary) gens.get(0); PyDictionary line2 = (PyDictionary) gens.get(1); - assertEquals("Temperature", (String) ((PyList) line1.get("name")).get(0)); - assertEquals("mm", line1.get("units")); + assertEquals("Temperature", (String) ((PyList) line1.get("axes")).get(0)); + assertArrayEquals(new String[] {"mm"}, ((PyList) line1.get("units")).toArray()); assertEquals(290.0, (double) ((PyList) line1.get("start")).get(0), 1E-10); assertEquals(295.0, (double) ((PyList) line1.get("stop")).get(0), 1E-10); - assertEquals(6, (int) line1.get("num")); + assertEquals(6, (int) line1.get("size")); - assertEquals("Position", (String) ((PyList) line2.get("name")).get(0)); - assertEquals("mm", line2.get("units")); + assertEquals("Position", (String) ((PyList) line2.get("axes")).get(0)); + assertArrayEquals(new String[] {"mm"}, ((PyList) line1.get("units")).toArray()); assertEquals(1.0, (double) ((PyList) line2.get("start")).get(0), 1E-10); assertEquals(4.0, (double) ((PyList) line2.get("stop")).get(0), 1E-10); - assertEquals(6, (int) line2.get("num")); + assertEquals(6, (int) line2.get("size")); PyList excluders = (PyList) dict.get("excluders"); PyList mutators = (PyList) dict.get("mutators"); diff --git a/org.eclipse.scanning.test/src/org/eclipse/scanning/test/points/RandomOffsetGridTest.java b/org.eclipse.scanning.test/src/org/eclipse/scanning/test/points/RandomOffsetGridTest.java index 9d4abcfd4..3e239af36 100644 --- a/org.eclipse.scanning.test/src/org/eclipse/scanning/test/points/RandomOffsetGridTest.java +++ b/org.eclipse.scanning.test/src/org/eclipse/scanning/test/points/RandomOffsetGridTest.java @@ -12,15 +12,16 @@ package org.eclipse.scanning.test.points; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; -import java.util.List; +import java.util.Iterator; import org.eclipse.scanning.api.points.IPointGenerator; import org.eclipse.scanning.api.points.IPointGeneratorService; import org.eclipse.scanning.api.points.IPosition; -import org.eclipse.scanning.api.points.Point; import org.eclipse.scanning.api.points.models.BoundingBox; +import org.eclipse.scanning.api.points.models.GridModel; import org.eclipse.scanning.api.points.models.RandomOffsetGridModel; import org.eclipse.scanning.points.PointGeneratorService; import org.junit.Before; @@ -44,22 +45,27 @@ public void testSimpleBox() throws Exception { box.setFastAxisLength(5); box.setSlowAxisLength(10); - RandomOffsetGridModel model = new RandomOffsetGridModel("x", "y"); - model.setSlowAxisPoints(5); - model.setFastAxisPoints(5); - model.setBoundingBox(box); - model.setSeed(10); - model.setOffset(25); + RandomOffsetGridModel rm = new RandomOffsetGridModel("x", "y"); + rm.setSlowAxisPoints(5); + rm.setFastAxisPoints(5); + rm.setBoundingBox(box); + rm.setSeed(10); + rm.setOffset(25); + IPointGenerator rg = service.createGenerator(rm); + GeneratorUtil.testGeneratorPoints(rg, 5, 5); - IPointGenerator gen = service.createGenerator(model); - List pointList = gen.createPoints(); - - assertEquals(new Point("x", 0, 0.012403455250000084,"y", 0, 0.09924303325000006), pointList.get(0)); - assertEquals(new Point("x", 1, 0.837235318,"y", 0, 0.1643560529999999), pointList.get(1)); - assertEquals(new Point("x", 2, 2.20470153075,"y", 0, 0.018593022749999966), pointList.get(2)); - assertEquals(new Point("x", 3, 3.057925353,"y", 0, -0.024424061750000003), pointList.get(3)); - assertEquals(new Point("x", 4, 3.78130160075,"y", 0, 0.021858763000000003), pointList.get(4)); - assertEquals(new Point("x", 0, 0.09698760274999996,"y", 1, 1.83863665575), pointList.get(5)); + GridModel m = new GridModel("x", "y"); + m.setSlowAxisPoints(5); + m.setFastAxisPoints(5); + m.setBoundingBox(box); + IPointGenerator g = service.createGenerator(m); + + for (Iterator it1 = rg.iterator(), it2 = g.iterator(); it1.hasNext() && it2.hasNext();) { + IPosition t1 = it1.next(); + IPosition t2 = it2.next(); + assertTrue(Math.abs(t1.getValue("x") - t2.getValue("x")) <= 0.25 * 1.25); + assertTrue(Math.abs(t1.getValue("y") - t2.getValue("y")) <= 0.25 * 2.5); + } } } diff --git a/org.eclipse.scanning.test/src/org/eclipse/scanning/test/points/ScanPointGeneratorFactoryTest.java b/org.eclipse.scanning.test/src/org/eclipse/scanning/test/points/ScanPointGeneratorFactoryTest.java index beb9be824..5d70ecbfa 100644 --- a/org.eclipse.scanning.test/src/org/eclipse/scanning/test/points/ScanPointGeneratorFactoryTest.java +++ b/org.eclipse.scanning.test/src/org/eclipse/scanning/test/points/ScanPointGeneratorFactoryTest.java @@ -64,12 +64,13 @@ public void testJLineGeneratorFactory2D() { JythonObjectFactory lineGeneratorFactory = ScanPointGeneratorFactory.JLineGenerator2DFactory(); PyList names = new PyList(Arrays.asList(new String[] {"X", "Y"})); + PyList units = new PyList(Arrays.asList(new String[] {"mm", "mm"})); double[] start = {1.0, 2.0}; double[] stop = {5.0, 10.0}; @SuppressWarnings("unchecked") Iterator iterator = (Iterator) lineGeneratorFactory.createObject( - names, "mm", start, stop, 5); + names, units, start, stop, 5); List expected_points = new ArrayList(); expected_points.add(new Point("X", 0, 1.0, "Y", 0, 2.0, false)); @@ -160,12 +161,13 @@ public void testJLissajousGeneratorFactory() { box.put("centre", new double[] {0.0, 0.0}); PyList names = new PyList(Arrays.asList(new String[] {"X", "Y"})); + PyList units = new PyList(Arrays.asList(new String[] {"mm", "mm"})); int numLobes = 2; int numPoints = 500; @SuppressWarnings("unchecked") Iterator iterator = (Iterator) lissajousGeneratorFactory.createObject( - names, "mm", box, numLobes, numPoints); + names, units, box, numLobes, numPoints); List expected_points = new ArrayList(); expected_points.add(new Point("X", 0, 0.0, "Y", 0, 0.0, false)); @@ -263,28 +265,20 @@ public void testJCompoundGeneratorFactoryWithMutatedRaster() { generators, excluders, mutators); List expected_points = new ArrayList(); -// expected_points.add(new MapPosition("x:0:1.1984860665000001, y:0:2.0248069105")); -// expected_points.add(new MapPosition("x:1:2.328712106, y:0:1.674470636")); -// expected_points.add(new MapPosition("x:2:3.0371860455, y:0:2.4094030615")); -// expected_points.add(new MapPosition("x:3:3.9511518765, y:0:2.115850706")); -// expected_points.add(new MapPosition("x:4:5.043717526, y:0:1.5626032015")); -// expected_points.add(new MapPosition("x:0:0.6772733115, y:1:4.1939752055")); -// expected_points.add(new MapPosition("x:1:1.5828061555000001, y:1:3.9489767459999996")); -// expected_points.add(new MapPosition("x:2:3.3888981960000004, y:1:3.661987452")); -// expected_points.add(new MapPosition("x:3:3.9093635265, y:1:4.2730717205")); -// expected_points.add(new MapPosition("x:4:4.554744956, y:1:3.8436031415")); - expected_points.add(new Point("x", 0, 1.0248069105000002, "y", 0, 2.1984860665)); - expected_points.add(new Point("x", 1, 1.674470636, "y", 0, 2.328712106)); - expected_points.add(new Point("x", 2, 3.4094030615, "y", 0, 2.0371860455)); - expected_points.add(new Point("x", 3, 4.115850706, "y", 0, 1.9511518765)); - expected_points.add(new Point("x", 4, 4.5626032015, "y", 0, 2.043717526)); - expected_points.add(new Point("x", 0, 1.1939752055, "y", 1, 3.6772733115)); - expected_points.add(new Point("x", 1, 1.9489767459999996, "y", 1, 3.5828061555000001)); - expected_points.add(new Point("x", 2, 2.661987452, "y", 1, 4.3888981960000004)); - expected_points.add(new Point("x", 3, 4.2730717205, "y", 1, 3.9093635265)); - expected_points.add(new Point("x", 4, 4.8436031415, "y", 1, 3.554744956)); - - int index = 0; + // This list of values can be regenerated by running the iterator and printing + // the points. The toString() method is then inverted to a Point using parse. + expected_points.add(Point.parse("y(0)=2.3467273793292063, x(0)=1.3423125841287693")); + expected_points.add(Point.parse("y(0)=1.9389526430608128, x(1)=2.386072452619223")); + expected_points.add(Point.parse("y(0)=2.2767811479924203, x(2)=3.2131047487987914")); + expected_points.add(Point.parse("y(0)=1.585269318447744, x(3)=3.9674925148644236")); + expected_points.add(Point.parse("y(0)=2.397560627408689, x(4)=5.266805527444651")); + expected_points.add(Point.parse("y(1)=3.5015289536680863, x(0)=1.3197151394141176")); + expected_points.add(Point.parse("y(1)=4.341493416307841, x(1)=2.150155718356873")); + expected_points.add(Point.parse("y(1)=4.480657927454603, x(2)=3.377400373778632")); + expected_points.add(Point.parse("y(1)=4.1811883078611425, x(3)=4.067826935687993")); + expected_points.add(Point.parse("y(1)=4.308295570036465, x(4)=4.779800611147611")); + + int index = 0; while (iterator.hasNext() && index < 10){ // Just get first few points Object point = iterator.next();