Given two angle intervals, what is their intersection? An example of this problem is shown in the figure below.
Before we get to the solution, we need to make the problem more precise. But even before that, let’s start with a simpler problem whose solution will be a subroutine: computing the intersection of two real intervals on the number line. (I passed a milestone birthday recently, and I’m trying to lean into my age by using words like subroutine.)
Suppose that we have two such real intervals, each identified by their lower and upper bounds, and . The following Python code computes their intersection as another interval:
def interval_intersect(a, b):
"""Intersection of two open intervals."""
lower, upper = (max(a[0], b[0]), min(a[1], b[1]))
return (min(lower, upper), upper)
Note that it is convenient to work with open intervals, excluding the endpoints, so that we won’t have to treat empty or degenerate intersections as a special case. (This does come at a cost of allowing multiple representations of the empty set, namely any interval with equal endpoints.)
Armed with this setup, we can now define an angle interval to be any open circular sector or arc… although I think using corresponding portions of an annulus in these visualizations does a better job than sectors or arcs of conveying the Venn-diagram-ness of the intersections we are trying to compute.
Just as we identify a real interval on the number line by its two endpoints, we can represent an angle interval by the two angles made by its bounding radii relative to some zero reference… but we need some convention to deal with angle intervals that may contain that zero reference direction, as shown in the figure below.
A convenient convention will be to enforce , so that the interval “sweeps counterclockwise” from direction , through angle , to direction . And we can make such a representation unique if we “normalize” the bounding angles to enforce (so that may exceed 360°; more on this shortly). The function below implements this normalization:
def normalize_angle_interval(a, circle=360):
"""Normalize open angle interval to lower bound in [0, circle)."""
lower = a[0] % circle
return (lower, lower + (a[1] - a[0]))
This convention greatly simplifies the case analysis involved in computing the intersection of two such normalized open angle intervals:
def angle_interval_intersect(a, b, circle=360):
"""Unnormalized intersection of normalized open angle intervals."""
if a[1] > b[1]:
b, a = a, b
if a[0] + circle < b[1]:
a = (a[0] + circle, a[1] + circle)
return interval_intersect(a, b)
However, I’ve so far left out an important detail. The above implementation is correct, and reasonably elegant… as long as we are computing the intersection of open angle intervals that are not “wider” than 180°, such as the example shown in the figure below.
If we want to support intersections involving such “wide” angle intervals, then we have two problems: not only does the implementation become more complex, but even the interface does as well! That is, as the example above shows, in computing the intersection of two angle intervals we may need to return not just one but possibly two disjoint angle intervals.
In the application motivating this post, it was an acceptable tradeoff of generality for simplicity to restrict input angle intervals to at most 180°. And it’s a nice exercise to show that even if we allow “wide” inputs, the resulting single angle interval returned by the above implementation is either still correct, or at worst a subset of the full intersection.