def test_excluder_spread_axes(self): sp = SpiralGenerator(["s1", "s2"], ["mm", "mm"], centre=[0, 0], radius=1, scale=0.5, alternate=True) y = LineGenerator("y", "mm", 0, 1, 3, True) z = LineGenerator("z", "mm", -2, 3, 6, True) e = ROIExcluder([CircularROI([0., 0.], 1.0)], ["s1", "z"]) g = CompoundGenerator([z, y, sp], [e], []) g.prepare() s1_pos, s2_pos = sp.positions["s1"], sp.positions["s2"] s1_pos = np.tile(np.append(s1_pos, s1_pos[::-1]), 9) s2_pos = np.tile(np.append(s2_pos, s2_pos[::-1]), 9) y_pos = np.tile(np.repeat(np.array([0, 0.5, 1.0, 1.0, 0.5, 0]), sp.size), 3) z_pos = np.repeat(np.array([-2, -1, 0, 1, 2, 3]), sp.size * 3) mask_func = lambda ps1, pz: ps1**2 + pz**2 <= 1 mask = mask_func(s1_pos, z_pos) expected_s1 = s1_pos[mask] expected_s2 = s2_pos[mask] expected_y = y_pos[mask] expected_z = z_pos[mask] expected_positions = [{'s1':ps1, 's2':ps2, 'y':py, 'z':pz} for (ps1, ps2, py, pz) in zip(expected_s1, expected_s2, expected_y, expected_z)] positions = [point.positions for point in list(g.iterator())] self.assertEqual(positions, expected_positions)
def test_mixed_alternating_generators(self): x_pos = np.array([0, 1, 2]) x_bounds = np.array([-0.5, 0.5, 1.5, 2.5]) y_pos = np.array([10, 11, 12]) z_pos = np.array([20, 21, 22]) w_pos = np.array([30, 31, 32]) gx = Mock(axes=["x"], positions={"x": x_pos}, bounds={"x": x_bounds}, size=3, alternate=True) gy = Mock(axes=["y"], positions={"y": y_pos}, size=3, alternate=True) gz = Mock(axes=["z"], positions={"z": z_pos}, size=3, alternate=False) gw = Mock(axes=["w"], positions={"w": w_pos}, size=3, alternate=False) mask = np.array([1, 1, 0, 1, 1, 1, 1, 0, 1] * 9) indices = np.nonzero(mask)[0] e = Mock(axes=["x", "y", "z", "w"]) e.create_mask.return_value = mask d = Dimension([gw, gz, gy, gx], [e]) d.prepare() expected_x = np.append(np.tile(np.append(x_pos, x_pos[::-1]), 13), x_pos)[indices] expected_y = np.repeat( np.append(np.tile(np.append(y_pos, y_pos[::-1]), 4), y_pos), 3)[indices] expected_z = np.tile(np.repeat(z_pos, 9), 3)[indices] expected_w = np.repeat(w_pos, 27)[indices] self.assertEqual(False, d.alternate) self.assertEqual(expected_x.tolist(), d.get_positions("x").tolist()) self.assertEqual(expected_y.tolist(), d.get_positions("y").tolist()) self.assertEqual(expected_z.tolist(), d.get_positions("z").tolist()) self.assertEqual(expected_w.tolist(), d.get_positions("w").tolist())
def get_mesh_map(self, axis): """ Retrieve the mesh map (indices) for a given axis within the dimension. Args: axis (str): axis to get positions for Returns: Positions (np.array): Array of mesh indices """ # the points for this axis must be scaled and then indexed if not self._prepared: raise ValueError("Must call prepare first") # scale up points for axis gen = [g for g in self.generators if axis in g.axes][0] points = gen.positions[axis] # just get index of points instead of actual point value points = np.arange(len(points)) if gen.alternate: points = np.append(points, points[::-1]) tile = 0.5 if self.alternate else 1 repeat = 1 for g in self.generators[:self.generators.index(gen)]: tile *= g.size for g in self.generators[self.generators.index(gen) + 1:]: repeat *= g.size points = np.repeat(points, repeat) if tile % 1 != 0: p = np.tile(points, int(tile)) points = np.append(p, points[:int(len(points) // 2)]) else: points = np.tile(points, int(tile)) return points[self.indices]
def test_apply_excluders_over_single_alternating(self): x_pos = np.array([1, 2, 3, 4, 5]) y_pos = np.array([10, 11, 12, 13, 14, 15]) g = Mock(axes=["x", "y"], positions={"x":x_pos, "y":y_pos}) g.alternate = True mask = np.array([1, 1, 0, 1, 0, 0], dtype=np.int8) e = Mock(axes=["x", "y"], create_mask=Mock(return_value=mask)) d = Dimension(g) d.apply_excluder(e) d._masks[0]["mask"] = d._masks[0]["mask"].tolist() self.assertEqual([{"repeat":1, "tile":0.5, "mask":mask.tolist()}], d._masks) self.assertTrue((np.append(x_pos, x_pos[::-1]) == e.create_mask.call_args[0][0]).all()) self.assertTrue((np.append(y_pos, y_pos[::-1]) == e.create_mask.call_args[0][1]).all())
def test_prepare(self): d = Dimension( Mock(axes=["x", "y"], positions={ "x": np.array([0]), "y": np.array([0]) }, size=30)) m1 = np.array([0, 1, 0, 1, 1, 0], dtype=np.int8) m2 = np.array([1, 1, 0, 0, 1], dtype=np.int8) d._masks = [{ "repeat": 2, "tile": 2.5, "mask": m1 }, { "repeat": 2, "tile": 3, "mask": m2 }] e1 = np.array([0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0], dtype=np.int8) e1 = np.append(np.tile(e1, 2), e1[:len(e1) // 2]) e2 = np.array([1, 1, 1, 1, 0, 0, 0, 0, 1, 1], dtype=np.int8) e2 = np.tile(e2, 3) expected = e1 & e2 d.prepare() self.assertEqual(expected.tolist(), d.mask.tolist())
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 apply_excluder(self, excluder): """Apply an excluder with axes matching some axes in the dimension to produce an internal mask""" if self._prepared: raise ValueError("Can not apply excluders after" "prepare has been called") axis_inner = excluder.axes[0] axis_outer = excluder.axes[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) if axis_inner == excluder.axes[0]: excluder_mask = excluder.create_mask(points_x, points_y) else: excluder_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":excluder_mask} self._masks.append(m)
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 prepare_arrays(self, index_array): # The ConcatGenerator gets its positions from its sub-generators merged_arrays = {} for axis in self.axes: merged_arrays[axis] = np.array first = True if index_array.size == self.size + 1: # getting bounds preparing_bounds = True else: # getting positions preparing_bounds = False for generator in self.generators: if preparing_bounds: # getting bounds arr = generator.prepare_arrays(index_array[:generator.size + 1]) else: # getting positions arr = generator.prepare_arrays(index_array[:generator.size]) for axis in self.axes: axis_array = arr[axis] if first: merged_arrays[axis] = axis_array else: # This avoids appending an ndarray to a list cur_array = merged_arrays[axis] if preparing_bounds: assert np.abs(cur_array[-1] - axis_array[0]) < self.DIFF_LIMIT, \ "Merged generator bounds don't meet" \ " for axis %s (%f, %f)" \ % (str(axis), cur_array[-1], axis_array[0]) cur_array = np.append(cur_array[:-1], axis_array) else: cur_array = np.append(cur_array, axis_array) merged_arrays[axis] = cur_array first = False return merged_arrays
def __add__(self, other): """ input: other (Point or Points) Appends the positions, bounds, indices, duration, delay of another Points or Point to self Assumes that dimensions are shared, or that either self or other have no positions. returns: self """ if not len(self): if isinstance(other, Points): return other.__copy__() return self.wrap(other) if len(other): self.positions.update({axis: np.append(self.positions[axis], other.positions[axis]) for axis in other.positions}) self.lower.update({axis: np.append(self.lower[axis], other.lower[axis]) for axis in other.lower}) self.upper.update({axis: np.append(self.upper[axis], other.upper[axis]) for axis in other.upper}) if isinstance(other, Point): self.indexes = np.vstack((self.indexes, other.indexes)) elif len(other): self.indexes = np.concatenate((self.indexes, other.indexes), axis=0) self.duration = np.append(self.duration, other.duration) self.delay_after = np.append(self.delay_after, other.delay_after) return self
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 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 apply_excluder(self, excluder): """Apply an excluder with axes matching some axes in the dimension to produce an internal mask""" if self._prepared: raise ValueError("Can not apply excluders after" "prepare has been called") # find generators referenced by excluder matched_gens = [ g for g in self.generators if len(set(g.axes) & set(excluder.axes)) != 0 ] if len(matched_gens) == 0: raise ValueError( "Excluder references axes not present in dimension : %s" % str(excluder.axes)) g_start = self.generators.index(matched_gens[0]) g_end = self.generators.index(matched_gens[-1]) point_arrays = { axis: [g for g in matched_gens if axis in g.axes][0].positions[axis] for axis in excluder.axes } if self.alternate: for axis in point_arrays.keys(): arr = point_arrays[axis] point_arrays[axis] = np.append(arr, arr[::-1]) # scale up all point arrays using generators within the range # inner generators are tiled by the size of outer generators # outer generators have points repeated by the size of inner ones axes_tiling = {axis: 1 for axis in excluder.axes} axes_repeats = {axis: 1 for axis in excluder.axes} axes_seen = [] axes_to_see = [axis for axis in excluder.axes] for g in self.generators[g_start:g_end + 1]: found_axes = [axis for axis in g.axes if axis in excluder.axes] axes_to_see = [ axis for axis in axes_to_see if axis not in found_axes ] for axis in axes_to_see: axes_tiling[axis] *= g.size for axis in axes_seen: axes_repeats[axis] *= g.size axes_seen.extend(found_axes) for axis in point_arrays.keys(): arr = point_arrays[axis] point_arrays[axis] = np.tile(np.repeat(arr, axes_repeats[axis]), axes_tiling[axis]) arrays = [point_arrays[axis] for axis in excluder.axes] excluder_mask = excluder.create_mask(*arrays) # record the tiling/repeat information for generators outside the axis range tile = 0.5 if self.alternate else 1 repeat = 1 for g in self.generators[0:g_start]: tile *= g.size for g in self.generators[g_end + 1:]: repeat *= g.size m = {"repeat": repeat, "tile": tile, "mask": excluder_mask} self._masks.append(m)
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