little python hacks: intervals and line segments

Kragen Sitaker kragen@pobox.com
Sat, 30 Dec 2000 05:26:09 -0500 (EST)


An interval-math ADT and a line-segment ADT.  Insomnia.

# Kragen Sitaker, 2000-12-30.
# Written pursuant to SICP data abstraction exercise.  Not intended
# for real-life use, and here's why:
# - __mul__ (and therefore __div__) is likely unnecessarily slow
# - mixed-mode arithmetic isn't really defined; this is needed at least for
#   things like 1/x, where x is an Interval
# - it's quite likely buggy; I've only used it in cute little tests
# - there are no automated tests

class Interval:
    # general python glue methods
    def __init__(self, min, max):
        self.min = min
        self.max = max
    def __repr__(self):
        return "Interval(%s, %s)" % (self.min, self.max)
    def __str__(self):
        if self.min == self.max:
            return str(self.min)
        else:
            return "(%s..%s)" % (self.min, self.max)

    # arithmetic methods
    def __add__(self, other):
        return Interval(self.min + other.min,
                        self.max + other.max)
    def __neg__(self): return Interval(-self.max, -self.min)
    def __pos__(self): return self  # silly!
    def __abs__(self):
        if self.min <= 0 and self.max >= 0:
            return Interval(0, max([abs(self.min), abs(self.max)]))
        elif self.min <= 0:
            return Interval(abs(self.max), abs(self.min))
        else:
            return Interval(abs(self.min), abs(self.max))
    def __sub__(self, other): return self + -other
    def __mul__(self, other):
        corners = [self.min * other.min,
                   self.max * other.max,
                   self.min * other.max,
                   self.max * other.min]
        return Interval(min(corners), max(corners))
    def reciprocal(self):
        if self.min <= 0 and self.max >= 0:
            raise ZeroDivisionError, "interval division"
        else:
            return Interval(1./self.max, 1./self.min)
    def __div__(self, other): return self * other.reciprocal()

def const(value): return Interval(value, value)
def make_center_width(center, width):
    return Interval(center - width, center + width)
def tolerance(center, error):
    return make_center_width(center, error * center)


And here's the line-segment ADT:

# Kragen Sitaker, 2000-12-29.
# Representation of line segments, pursuant to a SICP exercise in
# the "Abstraction Barriers" section, although I did get a little
# carried away.
# Not intended for real use:
# * there are probably bugs
# * I'm not using it except for fun
# * it's too tied to floating-point, which has disadvantages for
#   eometrical stuff

from math import sqrt

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __repr__(self):
        return "Point(%s, %s)" % (self.x, self.y)
    def __cmp__(self, other):
        if self.x == other.x and self.y == other.y:
            return 0
        else: # try to come up with something stable
            return cmp(id(self), id(other))

class Segment:
    def __init__(self, start, end):
        self.start = start
        self.end = end
    def midpoint(self):
        return Point((self.start.x + self.end.x)/2.,
                     (self.start.y + self.end.y)/2.)
    def length(self):
        return sqrt((self.start.x - self.end.x)**2 +
                    (self.start.y - self.end.y)**2)
    def __repr__(self):
        return "[%s..%s]" % (self.start, self.end)

    # now for some fun stuff

    def vertical(self):
        return self.start.x == self.end.x
    # don't call this if the line is vertical
    def slope(self):
        return (self.end.y - self.start.y)/(self.end.x - self.start.x)
    # or this
    def yintercept(self):
        return self.start.y - self.start.x * self.slope()
    def parallel(self, other):
        if self.vertical():
            return other.vertical()
        elif other.vertical():
            return 0
        else:
            return self.slope() == other.slope()

    # return the point at which two segments would intersect if they extended
    # far enough
    def intersection(self, other):
        if self.parallel(other):
            # the segments may intersect, but we don't care
            return None
        elif self.vertical():
            return other.intersection(self)
        elif other.vertical():
            return Point(other.start.x,
                         self.yintercept() + other.start.x * self.slope())
        else:
            # m1x + b1 = m2x + b2; so
            # (m1 - m2)x + b1 - b2 == 0
            # (m1 - m2)x = b2 - b1
            # x = (b2 - b1)/(m1 - m2)
            x = ((other.yintercept() - self.yintercept()) /
                 (self.slope() - other.slope()))
            return Point(x, self.yintercept() + x * self.slope())

    def in_bbox(self, point):
        return ((point.x <= self.start.x and point.x >= self.end.x or
                 point.x <= self.end.x and point.x >= self.start.x) and
                (point.y <= self.start.y and point.y >= self.end.y or
                 point.y <= self.end.y and point.y >= self.start.y))
    # is a point collinear with this line segment?
    def on_line(self, point):
        if self.vertical():
            return point.x == self.start.x
        else:
            return (point.x * self.slope() + self.yintercept() == point.y)

    def intersects(self, other):
        if self.parallel(other):
            # they can "intersect" if they are collinear and overlap
            if not (self.in_bbox(other.start) or self.in_bbox(other.end)):
                return 0
            elif self.vertical():
                return self.start.x == other.start.x
            else:
                return self.yintercept() == other.yintercept()
        else:
            i = self.intersection(other)
            assert i is not None
            return self.in_bbox(i) and other.in_bbox(i)
        
def test():
    x = Segment(Point(1., 2.), Point(4., 6.))
    y = Segment(Point(2., 3.), Point(5., 7.))
    z = Segment(Point(1., 1.), Point(1., 3.))
    assert x.start == Point(1., 2.)
    assert x.start != x.end
    assert x.parallel(y)
    assert y.parallel(x)
    assert not x.parallel(z)
    assert not y.parallel(z)
    assert not z.parallel(x)
    assert not z.parallel(y)
    assert x.intersection(y) is None
    assert y.intersection(x) is None
    assert z.intersection(x) is not None
    assert x.intersection(z) is not None
    i = z.intersection(y)
    assert i is not None # even though they don't intersect
    assert z.on_line(i)
    assert y.on_line(i)
    assert z.in_bbox(i)
    assert not y.in_bbox(i)
    assert z.intersection(x) == x.intersection(z)
    assert x.intersection(z) == Point(1., 2.)
    xcross = Segment(Point(1., 6.), Point(4., 2.))
    assert x.length() == xcross.length()
    assert x.length() == 5.0
    assert xcross.intersection(x) is not None
    assert xcross.intersection(x) == x.intersection(xcross)
    assert x.intersection(xcross) == x.midpoint()
    assert z.intersects(x)
    assert not z.intersects(y)
    assert not z.intersects(xcross)
    assert not x.intersects(y)
    v1 = Segment(Point(1., 10.), Point(1., 20.))
    v2 = Segment(Point(1., 3.), Point(1., 20.))
    v3 = Segment(Point(2., 1.), Point(2., 3.))
    assert z.intersection(v1) is None
    assert not z.intersects(v1)
    assert z.intersects(v2)
    assert not z.intersects(v3)
    assert v1.intersects(v2)
    assert not v1.intersects(v3)

test()