3

I am using Welzl's algorithm to find the smallest enclosing circle (2d) or smallest enclosing sphere (3d) of a point cloud. Unfortunately, the algorithm has a very high recursion depth, namely the number of input points. Is there an iterative version of this algorithm? I could not find any and have no idea how the recursion can be changed to a loop.

I found some iterative smallest enclosing circle/sphere algorithms, but they work completely different and do not have the expected linear runtime of Welzl's algorithm.

pschill
  • 5,055
  • 1
  • 21
  • 42
  • 3
    Basically any recursive algorithm can be converted to iterative using a stack. – tobias_k Jun 19 '18 at 09:53
  • Does this also work if the function calls itself more than once in its body and uses local variables in between the call? – pschill Jun 19 '18 at 10:36
  • Yes - you can keep track of those in the stack too. Then again, this sort of emulated recursion has the same complexity as normal recursion – tucuxi Jun 19 '18 at 10:46
  • The complexity of the algorithm is okay. I just need to write it iteratively because I have a lot of input points and my recursion depth is limited (by default, Python only allows 1000 recursive calls). Is there a general rule that states what I need to put onto the stack, for example, function arguments and local variables? – pschill Jun 19 '18 at 11:08
  • 1
    @pschill if it's for an online judge, you can try `sys.setrecursionlimit()`. As for general rule, you basically have to understand what is "stack frame" and reimplement it manually. Typically all local variables (incl. function parameters) and return address are included there. – yeputons Jun 19 '18 at 12:11
  • Hi @pschill, not sure if this is useful to you years later, but I posted working Python code below. – David Eisenstat Oct 28 '21 at 17:30

4 Answers4

2

Shuffle the point array P[0…n−1]. Let R = ∅ and i = 0. Until i = n,

  • If P[i] ∈ R or R defines a circle that contains P[i], then set i ← i+1.
  • Otherwise, set R ← {P[i]} ∪ (R ∩ {P[i+1], …, P[n−1]}). If |R| < 3, then set i ← 0, else set i ← i+1.

Return R.

Tested implementation in Python 3:

from itertools import combinations
from random import randrange, shuffle


def mag_squared(v):
    return v.real ** 2 + v.imag ** 2


def point_in_circle(p, circle):
    if not circle:
        return False
    if len(circle) == 1:
        (q,) = circle
        return q == p
    if len(circle) == 2:
        q, r = circle
        return mag_squared((p - q) + (p - r)) <= mag_squared(q - r)
    if len(circle) == 3:
        q, r, s = circle
        # Translate p to the origin.
        q -= p
        r -= p
        s -= p
        # Orient the triangle counterclockwise.
        qr = r - q
        qs = s - q
        a, b = qr.real, qr.imag
        c, d = qs.real, qs.imag
        determinant = a * d - b * c
        assert determinant != 0
        if determinant < 0:
            r, s = s, r
        # Test whether the origin lies in the circle.
        a, b, c = q.real, q.imag, mag_squared(q)
        d, e, f = r.real, r.imag, mag_squared(r)
        g, h, i = s.real, s.imag, mag_squared(s)
        determinant = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g)
        return determinant >= 0
    assert False, str(circle)


def brute_force(points):
    for k in range(4):
        for circle in combinations(points, k):
            if any(
                point_in_circle(p, circle[:i] + circle[i + 1 :])
                for (i, p) in enumerate(circle)
            ):
                continue
            if all(point_in_circle(p, circle) for p in points):
                return circle
    assert False, str(points)


def welzl_recursive(points, required=()):
    points = list(points)
    if not points or len(required) == 3:
        return required
    p = points.pop(randrange(len(points)))
    circle = welzl_recursive(points, required)
    if point_in_circle(p, circle):
        return circle
    return welzl_recursive(points, required + (p,))


def welzl(points):
    points = list(points)
    shuffle(points)
    circle_indexes = {}
    i = 0
    while i < len(points):
        if i in circle_indexes or point_in_circle(
            points[i], [points[j] for j in circle_indexes]
        ):
            i += 1
        else:
            circle_indexes = {j for j in circle_indexes if j > i}
            circle_indexes.add(i)
            i = 0 if len(circle_indexes) < 3 else i + 1
    return [points[j] for j in circle_indexes]


def random_binomial():
    return sum(2 * randrange(2) - 1 for i in range(100))


def random_point():
    return complex(random_binomial(), random_binomial())


def test(implementation):
    for rep in range(1000):
        points = [random_point() for i in range(randrange(20))]
        expected = set(brute_force(points))
        for j in range(10):
            shuffle(points)
            got = set(implementation(points))
            assert expected == got or (
                all(point_in_circle(p, expected) for p in got)
                and all(point_in_circle(p, got) for p in expected)
            )


