def subtract_and_intersect_circle(self, center, radius): '''Will throw a VennRegionException if the circle to be subtracted is completely inside and not touching the given region.''' # Check whether the target circle intersects us center = np.asarray(center, float) d = np.linalg.norm(center - self.center) if d > (radius + self.radius - tol): return [self, VennEmptyRegion()] # The circle does not intersect us elif d < tol: if radius > self.radius - tol: # We are completely covered by that circle or we are the same circle return [VennEmptyRegion(), self] else: # That other circle is inside us and smaller than us - we can't deal with it raise VennRegionException( "Invalid configuration of circular regions (holes are not supported)." ) else: # We *must* intersect the other circle. If it is not the case, then it is inside us completely, # and we'll complain. intersections = circle_circle_intersection(self.center, self.radius, center, radius) if intersections is None: raise VennRegionException( "Invalid configuration of circular regions (holes are not supported)." ) elif np.all(abs(intersections[0] - intersections[1]) < tol) and self.radius < radius: # There is a single intersection point (i.e. we are touching the circle), # the circle to be subtracted is not outside of us (this was checked before), and is larger than us. # This is a particular corner case that is not dealt with correctly by the general-purpose code below and must # be handled separately return [VennEmptyRegion(), self] else: # Otherwise the subtracted region is a 2-arc-gon # Before we need to convert the intersection points as angles wrt each circle. a_1 = vector_angle_in_degrees(intersections[0] - self.center) a_2 = vector_angle_in_degrees(intersections[1] - self.center) b_1 = vector_angle_in_degrees(intersections[0] - center) b_2 = vector_angle_in_degrees(intersections[1] - center) # We must take care of the situation where the intersection points happen to be the same if (abs(b_1 - b_2) < tol): b_1 = b_2 - tol / 2 if (abs(a_1 - a_2) < tol): a_2 = a_1 + tol / 2 # The subtraction is a 2-arc-gon [(AB, B-), (BA, A+)] s_arc1 = Arc(center, radius, b_1, b_2, False) s_arc2 = Arc(self.center, self.radius, a_2, a_1, True) subtraction = VennArcgonRegion([s_arc1, s_arc2]) # .. and the intersection is a 2-arc-gon [(AB, A+), (BA, B+)] i_arc1 = Arc(self.center, self.radius, a_1, a_2, True) i_arc2 = Arc(center, radius, b_2, b_1, True) intersection = VennArcgonRegion([i_arc1, i_arc2]) return [subtraction, intersection]
def subtract_and_intersect_circle(self, center, radius): '''Will throw a VennRegionException if the circle to be subtracted is completely inside and not touching the given region.''' # Check whether the target circle intersects us center = np.asarray(center, float) d = np.linalg.norm(center - self.center) if d > (radius + self.radius - tol): return [self, VennEmptyRegion()] # The circle does not intersect us elif d < tol: if radius > self.radius - tol: # We are completely covered by that circle or we are the same circle return [VennEmptyRegion(), self] else: # That other circle is inside us and smaller than us - we can't deal with it raise VennRegionException("Invalid configuration of circular regions (holes are not supported).") else: # We *must* intersect the other circle. If it is not the case, then it is inside us completely, # and we'll complain. intersections = circle_circle_intersection(self.center, self.radius, center, radius) if intersections is None: raise VennRegionException("Invalid configuration of circular regions (holes are not supported).") elif np.all(abs(intersections[0] - intersections[1]) < tol) and self.radius < radius: # There is a single intersection point (i.e. we are touching the circle), # the circle to be subtracted is not outside of us (this was checked before), and is larger than us. # This is a particular corner case that is not dealt with correctly by the general-purpose code below and must # be handled separately return [VennEmptyRegion(), self] else: # Otherwise the subtracted region is a 2-arc-gon # Before we need to convert the intersection points as angles wrt each circle. a_1 = vector_angle_in_degrees(intersections[0] - self.center) a_2 = vector_angle_in_degrees(intersections[1] - self.center) b_1 = vector_angle_in_degrees(intersections[0] - center) b_2 = vector_angle_in_degrees(intersections[1] - center) # We must take care of the situation where the intersection points happen to be the same if (abs(b_1 - b_2) < tol): b_1 = b_2 - tol/2 if (abs(a_1 - a_2) < tol): a_2 = a_1 + tol/2 # The subtraction is a 2-arc-gon [(AB, B-), (BA, A+)] s_arc1 = Arc(center, radius, b_1, b_2, False) s_arc2 = Arc(self.center, self.radius, a_2, a_1, True) subtraction = VennArcgonRegion([s_arc1, s_arc2]) # .. and the intersection is a 2-arc-gon [(AB, A+), (BA, B+)] i_arc1 = Arc(self.center, self.radius, a_1, a_2, True) i_arc2 = Arc(center, radius, b_2, b_1, True) intersection = VennArcgonRegion([i_arc1, i_arc2]) return [subtraction, intersection]
def point_as_angle(self, pt): ''' Given a point located on the arc's circle, return the corresponding angle in degrees. No check is done that the point lies on the circle (this is essentially a convenience wrapper around _math.vector_angle_in_degrees) >>> a = Arc((0, 0), 1, 0, 0, True) >>> a.point_as_angle((1, 0)) 0.0 >>> a.point_as_angle((1, 1)) 45.0 >>> a.point_as_angle((0, 1)) 90.0 >>> a.point_as_angle((-1, 1)) 135.0 >>> a.point_as_angle((-1, 0)) 180.0 >>> a.point_as_angle((-1, -1)) -135.0 >>> a.point_as_angle((0, -1)) -90.0 >>> a.point_as_angle((1, -1)) -45.0 ''' return vector_angle_in_degrees(np.asarray(pt) - self.center)
def point_as_angle(self, pt): ''' Given a point located on the arc's circle, return the corresponding angle in degrees. No check is done that the point lies on the circle (this is essentially a convenience wrapper around _math.vector_angle_in_degrees) >>> a = Arc((0, 0), 1, 0, 0, True) >>> a.point_as_angle((1, 0)) 0.0 >>> a.point_as_angle((1, 1)) 45.0 >>> a.point_as_angle((0, 1)) 90.0 >>> a.point_as_angle((-1, 1)) 135.0 >>> a.point_as_angle((-1, 0)) 180.0 >>> a.point_as_angle((-1, -1)) -135.0 >>> a.point_as_angle((0, -1)) -90.0 >>> a.point_as_angle((1, -1)) -45.0 ''' return vector_angle_in_degrees(np.asarray(pt) - self.center)
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]