def subtract_and_intersect_circle(self, center, radius): ''' Circle subtraction / intersection only supported by 2-gon regions, otherwise a VennRegionException is thrown. In addition, such an exception will be thrown if the circle to be subtracted is completely within the region and forms a "hole". The result may be either a VennArcgonRegion or a VennMultipieceRegion (the latter happens when the circle "splits" a crescent in two). ''' if len(self.arcs) != 2: raise VennRegionException( "Circle subtraction and intersection with poly-arc regions is currently only supported for 2-arc-gons." ) # In the following we consider the 2-arc-gon case. # Before we do anything, we check for a special case, where the circle of interest is one of the two circles forming the arcs. # In this case we can determine the answer quite easily. matching_arcs = [ a for a in self.arcs if a.lies_on_circle(center, radius) ] if len(matching_arcs) != 0: # If the circle matches a positive arc, the result is [empty, self], otherwise [self, empty] return [VennEmptyRegion(), self ] if matching_arcs[0].direction else [ self, VennEmptyRegion() ] # Consider the intersection points of the circle with the arcs. # If any of the intersection points corresponds exactly to any of the arc's endpoints, we will end up with # a lot of messy special cases (as if the usual situation is not messy enough, eh). # To avoid that, we cheat by slightly increasing the circle's radius until this is not the case any more. center = np.asarray(center) illegal_intersections = [a.start_point() for a in self.arcs] while True: valid = True intersections = [ a.intersect_circle(center, radius) for a in self.arcs ] for ints in intersections: for pt in ints: for illegal_pt in illegal_intersections: if np.all(abs(pt - illegal_pt) < tol): valid = False if valid: break else: radius += tol # There must be an even number of those points in total. # (If this is not the case, then we have an unfortunate case with weird numeric errors [TODO: find examples and deal with it?]). # There are three possibilities with the following subcases: # I. No intersection points # a) The polyarc is completely within the circle. # result = [ empty, self ] # b) The polyarc is completely outside the circle. # result = [ self, empty ] # II. Four intersection points, two for each arc. Points x1, x2 for arc X and y1, y2 for arc Y, ordered along the arc. # a) The polyarc endpoints are both outside the circle. # result_subtraction = a combination of two 3-arc polyarcs: # 1: {X - start to x1, # x1 to y2 along circle (negative direction)), # Y - y2 to end} # 2: {Y start to y1, # y1 to x2 along circle (negative direction)), # X - x2 to end} # b) The polyarc endpoints are both inside the circle # same as above, but the "along circle" arc directions are flipped and subtraction/intersection parts are exchanged # III. Two intersection points # a) One arc, X, has two intersection points i & j, another arc, Y, has no intersection points # a.1) Polyarc endpoints are outside the circle # result_subtraction = {X from start to i, circle i to j (direction = negative), X j to end, Y} # result_intersection = {X i to j, circle j to i (direction = positive} # a.2) Polyarc endpoints are inside the circle # result_subtraction = {X i to j, circle j to i negative} # result_intersection = {X 0 to i, circle i to j positive, X j to end, Y} # b) Both arcs, X and Y, have one intersection point each. In this case one of the arc endpoints must be inside circle, another outside. # call the arc that starts with the outside point X, the other arc Y. # result_subtraction = {X start to intersection, intersection to intersection along circle (negative direction), Y from intersection to end} # result_intersection = {X intersection to end, Y start to intersecton, intersection to intersecion along circle (positive)} center = np.asarray(center) intersections = [a.intersect_circle(center, radius) for a in self.arcs] if len(intersections[0]) == 0 and len(intersections[1]) == 0: # Case I if point_in_circle(self.arcs[0].start_point(), center, radius): # Case I.a) return [VennEmptyRegion(), self] else: # Case I.b) return [self, VennEmptyRegion()] elif len(intersections[0]) == 2 and len(intersections[1]) == 2: # Case II. a) or b) case_II_a = not point_in_circle(self.arcs[0].start_point(), center, radius) a1 = self.arcs[0].subarc_between_points(None, intersections[0][0]) a2 = Arc(center, radius, vector_angle_in_degrees(intersections[0][0] - center), vector_angle_in_degrees(intersections[1][1] - center), not case_II_a) a2.fix_360_to_0() a3 = self.arcs[1].subarc_between_points(intersections[1][1], None) piece1 = VennArcgonRegion([a1, a2, a3]) b1 = self.arcs[1].subarc_between_points(None, intersections[1][0]) b2 = Arc(center, radius, vector_angle_in_degrees(intersections[1][0] - center), vector_angle_in_degrees(intersections[0][1] - center), not case_II_a) b2.fix_360_to_0() b3 = self.arcs[0].subarc_between_points(intersections[0][1], None) piece2 = VennArcgonRegion([b1, b2, b3]) subtraction = VennMultipieceRegion([piece1, piece2]) c1 = self.arcs[0].subarc(a1.to_angle, b3.from_angle) c2 = b2.reversed() c3 = self.arcs[1].subarc(b1.to_angle, a3.from_angle) c4 = a2.reversed() intersection = VennArcgonRegion([c1, c2, c3, c4]) return [subtraction, intersection ] if case_II_a else [intersection, subtraction] else: # Case III. Yuck. if len(intersections[0]) == 0 or len(intersections[1]) == 0: # Case III.a) x = 0 if len(intersections[0]) != 0 else 1 y = 1 - x if len(intersections[x]) != 2: warnings.warn( "Numeric precision error during polyarc intersection, case IIIa. Expect wrong results." ) intersections[x] = [ intersections[x][0], intersections[x][0] ] # This way we'll at least produce some result, although it will probably be wrong if not point_in_circle(self.arcs[0].start_point(), center, radius): # Case III.a.1) # result_subtraction = {X from start to i, circle i to j (direction = negative), X j to end, Y} a1 = self.arcs[x].subarc_between_points( None, intersections[x][0]) a2 = Arc( center, radius, vector_angle_in_degrees(intersections[x][0] - center), vector_angle_in_degrees(intersections[x][1] - center), False) a3 = self.arcs[x].subarc_between_points( intersections[x][1], None) a4 = self.arcs[y] subtraction = VennArcgonRegion([a1, a2, a3, a4]) # result_intersection = {X i to j, circle j to i (direction = positive)} b1 = self.arcs[x].subarc(a1.to_angle, a3.from_angle) b2 = a2.reversed() intersection = VennArcgonRegion([b1, b2]) return [subtraction, intersection] else: # Case III.a.2) # result_subtraction = {X i to j, circle j to i negative} a1 = self.arcs[x].subarc_between_points( intersections[x][0], intersections[x][1]) a2 = Arc( center, radius, vector_angle_in_degrees(intersections[x][1] - center), vector_angle_in_degrees(intersections[x][0] - center), False) subtraction = VennArcgonRegion([a1, a2]) # result_intersection = {X 0 to i, circle i to j positive, X j to end, Y} b1 = self.arcs[x].subarc(None, a1.from_angle) b2 = a2.reversed() b3 = self.arcs[x].subarc(a1.to_angle, None) b4 = self.arcs[y] intersection = VennArcgonRegion([b1, b2, b3, b4]) return [subtraction, intersection] else: # Case III.b) if len(intersections[0]) == 2 or len(intersections[1]) == 2: warnings.warn( "Numeric precision error during polyarc intersection, case IIIb. Expect wrong results." ) # One of the arcs must start outside the circle, call it x x = 0 if not point_in_circle(self.arcs[0].start_point(), center, radius) else 1 y = 1 - x a1 = self.arcs[x].subarc_between_points( None, intersections[x][0]) a2 = Arc(center, radius, vector_angle_in_degrees(intersections[x][0] - center), vector_angle_in_degrees(intersections[y][0] - center), False) a3 = self.arcs[y].subarc_between_points( intersections[y][0], None) subtraction = VennArcgonRegion([a1, a2, a3]) b1 = self.arcs[x].subarc(a1.to_angle, None) b2 = self.arcs[y].subarc(None, a3.from_angle) b3 = a2.reversed() intersection = VennArcgonRegion([b1, b2, b3]) return [subtraction, intersection]
def subtract_and_intersect_circle(self, center, radius): ''' Circle subtraction / intersection only supported by 2-gon regions, otherwise a VennRegionException is thrown. In addition, such an exception will be thrown if the circle to be subtracted is completely within the region and forms a "hole". The result may be either a VennArcgonRegion or a VennMultipieceRegion (the latter happens when the circle "splits" a crescent in two). ''' if len(self.arcs) != 2: raise VennRegionException("Circle subtraction and intersection with poly-arc regions is currently only supported for 2-arc-gons.") # In the following we consider the 2-arc-gon case. # Before we do anything, we check for a special case, where the circle of interest is one of the two circles forming the arcs. # In this case we can determine the answer quite easily. matching_arcs = [a for a in self.arcs if a.lies_on_circle(center, radius)] if len(matching_arcs) != 0: # If the circle matches a positive arc, the result is [empty, self], otherwise [self, empty] return [VennEmptyRegion(), self] if matching_arcs[0].direction else [self, VennEmptyRegion()] # Consider the intersection points of the circle with the arcs. # If any of the intersection points corresponds exactly to any of the arc's endpoints, we will end up with # a lot of messy special cases (as if the usual situation is not messy enough, eh). # To avoid that, we cheat by slightly increasing the circle's radius until this is not the case any more. center = np.asarray(center) illegal_intersections = [a.start_point() for a in self.arcs] while True: valid = True intersections = [a.intersect_circle(center, radius) for a in self.arcs] for ints in intersections: for pt in ints: for illegal_pt in illegal_intersections: if np.all(abs(pt - illegal_pt) < tol): valid = False if valid: break else: radius += tol # There must be an even number of those points in total. # (If this is not the case, then we have an unfortunate case with weird numeric errors [TODO: find examples and deal with it?]). # There are three possibilities with the following subcases: # I. No intersection points # a) The polyarc is completely within the circle. # result = [ empty, self ] # b) The polyarc is completely outside the circle. # result = [ self, empty ] # II. Four intersection points, two for each arc. Points x1, x2 for arc X and y1, y2 for arc Y, ordered along the arc. # a) The polyarc endpoints are both outside the circle. # result_subtraction = a combination of two 3-arc polyarcs: # 1: {X - start to x1, # x1 to y2 along circle (negative direction)), # Y - y2 to end} # 2: {Y start to y1, # y1 to x2 along circle (negative direction)), # X - x2 to end} # b) The polyarc endpoints are both inside the circle # same as above, but the "along circle" arc directions are flipped and subtraction/intersection parts are exchanged # III. Two intersection points # a) One arc, X, has two intersection points i & j, another arc, Y, has no intersection points # a.1) Polyarc endpoints are outside the circle # result_subtraction = {X from start to i, circle i to j (direction = negative), X j to end, Y} # result_intersection = {X i to j, circle j to i (direction = positive} # a.2) Polyarc endpoints are inside the circle # result_subtraction = {X i to j, circle j to i negative} # result_intersection = {X 0 to i, circle i to j positive, X j to end, Y} # b) Both arcs, X and Y, have one intersection point each. In this case one of the arc endpoints must be inside circle, another outside. # call the arc that starts with the outside point X, the other arc Y. # result_subtraction = {X start to intersection, intersection to intersection along circle (negative direction), Y from intersection to end} # result_intersection = {X intersection to end, Y start to intersecton, intersection to intersecion along circle (positive)} center = np.asarray(center) intersections = [a.intersect_circle(center, radius) for a in self.arcs] if len(intersections[0]) == 0 and len(intersections[1]) == 0: # Case I if point_in_circle(self.arcs[0].start_point(), center, radius): # Case I.a) return [VennEmptyRegion(), self] else: # Case I.b) return [self, VennEmptyRegion()] elif len(intersections[0]) == 2 and len(intersections[1]) == 2: # Case II. a) or b) case_II_a = not point_in_circle(self.arcs[0].start_point(), center, radius) a1 = self.arcs[0].subarc_between_points(None, intersections[0][0]) a2 = Arc(center, radius, vector_angle_in_degrees(intersections[0][0] - center), vector_angle_in_degrees(intersections[1][1] - center), not case_II_a) a2.fix_360_to_0() a3 = self.arcs[1].subarc_between_points(intersections[1][1], None) piece1 = VennArcgonRegion([a1, a2, a3]) b1 = self.arcs[1].subarc_between_points(None, intersections[1][0]) b2 = Arc(center, radius, vector_angle_in_degrees(intersections[1][0] - center), vector_angle_in_degrees(intersections[0][1] - center), not case_II_a) b2.fix_360_to_0() b3 = self.arcs[0].subarc_between_points(intersections[0][1], None) piece2 = VennArcgonRegion([b1, b2, b3]) subtraction = VennMultipieceRegion([piece1, piece2]) c1 = self.arcs[0].subarc(a1.to_angle, b3.from_angle) c2 = b2.reversed() c3 = self.arcs[1].subarc(b1.to_angle, a3.from_angle) c4 = a2.reversed() intersection = VennArcgonRegion([c1, c2, c3, c4]) return [subtraction, intersection] if case_II_a else [intersection, subtraction] else: # Case III. Yuck. if len(intersections[0]) == 0 or len(intersections[1]) == 0: # Case III.a) x = 0 if len(intersections[0]) != 0 else 1 y = 1 - x if len(intersections[x]) != 2: warnings.warn("Numeric precision error during polyarc intersection, case IIIa. Expect wrong results.") intersections[x] = [intersections[x][0], intersections[x][0]] # This way we'll at least produce some result, although it will probably be wrong if not point_in_circle(self.arcs[0].start_point(), center, radius): # Case III.a.1) # result_subtraction = {X from start to i, circle i to j (direction = negative), X j to end, Y} a1 = self.arcs[x].subarc_between_points(None, intersections[x][0]) a2 = Arc(center, radius, vector_angle_in_degrees(intersections[x][0] - center), vector_angle_in_degrees(intersections[x][1] - center), False) a3 = self.arcs[x].subarc_between_points(intersections[x][1], None) a4 = self.arcs[y] subtraction = VennArcgonRegion([a1, a2, a3, a4]) # result_intersection = {X i to j, circle j to i (direction = positive)} b1 = self.arcs[x].subarc(a1.to_angle, a3.from_angle) b2 = a2.reversed() intersection = VennArcgonRegion([b1, b2]) return [subtraction, intersection] else: # Case III.a.2) # result_subtraction = {X i to j, circle j to i negative} a1 = self.arcs[x].subarc_between_points(intersections[x][0], intersections[x][1]) a2 = Arc(center, radius, vector_angle_in_degrees(intersections[x][1] - center), vector_angle_in_degrees(intersections[x][0] - center), False) subtraction = VennArcgonRegion([a1, a2]) # result_intersection = {X 0 to i, circle i to j positive, X j to end, Y} b1 = self.arcs[x].subarc(None, a1.from_angle) b2 = a2.reversed() b3 = self.arcs[x].subarc(a1.to_angle, None) b4 = self.arcs[y] intersection = VennArcgonRegion([b1, b2, b3, b4]) return [subtraction, intersection] else: # Case III.b) if len(intersections[0]) == 2 or len(intersections[1]) == 2: warnings.warn("Numeric precision error during polyarc intersection, case IIIb. Expect wrong results.") # One of the arcs must start outside the circle, call it x x = 0 if not point_in_circle(self.arcs[0].start_point(), center, radius) else 1 y = 1 - x a1 = self.arcs[x].subarc_between_points(None, intersections[x][0]) a2 = Arc(center, radius, vector_angle_in_degrees(intersections[x][0] - center), vector_angle_in_degrees(intersections[y][0] - center), False) a3 = self.arcs[y].subarc_between_points(intersections[y][0], None) subtraction = VennArcgonRegion([a1, a2, a3]) b1 = self.arcs[x].subarc(a1.to_angle, None) b2 = self.arcs[y].subarc(None, a3.from_angle) b3 = a2.reversed() intersection = VennArcgonRegion([b1, b2, b3]) return [subtraction, intersection]