def graphics():
    points = [random_point() for i in range(100)]
    circle = welzl(points)
    print("%!PS")
    print(0, "setlinewidth")
    inch = 72
    print(8.5 * inch / 2, 11 * inch / 2, "translate")
    print(inch / 16, inch / 16, "scale")
    for p in points:
        print(p.real, p.imag, 1 / 4, 0, 360, "arc", "stroke")
    for p in circle:
        print(p.real, p.imag, 1 / 4, 0, 360, "arc", "fill")
    print("showpage")


test(welzl_recursive)
test(welzl)
graphics()
David Eisenstat
  • 64,237
  • 7
  • 60
  • 120
  • I implemented this algorithm in Julia, and my program produces different outputs from run to run, while Welzl's algorithm should AFAIK always give an optimal solution. The Julia code is a quite straightforward translation of your description, except that I converted it to one-based indexing. So I think that your algorithm is wrong. – user2373145 Oct 28 '21 at 15:42
  • 1
    @user2373145 you're right; it was subtly bugged. We can't leap past the end when there are three points, but we don't have to start at 0, either. I edited the post with a fix and tested Python 3 code. – David Eisenstat Oct 28 '21 at 17:28
  • Comparing your algorithm and the recursive algorithm on many vectors of 10000 to 100000 points, I notice something: the iterative version can be made to be mostly faster than the recursive version, but with a catch: the iterative version seems to go into an infinite loop for some input vectors! I suspect this could have something to do with numerical inaccuracy of IEEE 754 and with the comparison which determines whether a point lies on a disk. Any ideas as to how your algorithm could be made more robust in this regard? – user2373145 Oct 29 '21 at 13:43
  • 1
    @user2373145 Are you sure it's an infinite loop and not just cubic? Definitely possible that my crappy predicates are the problem here (there's a reason I only tested on integers); https://www.cs.cmu.edu/~quake/robust.html is the standard. – David Eisenstat Oct 29 '21 at 13:45
  • @user2373145 The other possibility is that the fixed shuffle variant causes the variance of the running time to be large despite the mean being OK, which would be interesting. – David Eisenstat Oct 29 '21 at 13:47
  • I think it's an infinite loop, as I never observed a longer-than-usual running time that was short enough to wait it out. – user2373145 Oct 29 '21 at 13:48
  • Is there a reason to use a set rather than a list to store `circle_indexes`? –  Feb 20 '22 at 21:39
  • And how did you get to this version from the recursive one? Will i be reset to zero only a constant number of times on average? –  Feb 20 '22 at 21:52
  • @YvesDaoust no reason for a set, just better correspondence with the math. To get from the recursive version to the iterative involves 1) shuffling once and always popping the last element of the list (which as Welzl observes is allowed in expectation, though presumably the variance gets worse) 2) translating to continuation passing form and defunctionalizing it. – David Eisenstat Feb 21 '22 at 02:26
  • @DavidEisenstat: thanks for these answers. Though your 2) sounds chinese to me. –  Feb 21 '22 at 07:59
1

The Python code below is a conversion from working Rust code; but this converted code is untested, so consider it pseudocode. get_circ_2_pts() and get_circ_3_pts() are stubs for functions that take 2 and 3 Points respectively to get the intersecting circle. Formulae for these should be easy to find.

The code below mimics how compiled native code behaves wrt pushing/restoring stack frames during recursive calls and passing return values back to the caller, etc.

This is not a good way to go about converting a recursive function to an iterative one, but it works. For native compiled code, it basically shifts the function's stack from stack memory to heap memory

There isn't going to be much payoff in terms of speed. And there also isn't any payoff in memory efficiency unless the Welzl algorithm implemented in an application is processing so many points that it's eating up stack space; in which case, this may address the problem.

For a generalized example of this approach of recursive to iterative conversion, you can check out my other post on this topic.

from random import randint
from copy   import deepcopy

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def distance(self, p):
        ((p.x - self.x)**2 + (p.y - self.y)**2)**.5
        
class Circle:
    def __init__(self, center, radius):
        self.center = center
        self.radius = radius

    def contains_point(self, p):
        return self.center.distance(p) <= self.radius


class Frame:
    def __init__(self, size, stage='FIRST'):
        self.stage = stage
        self.r     = []
        self.n     = size
        self.p     = Point(0, 0)
        self.ret   = None
        
    def clone(self):
        return deepcopy(self)

