def points_from_axis_point(dim, index, length): points = Points() dimension_points = {axis: np.full(length, dim.positions[axis][index]) for axis in dim.positions} lower, upper = dim.get_bounds(index) points.positions.update(dimension_points) points.lower.update({axis: np.full(length, lower[axis]) for axis in lower}) points.upper.update({axis: np.full(length, upper[axis]) for axis in upper}) points.indexes = np.full(length, index) return points
def get_points(self, start, finish): """ Retrieve a Points object: a wrapper for an array of Point from the generator Args: start (int), finish (int): indices of the first point and final+1th point to include i.e. get_points(1, 5) would return a Points of Point 1, 2, 3 & 4 but not 5. Returns: Points: a wrapper object with the data of the requested Point [plural] """ if not self._prepared: raise ValueError("CompoundGenerator has not been prepared") ''' situations: dim N constant, dim N+1 constant (e.g. 1,1 -> 1,1) dim N constant, dim N+1 increasing: (e.g. n,1->n,5) dim N increasing, dim N+1 increasing: (n,1->n+1,2) N[n],N+1[start]->N[n],N+1[max]->N[n+1],N+1[0]->...->N[K],N+1[finish] dim N increasing, dim N+1 decreasing: (n,2->n+1,1) as above dim N increasing, dim N+1 constant: (n,1->n+1,1) as above for each pair of consecutive dim N, N+1 => must be first dim M where changes (even if it's outermost). => All dimensions outside M must have single point => M must be within single dimension "run" => All dimensions inside M must tile => M behaves like all dimensions within it innermost dim must be moving ''' if finish == start: return Points() indices = np.arange(start, finish, np.sign(finish - start)) indices = np.where(indices < 0, indices + self.size, indices) if max(indices) >= self.size: raise IndexError("Requested points extend out of range") length = len(indices) points = Points() for dim in self.dimensions: point_repeat = int(self._dim_meta[dim]["repeat"]) point_indices = indices // point_repeat # Number of point this step is on found_m = np.any( point_indices != point_indices[0]) # For alternating case if found_m: points.extract(self._points_from_below_m(dim, point_indices)) else: points.extract( self._points_above_m(dim, point_indices[0], length)) points.duration = np.full(length, self.duration) points.delay_after = np.full(length, self.delay_after) for m in self.mutators: points = m.mutate(points, indices) return points
def test_simple_points(self): points = self.comp.get_points(7, 12) self.assertEqual( list([[0, 1, 2], [0, 1, 3], [0, 1, 4], [0, 2, 0], [0, 2, 1]]), points.indexes.tolist()) # Lower = Bounds for outer dimension self.assertEqual(list([0., 0., 0., 0., 0.]), points.lower['x'].tolist()) # Lower != bounds for innermost dimension self.assertEqual(list([1.25, 1.25, 1.25, 2.5, 2.5]), points.lower['y'].tolist()) self.assertEqual(list([1.875, 3.125, 4.375, -0.625, 0.625]), points.lower['z'].tolist()) self.assertTrue(np.all(np.full(5, 5) == points.duration)) self.assertTrue(np.all(np.full(5, 7) == points.delay_after))
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 = np.full(len(x), True, dtype=np.int8) mask &= dp >= 0 mask &= 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 test_apply_excluders_over_multiple_gens(self): gx_pos = np.array([1, 2, 3, 4, 5]) gy_pos = np.zeros(5) hx_pos = np.zeros(5) hy_pos = np.array([-1, -2, -3]) mask = np.full(15, 1, dtype=np.int8) e = Mock(axes=["gx", "hy"], create_mask=Mock(return_value=mask)) g = Mock(axes=["gx", "gy"], positions={ "gx": gx_pos, "gy": gy_pos }, size=len(gx_pos), alternate=False) h = Mock(axes=["hx", "hy"], positions={ "hx": hx_pos, "hy": hy_pos }, size=len(hy_pos), alternate=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(np.array([1, 2, 3, 4, 5]), 3) == e.create_mask.call_args[0][0]).all()) self.assertTrue((np.tile(np.array([-1, -2, -3]), 5) == e.create_mask.call_args[0][1]).all())
def mask_points(self, points): x = points[0] y = points[1] v1x, v1y = self.points_x[-1], self.points_y[-1] mask = np.full(len(x), False, dtype=np.int8) for v2x, v2y in zip(self.points_x, self.points_y): # skip horizontal edges if (v2y != v1y): vmask = np.full(len(x), False, dtype=np.int8) vmask |= ((y < v2y) & (y >= v1y)) vmask |= ((y < v1y) & (y >= v2y)) t = (y - v1y) / (v2y - v1y) vmask &= x < v1x + t * (v2x - v1x) mask ^= vmask v1x, v1y = v2x, v2y return mask
def prepare(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 """ if self._prepared: return mask = np.full(self._max_length, 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 # we have to assume the "returned" mask may be edited in place # so we have to store a copy self.mask = mask self.indices = self.mask.nonzero()[0] self.size = len(self.indices) self._prepared = True
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 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(axes=["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())
def mask_points(self, points): x = points[0].copy() y = points[1].copy() 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 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 = np.full(len(x), 1, dtype=np.int8) mask &= r2 <= self.radii[1] mask &= r2 >= self.radii[0] mask &= (phi_x <= phi_s) return mask
def prepare(self): self.num = 1 self.dimensions = [] # 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) 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) 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) \ - 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 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] 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"] \ 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 # 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"] 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) 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]: mask = excluder.create_mask(points_1, points_2) else: mask = excluder.create_mask(points_2, points_1) ##### # 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" ##### 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: raise ValueError("Regions would exclude entire scan") repeat *= len(dim["indicies"]) self.num = repeat for dim in self.dimensions: l = len(dim["indicies"]) repeat /= l dim["tile"] = tile dim["repeat"] = repeat tile *= l 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 d = {"tile": tile, "repeat": repeat} tile *= g.num self.generator_dim_scaling[g] = d
def prepare(self): """ Prepare data structures required for point generation and initialize size, shape, and dimensions attributes. 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) # 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 excluder_ in [e for e in excluders if isinstance(e, ROIExcluder)]: if len(excluder_.rois) == 1 \ and isinstance(excluder_.rois[0], RectangularROI) \ and excluder_.rois[0].angle == 0: rect = excluder_.rois[0] axis_1, axis_2 = excluder_.axes[0], excluder_.axes[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.prepare_positions() gen_2.prepare_positions() # Filter by axis 1 valid = np.full(gen_1.size, True, dtype=np.int8) valid &= \ gen_1.positions[axis_1] <= rect.width + rect.start[0] valid &= \ gen_1.positions[axis_1] >= rect.start[0] points_1 = gen_1.positions[axis_1][valid.astype(np.bool)] # Filter by axis 2 valid = np.full(gen_2.size, True, dtype=np.int8) valid &= \ gen_2.positions[axis_2] <= rect.height + rect.start[1] valid &= gen_2.positions[axis_2] >= rect.start[1] points_2 = gen_2.positions[axis_2][valid.astype(np.bool)] # Recreate generators to replace larger generators + ROI new_gen1 = LineGenerator(gen_1.axes, gen_1.units, points_1[0], points_1[-1], len(points_1), gen_1.alternate) new_gen2 = LineGenerator(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 # Remove Excluder as it is now empty excluders.remove(excluder_) for generator in generators: generator.prepare_positions() self.dimensions.append(Dimension(generator)) # only the inner-most generator needs to have bounds calculated if self.continuous: generators[-1].prepare_bounds() for excluder in excluders: matched_dims = [ d for d in self.dimensions if len(set(d.axes) & set(excluder.axes)) != 0 ] if len(matched_dims) == 0: raise ValueError( "Excluder references axes that have not been provided by generators: %s" % str(excluder.axes)) d_start = self.dimensions.index(matched_dims[0]) d_end = self.dimensions.index(matched_dims[-1]) if d_start != d_end: # merge all excluders between d_start and d_end (inclusive) alternate = self.dimensions[d_end].alternate # verify consistent alternate settings (ignoring outermost dimesion where it doesn't matter) for d in self.dimensions[max(1, d_start):d_end]: # filter out dimensions consisting of a single NullPointGenerator, since alternation means nothing if len(d.generators) == 1 and isinstance( d.generators[0], NullPointGenerator): continue if alternate != d.alternate: raise ValueError( "Nested generators connected by regions must have the same alternate setting" ) merged_dim = Dimension.merge_dimensions( self.dimensions[d_start:d_end + 1]) self.dimensions = self.dimensions[:d_start] + [ merged_dim ] + self.dimensions[d_end + 1:] dim = merged_dim else: dim = self.dimensions[d_start] dim.apply_excluder(excluder) self.size = 1 for dim in self.dimensions: self._dim_meta[dim] = {} dim.prepare() if dim.size == 0: raise ValueError("Regions would exclude entire scan") self.size *= dim.size self.shape = tuple(dim.size for dim in self.dimensions) repeat = self.size tile = 1 for dim in self.dimensions: repeat /= dim.size self._dim_meta[dim]["tile"] = tile self._dim_meta[dim]["repeat"] = repeat tile *= dim.size for dim in self.dimensions: tile = 1 repeat = dim._max_length for g in dim.generators: repeat /= g.size d = {"tile": tile, "repeat": repeat} tile *= g.size self._generator_dim_scaling[g] = d self._prepared = True
def prepare(self): """ Prepare data structures required for point generation and initialize size, shape, and dimensions attributes. 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) # 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 excluder_ in [e for e in excluders if isinstance(e, ROIExcluder)]: if len(excluder_.rois) == 1 \ and isinstance(excluder_.rois[0], RectangularROI) \ and excluder_.rois[0].angle == 0: rect = excluder_.rois[0] axis_1, axis_2 = excluder_.axes[0], excluder_.axes[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.prepare_positions() gen_2.prepare_positions() # Filter by axis 1 valid = np.full(gen_1.size, True, dtype=np.int8) valid &= \ gen_1.positions[axis_1] <= rect.width + rect.start[0] valid &= \ gen_1.positions[axis_1] >= rect.start[0] points_1 = gen_1.positions[axis_1][valid.astype(np.bool)] # Filter by axis 2 valid = np.full(gen_2.size, True, dtype=np.int8) valid &= \ gen_2.positions[axis_2] <= rect.height + rect.start[1] valid &= gen_2.positions[axis_2] >= rect.start[1] points_2 = gen_2.positions[axis_2][valid.astype(np.bool)] # Recreate generators to replace larger generators + ROI new_gen1 = LineGenerator(gen_1.axes, gen_1.units, points_1[0], points_1[-1], len(points_1), gen_1.alternate) new_gen2 = LineGenerator(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 # Remove Excluder as it is now empty excluders.remove(excluder_) for generator in generators: 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.axes 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 " \ "generator order") # 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: 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 setting") # merge "inner" into "outer" if dim_diff == -1: # dim_1 is "outer" - preserves axis ordering new_dim = Dimension.merge_dimensions(dim_1, dim_2) self.dimensions[self.dimensions.index(dim_1)] = new_dim self.dimensions.remove(dim_2) dim = new_dim else: dim = dim_1 dim.apply_excluder(excluder) self.size = 1 for dim in self.dimensions: self._dim_meta[dim] = {} dim.prepare() if dim.size == 0: raise ValueError("Regions would exclude entire scan") self.size *= dim.size self.shape = tuple(dim.size for dim in self.dimensions) repeat = self.size tile = 1 for dim in self.dimensions: repeat /= dim.size self._dim_meta[dim]["tile"] = tile self._dim_meta[dim]["repeat"] = repeat tile *= dim.size for dim in self.dimensions: tile = 1 repeat = dim._max_length for g in dim.generators: repeat /= g.size d = {"tile": tile, "repeat": repeat} tile *= g.size self._generator_dim_scaling[g] = d self._prepared = True
def prepare(self): """ Prepare data structures required to determine size and filtered positions of the dimension. Must be called before get_positions or get_mesh_map are called. """ axis_positions = {} axis_bounds_lower = {} axis_bounds_upper = {} masks = [] # scale up all position arrays # inner generators are tiled by the size of out generators # outer generators have positions repeated by the size of inner generators repeats, tilings, dim_size = 1, 1, 1 for g in self.generators: repeats *= g.size dim_size *= g.size for gen in self.generators: repeats /= gen.size for axis in gen.axes: positions = gen.positions[axis] if gen.alternate: positions = np.append(positions, positions[::-1]) positions = np.repeat(positions, repeats) p = np.tile(positions, (tilings // 2)) if tilings % 2 != 0: positions = np.append( p, positions[:int(len(positions) // 2)]) else: positions = p else: positions = np.repeat(positions, repeats) positions = np.tile(positions, tilings) axis_positions[axis] = positions tilings *= gen.size # produce excluder masks for excl in self.excluders: arrays = [axis_positions[axis] for axis in excl.axes] excluder_mask = excl.create_mask(*arrays) masks.append(excluder_mask) # AND all masks together (empty mask is all values selected) mask = masks[0] if len(masks) else np.full( dim_size, True, dtype=np.int8) for m in masks[1:]: mask &= m gen = self.generators[-1] if getattr(gen, "bounds", None): tilings = np.prod(np.array([g.size for g in self.generators[:-1]])) if gen.alternate: tilings /= 2. for axis in gen.axes: upper_base = gen.bounds[axis][1:] lower_base = gen.bounds[axis][:-1] upper, lower = upper_base, lower_base if gen.alternate: upper = np.append(upper_base, lower_base[::-1]) lower = np.append(lower_base, upper_base[::-1]) upper = np.tile(upper, int(tilings)) lower = np.tile(lower, int(tilings)) if tilings % 1 != 0: upper = np.append(upper, upper_base) lower = np.append(lower, lower_base) axis_bounds_upper[axis] = upper axis_bounds_lower[axis] = lower self.mask = mask self.indices = self.mask.nonzero()[0] self.size = len(self.indices) self.positions = { axis: axis_positions[axis][self.indices] for axis in axis_positions } self.upper_bounds = { axis: self.positions[axis] for axis in self.positions } self.lower_bounds = { axis: self.positions[axis] for axis in self.positions } for axis in axis_bounds_lower: self.upper_bounds[axis] = axis_bounds_upper[axis][self.indices] self.lower_bounds[axis] = axis_bounds_lower[axis][self.indices] self._prepared = True
def prepare(self): self.num = 1 self.dimensions = [] # 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) 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) 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) \ - 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 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] 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"] \ 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 # 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"] 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) 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]: mask = excluder.create_mask(points_1, points_2) else: mask = excluder.create_mask(points_2, points_1) ##### # 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" ##### 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: raise ValueError("Regions would exclude entire scan") repeat *= len(dim["indicies"]) self.num = repeat for dim in self.dimensions: l = len(dim["indicies"]) repeat /= l dim["tile"] = tile dim["repeat"] = repeat tile *= l 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 d = {"tile":tile, "repeat":repeat} tile *= g.num self.generator_dim_scaling[g] = d