def test_range_rois_preserved(self): data = self.add_data_and_attributes() assert self.client.xatt is not self.client.yatt roi = XRangeROI() roi.set_range(1, 2) self.client.apply_roi(roi) assert isinstance(data.edit_subset.subset_state, RangeSubsetState) assert data.edit_subset.subset_state.att == self.client.xatt roi = RectangularROI() roi = YRangeROI() roi.set_range(1, 2) self.client.apply_roi(roi) assert data.edit_subset.subset_state.att == self.client.yatt
def test_yregion_roi(self): subset_state = RoiSubsetState(self.data.pixel_component_ids[1], self.data.pixel_component_ids[0], YRangeROI(10, 22.2)) self.dc.new_subset_group(subset_state=subset_state, label='xrangeroi') reg = self.data.get_selection_definition(format='astropy-regions') assert isinstance(reg, RectanglePixelRegion) assert_allclose(reg.center.x, 128) assert_allclose(reg.center.y, 16.1) assert_allclose(reg.width, 256) assert_allclose(reg.height, 12.2)
def subset_from_roi(self, att, roi, other_comp=None, other_att=None, coord='x', is_nested=False): """ Create a SubsetState object from an ROI. This encapsulates the logic for creating subset states with CategoricalComponents. There is an important caveat, only RangeROIs and RectangularROIs make sense in mixed type plots. As such, polygons are converted to their outer-most edges in this case. :param att: attribute name of this Component :param roi: an ROI object :param other_comp: The other Component for 2D ROIs :param other_att: The attribute name of the other Component :param coord: The orientation of this Component :param is_nested: True if this was passed from another Component. :return: A SubsetState (or subclass) object """ if coord not in ('x', 'y'): raise ValueError('coord should be one of x/y') if isinstance(roi, RangeROI): # The selection is either an x range or a y range if roi.ori == coord: # The selection applies to the current component return CategoricalROISubsetState.from_range( self, att, roi.min, roi.max) else: # The selection applies to the other component, so we delegate other_coord = 'y' if coord == 'x' else 'x' return other_comp.subset_from_roi(other_att, roi, other_comp=self, other_att=att, coord=other_coord) elif isinstance(roi, RectangularROI): # In this specific case, we can decompose the rectangular # ROI into two RangeROIs that are combined with an 'and' # logical operation. other_coord = 'y' if coord == 'x' else 'x' if coord == 'x': range1 = XRangeROI(roi.xmin, roi.xmax) range2 = YRangeROI(roi.ymin, roi.ymax) else: range2 = XRangeROI(roi.xmin, roi.xmax) range1 = YRangeROI(roi.ymin, roi.ymax) # We get the subset state for the current component subset1 = self.subset_from_roi(att, range1, other_comp=other_comp, other_att=other_att, coord=coord) # We now get the subset state for the other component subset2 = other_comp.subset_from_roi(other_att, range2, other_comp=self, other_att=att, coord=other_coord) return AndState(subset1, subset2) elif isinstance(roi, CategoricalROI): # The selection is categorical itself return CategoricalROISubsetState(roi=roi, att=att) else: # The selection is polygon-like, which requires special care. if isinstance(other_comp, CategoricalComponent): # For each category, we check which categories along the other # axis fall inside the polygon: selection = {} for code, label in enumerate(self.categories): # Determine the coordinates of the points to check n_other = len(other_comp.categories) y = np.arange(n_other) x = np.repeat(code, n_other) if coord == 'y': x, y = y, x # Determine which points are in the polygon, and which # categories these correspond to in_poly = roi.contains(x, y) categories = other_comp.categories[in_poly] if len(categories) > 0: selection[label] = set(categories) return CategoricalROISubsetState2D(selection, att, other_att) else: # If the other component is not categorical, we treat this as if # each categorical component was mapped to a numerical value, # and at each value, we keep track of the polygon intersection # with the component. This will result in zero, one, or # multiple separate numerical ranges for each categorical value. # TODO: if we ever allow the category order to be changed, we # need to figure out how to update this! x, y = roi.to_polygon() if is_nested: x, y = y, x # We loop over each category and for each one we find the # numerical ranges selection = {} for code, label in enumerate(self.categories): # We determine all the numerical segments that represent the # ensemble of points in y that fall in the polygon # TODO: profile the following function segments = polygon_line_intersections(x, y, xval=code) if len(segments) > 0: selection[label] = segments return CategoricalMultiRangeSubsetState( selection, att, other_att)
def roi_to_subset_state(roi, x_att=None, y_att=None, x_comp=None, y_comp=None): """ Given a 2D ROI and attributes on the x and y axis, determine the corresponding subset state. """ if isinstance(roi, RangeROI): if roi.ori == 'x': att = x_att comp = x_comp else: att = y_att comp = y_comp if comp.categorical: return CategoricalROISubsetState.from_range(comp, att, roi.min, roi.max) else: return RangeSubsetState(roi.min, roi.max, att) elif x_comp.categorical or y_comp.categorical: if isinstance(roi, RectangularROI): # In this specific case, we can decompose the rectangular ROI into # two RangeROIs that are combined with an 'and' logical operation. range1 = XRangeROI(roi.xmin, roi.xmax) range2 = YRangeROI(roi.ymin, roi.ymax) subset1 = roi_to_subset_state(range1, x_att=x_att, x_comp=x_comp) subset2 = roi_to_subset_state(range2, y_att=y_att, y_comp=y_comp) return AndState(subset1, subset2) elif isinstance(roi, CategoricalROI): # The selection is categorical itself. We assume this is along the x axis return CategoricalROISubsetState(roi=roi, att=x_att) else: # The selection is polygon-like, which requires special care. if x_comp.categorical and y_comp.categorical: # For each category, we check which categories along the other # axis fall inside the polygon: selection = {} for code, label in enumerate(x_comp.categories): # Determine the coordinates of the points to check n_other = len(y_comp.categories) y = np.arange(n_other) x = np.repeat(code, n_other) # Determine which points are in the polygon, and which # categories these correspond to in_poly = roi.contains(x, y) categories = y_comp.categories[in_poly] if len(categories) > 0: selection[label] = set(categories) return CategoricalROISubsetState2D(selection, x_att, y_att) else: # If one of the components is not categorical, we treat this as # if each categorical component was mapped to a numerical value, # and at each value, we keep track of the polygon intersection # with the component. This will result in zero, one, or multiple # separate numerical ranges for each categorical value. # TODO: if we ever allow the category order to be changed, we # need to figure out how to update this! # We loop over each category and for each one we find the # numerical ranges selection = {} if x_comp.categorical: cat_comp = x_comp cat_att = x_att num_att = y_att x, y = roi.to_polygon() else: cat_comp = y_comp cat_att = y_att num_att = x_att y, x = roi.to_polygon() for code, label in enumerate(cat_comp.categories): # We determine all the numerical segments that represent the # ensemble of points in y that fall in the polygon # TODO: profile the following function segments = polygon_line_intersections(x, y, xval=code) if len(segments) > 0: selection[label] = segments return CategoricalMultiRangeSubsetState(selection, cat_att=cat_att, num_att=num_att) else: # The selection is polygon-like and components are numerical subset_state = RoiSubsetState() subset_state.xatt = x_att subset_state.yatt = y_att subset_state.roi = PolygonalROI(*roi.to_polygon()) return subset_state