def welzl(points):
    circle = None
    stack  = []
    stack.append(Frame(len(points)))
    
    while len(stack):
        frame = stack.pop()
        
        if frame.n == 0 or len(frame.r) == 3:
            r_len = len(frame.r)
            ret   = None
            
            if   r_len == 0: ret = Circle(Point(0, 0), 0)
            elif r_len == 1: ret = Circle(frame.r[0], 0)
            elif r_len == 2: ret = get_circ_2_pts(*frame.r[:2])
            elif r_len == 3: ret = get_circ_3_pts(*frame.r[:3])
                
            if len(stack):
                stack[-1].ret = ret
            else:
                circle = ret
        else:
            if frame.stage == 'FIRST':
                n           = frame.n - 1
                idx         = randint(0, n)
                frame.p     = points[idx]
                points[idx] = points[n]
                points[n]   = frame.p
                call        = frame.clone()
                                
                frame.stage = 'SECOND'
                stack.append(frame)
                call.n      = n
                call.stage  = 'FIRST'
                stack.append(call)
                
            elif frame.stage == 'SECOND':
                if frame.ret.contains_point(frame.p):
                    if len(stack):
                        stack[-1].ret = frame.ret
                    else:
                        circle = frame.ret
                else:
                    frame.stage = 'THIRD'
                    stack.append(frame)                    
                    call        = frame.clone()
                    call.n     -= 1
                    call.stage  = 'FIRST'
                    call.r.append(frame.p)
                    stack.append(call)
                
            elif frame.stage == 'THIRD':
                if len(stack):
                    stack[-1].ret = frame.ret
                else:
                    circle = frame.ret
                
    return circle
Todd
  • 4,669
  • 1
  • 22
  • 30
0

A working version of a non-recursive implementation of Welzl's algorithm in Rust. The code below is part of a working solution to a coding challenge problem. It's been run against over a dozen test cases with large data sets and met timing constraints. Consider it tested code.

The only thing missing is the implementation for random() which produces a random value within a range of indexes - easy enough to implement for anyone wanting to use this.

The approach to conversion from recursive to non-recursive is very similar to my other posted answer in Python, but this posted answer is simpler and more complete. It is also more memory efficient.

Below is the primary method of a Circle object that implements the algorithm non-recursively:

    fn from_points_internal(p: &mut Vec<Point>, 
                            r: Vec<Point>, 
                            n: usize) 
        -> Self
    {
        enum Block { First, Second }
        use  Block::*;

        let mut params  = vec![(r, n, None, First)];
        let mut results = vec![];

        while let Some((mut r, n, p1, jump)) = params.pop() {
            match jump {
                First if n == 0 || r.len() == 3 => {
                    let c1 = match r.len() {
                        3 => Self::from_3_points(&r[0], &r[1], &r[2]),
                        2 => Self::from_2_points(&r[0], &r[1]),
                        1 => Self::new(r[0], 0.),
                        0 => Self::new(Point { x: 0., y: 0. }, 0.),
                        _ => panic!("Too many `r` points."),
                    };
                    results.push(c1);
                },
                First => {
                    let idx = random(n);
                    let p1  = p[idx];
                    p.swap(idx, n - 1);

                    params.push((r.clone(), n, Some(p1), Second));
                    params.push((r, n - 1, None, First));
                },
                Second => {
                    let c1 = results.pop().unwrap();
                    let p1 = p1.unwrap();

                    if c1.contains_point(&p1) {
                        results.push(c1);
                    }
                    else {
                        r.push(p1);
                        params.push((r, n - 1, None, First));
                    }
                }
            }
        }
        results.pop().unwrap()
    }

For comparison, here is the recursive version of the same method:

    fn from_points_internal(    p: &mut Vec<Point>, 
                            mut r: Vec<Point>, 
                                n: usize) 
        -> Self
    {
        if n == 0 || r.len() == 3 {
            match r.len() {
                3 => Self::from_3_points(&r[0], &r[1], &r[2]),
                2 => Self::from_2_points(&r[0], &r[1]),
                1 => Self::new(r[0], 0.),
                0 => Self::new(Point { x: 0., y: 0. }, 0.),
                _ => panic!("Too many 'r' points passed to \
                             Circle::from_points_internal()."),
            }
        } else {
            let idx = random(n); // Get random index.
            let p1  = p[idx];
            p.swap(idx, n - 1);
            
            let c1 = Self::from_points_internal(p, r.clone(), n - 1);
            
            if c1.contains_point(&p1) {
                c1
            }
            else {
                r.push(p1);
                Self::from_points_internal(p, r, n - 1)
            }
        }
    }

Here's the full code for Circle and Point (iterative solution):

/// Object representing a circle and providing methods to calculate the minimum
/// enclosing circle of a collection of points.
///
#[derive(Clone, Copy)]
pub struct Circle {
    center: Point,
    radius: f64,
}

impl Circle {
    /// Produce a new circle object with center point and given radius.
    ///
    pub fn new(center: Point, radius: f64) -> Self
    {
        Self { center, radius }
    }
    
    /// Produce an enclosing circle for the slice of points given.
    ///
    pub fn from_points<P>(points: P) -> Self
    where 
        P: IntoIterator<Item = Point>
    {
        let mut points = points.into_iter().collect::<Vec<_>>();
        let     len    = points.len();

        // Shuffle points.
        for i in 0..len {
            let j = random(len);
            points.swap(i, j);
        }
        Self::from_points_internal(&mut points, vec![], len)
    }
    
    /// Returns the radius of the enclosing circle.
    ///
    pub fn radius(&self) -> f64
    {
        self.radius
    }
    
    /// Internal non-recursive function that finds the minimum sized circle that can
    /// enclose all points. The time-complexity should work out to `O(n)`.
    ///
    fn from_points_internal(p: &mut Vec<Point>, 
                            r: Vec<Point>, 
                            n: usize) 
        -> Self
    {
        enum Block { First, Second }
        use  Block::*;

        let mut params  = vec![(r, n, None, First)];
        let mut results = vec![];

        while let Some((mut r, n, p1, jump)) = params.pop() {
            match jump {
                First if n == 0 || r.len() == 3 => {
                    let c1 = match r.len() {
                        3 => Self::from_3_points(&r[0], &r[1], &r[2]),
                        2 => Self::from_2_points(&r[0], &r[1]),
                        1 => Self::new(r[0], 0.),
                        0 => Self::new(Point { x: 0., y: 0. }, 0.),
                        _ => panic!("Too many `r` points."),
                    };
                    results.push(c1);
                },
                First => {
                    let idx = random(n);
                    let p1  = p[idx];
                    p.swap(idx, n - 1);

                    params.push((r.clone(), n, Some(p1), Second));
                    params.push((r, n - 1, None, First));
                },
                Second => {
                    let c1 = results.pop().unwrap();
                    let p1 = p1.unwrap();

                    if c1.contains_point(&p1) {
                        results.push(c1);
                    }
                    else {
                        r.push(p1);
                        params.push((r, n - 1, None, First));
                    }
                }
            }
        }
        results.pop().unwrap()
    }

    /// Create circle that intersects the 3 points.
    ///
    fn from_3_points(p1: &Point, p2: &Point, p3: &Point) -> Self
    {
        let p12 = Point::new(p2.x - p1.x, p2.y - p1.y);
        let p13 = Point::new(p3.x - p1.x, p3.y - p1.y);
        
        let c12  = p12.x * p12.x + p12.y * p12.y;
        let c13  = p13.x * p13.x + p13.y * p13.y;
        let c123 = p12.x * p13.y - p12.y * p13.x;
        
        let cx = (p13.y * c12 - p12.y * c13) / (2. * c123);
        let cy = (p12.x * c13 - p13.x * c12) / (2. * c123);
        
        let center = Point::new(cx + p1.x, cy + p1.y);
        
        Self::new(center, center.distance(p1))
    }
    
    /// Creates circle that intersects the two points.
    ///
    fn from_2_points(p1: &Point, p2: &Point) -> Self
    {
        let center = Point::new((p1.x + p2.x) / 2., 
                                (p1.y + p2.y) / 2.);
        Self::new(center, p1.distance(p2) / 2.)
    }
    
    /// Returns true if the circile contains the point.
    ///
    fn contains_point(&self, point: &Point) -> bool
    {
        self.center.distance(point) <= self.radius
    }
}

/// Represents a point on a 2D plane.
///
#[derive(Clone, Copy)]
pub struct Point {
    x: f64,
    y: f64,
}

impl Point {
    pub fn new(x: f64, y: f64) -> Self 
    {
        Self { x, y }
    }
    /// Returns the distance between `self` and `other`.
    ///
    pub fn distance(&self, other: &Point) -> f64
    {
        ((other.x - self.x).powf(2.) + 
         (other.y - self.y).powf(2.)).sqrt()
    }    
}
Todd
  • 4,669
  • 1
  • 22
  • 30
0

You can understand an iterative version as follows:

  • you can form a non-trivial smallest enclosing circle with two or three points of the set;

  • any point found to lie inside or on such a circle can be discarded from further search, as it cannot form a smaller circle with these two or three.

So the following procedure can be used:

  • pick a point;

  • A) pick a second point and construct the smallest circle enclosing these two;

  • B) if all remaining points are interior, you are done;

  • take an exterior point and construct the smallest circle enclosing these three; if this circle is through two points, goto B

  • if all remaining points are interior, you are done;

  • take an exterior point and go to A.