Task: Smallest enclosing circle problem




Go: package main

import (
    "fmt"
    "log"
    "math"
    "math/rand"
    "time"
)

type Point struct{ x, y float64 }

type Circle struct {
    c Point
    r float64
}


func (p Point) String() string { return fmt.Sprintf("(%f, %f)", p.x, p.y) }


func distSq(a, b Point) float64 {
    return (a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y)
}


func getCircleCenter(bx, by, cx, cy float64) Point {
    b := bx*bx + by*by
    c := cx*cx + cy*cy
    d := bx*cy - by*cx
    return Point{(cy*b - by*c) / (2 * d), (bx*c - cx*b) / (2 * d)}
}


func (ci Circle) contains(p Point) bool {
    return distSq(ci.c, p) <= ci.r*ci.r
}


func (ci Circle) encloses(ps []Point) bool {
    for _, p := range ps {
        if !ci.contains(p) {
            return false
        }
    }
    return true
}


func (ci Circle) String() string { return fmt.Sprintf("{%v, %f}", ci.c, ci.r) }


func circleFrom3(a, b, c Point) Circle {
    i := getCircleCenter(b.x-a.x, b.y-a.y, c.x-a.x, c.y-a.y)
    i.x += a.x
    i.y += a.y
    return Circle{i, math.Sqrt(distSq(i, a))}
}


func circleFrom2(a, b Point) Circle {
    c := Point{(a.x + b.x) / 2, (a.y + b.y) / 2}
    return Circle{c, math.Sqrt(distSq(a, b)) / 2}
}


func secTrivial(rs []Point) Circle {
    size := len(rs)
    if size > 3 {
        log.Fatal("There shouldn't be more than 3 points.")
    }
    if size == 0 {
        return Circle{Point{0, 0}, 0}
    }
    if size == 1 {
        return Circle{rs[0], 0}
    }
    if size == 2 {
        return circleFrom2(rs[0], rs[1])
    }
    for i := 0; i < 2; i++ {
        for j := i + 1; j < 3; j++ {
            c := circleFrom2(rs[i], rs[j])
            if c.encloses(rs) {
                return c
            }
        }
    }
    return circleFrom3(rs[0], rs[1], rs[2])
}


func welzlHelper(ps, rs []Point, n int) Circle {
    rc := make([]Point, len(rs))
    copy(rc, rs) 
    if n == 0 || len(rc) == 3 {
        return secTrivial(rc)
    }
    idx := rand.Intn(n)
    p := ps[idx]
    ps[idx], ps[n-1] = ps[n-1], p
    d := welzlHelper(ps, rc, n-1)
    if d.contains(p) {
        return d
    }
    rc = append(rc, p)
    return welzlHelper(ps, rc, n-1)
}


func welzl(ps []Point) Circle {
    var pc = make([]Point, len(ps))
    copy(pc, ps)
    rand.Shuffle(len(pc), func(i, j int) {
        pc[i], pc[j] = pc[j], pc[i]
    })
    return welzlHelper(pc, []Point{}, len(pc))
}

func main() {
    rand.Seed(time.Now().UnixNano())
    tests := [][]Point{
        {Point{0, 0}, Point{0, 1}, Point{1, 0}},
        {Point{5, -2}, Point{-3, -2}, Point{-2, 5}, Point{1, 6}, Point{0, 2}},
    }
    for _, test := range tests {
        fmt.Println(welzl(test))
    }
}




Python: import numpy as np

class ProjectorStack:
    
    def __init__(self, vec):
        self.vs = np.array(vec)
        
    def push(self, v):
        if len(self.vs) == 0:
            self.vs = np.array([v])
        else:
            self.vs = np.append(self.vs, [v], axis=0)
        return self
    
    def pop(self):
        if len(self.vs) > 0:
            ret, self.vs = self.vs[-1], self.vs[:-1]
            return ret
    
    def __mul__(self, v):
        s = np.zeros(len(v))
        for vi in self.vs:
            s = s + vi * np.dot(vi, v)
        return s
    
class GaertnerBoundary:
    
    def __init__(self, pts):
        self.projector = ProjectorStack([])
        self.centers, self.square_radii = np.array([]), np.array([])
        self.empty_center = np.array([np.NaN for _ in pts[0]])


def push_if_stable(bound, pt):
    if len(bound.centers) == 0:
        bound.square_radii = np.append(bound.square_radii, 0.0)
        bound.centers = np.array([pt])
        return True
    q0, center = bound.centers[0], bound.centers[-1]
    C, r2  = center - q0, bound.square_radii[-1]
    Qm, M = pt - q0, bound.projector
    Qm_bar = M * Qm
    residue, e = Qm - Qm_bar, sqdist(Qm, C) - r2
    z, tol = 2 * sqnorm(residue), np.finfo(float).eps * max(r2, 1.0)
    isstable = np.abs(z) > tol
    if isstable:
        center_new  = center + (e / z) * residue
        r2new = r2 + (e * e) / (2 * z)
        bound.projector.push(residue / np.linalg.norm(residue))
        bound.centers = np.append(bound.centers, np.array([center_new]), axis=0)
        bound.square_radii = np.append(bound.square_radii, r2new)
    return isstable

def pop(bound):
    n = len(bound.centers)
    bound.centers = bound.centers[:-1]
    bound.square_radii = bound.square_radii[:-1]
    if n >= 2:
        bound.projector.pop()
    return bound


class NSphere:
    def __init__(self, c, sqr):
        self.center = np.array(c)
        self.sqradius = sqr

def isinside(pt, nsphere, atol=1e-6, rtol=0.0):
    r2, R2 = sqdist(pt, nsphere.center), nsphere.sqradius
    return r2 <= R2 or np.isclose(r2, R2, atol=atol**2,rtol=rtol**2)

def allinside(pts, nsphere, atol=1e-6, rtol=0.0):
    for p in pts:
        if not isinside(p, nsphere, atol, rtol):
            return False
    return True

def move_to_front(pts, i):
    pt = pts[i]
    for j in range(len(pts)):
        pts[j], pt = pt, np.array(pts[j])
        if j == i:
            break
    return pts

def dist(p1, p2):
    return np.linalg.norm(p1 - p2)

def sqdist(p1, p2):
    return sqnorm(p1 - p2)

def sqnorm(p):
    return np.sum(np.array([x * x for x in p]))

def ismaxlength(bound):
    len(bound.centers) == len(bound.empty_center) + 1

def makeNSphere(bound):
    if len(bound.centers) == 0: 
        return NSphere(bound.empty_center, 0.0)
    return NSphere(bound.centers[-1], bound.square_radii[-1])

def _welzl(pts, pos, bdry):
    support_count, nsphere = 0, makeNSphere(bdry)
    if ismaxlength(bdry):
        return nsphere, 0
    for i in range(pos):
        if not isinside(pts[i], nsphere):
            isstable = push_if_stable(bdry, pts[i])
            if isstable:
                nsphere, s = _welzl(pts, i, bdry)
                pop(bdry)
                move_to_front(pts, i)
                support_count = s + 1
    return nsphere, support_count

def find_max_excess(nsphere, pts, k1):
    err_max, k_max = -np.Inf, k1 - 1
    for (k, pt) in enumerate(pts[k_max:]):
        err = sqdist(pt, nsphere.center) - nsphere.sqradius
        if  err > err_max:
            err_max, k_max = err, k + k1
    return err_max, k_max - 1

def welzl(points, maxiterations=2000):
    pts, eps = np.array(points, copy=True), np.finfo(float).eps
    bdry, t = GaertnerBoundary(pts), 1
    nsphere, s = _welzl(pts, t, bdry)
    for i in range(maxiterations):
        e, k = find_max_excess(nsphere, pts, t + 1)
        if e <= eps:
            break
        pt = pts[k]
        push_if_stable(bdry, pt)
        nsphere_new, s_new = _welzl(pts, s, bdry)
        pop(bdry)
        move_to_front(pts, k)
        nsphere = nsphere_new
        t, s = s + 1, s_new + 1
    return nsphere

if __name__ == '__main__':
    TESTDATA =[
        np.array([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0]]),
        np.array([[5.0, -2.0], [-3.0, -2.0], [-2.0, 5.0], [1.0, 6.0], [0.0, 2.0]]),
        np.array([[2.0, 4.0, -1.0], [1.0, 5.0, -3.0], [8.0, -4.0, 1.0], [3.0, 9.0, -5.0]]),
        np.random.normal(size=(8, 5))
    ]
    for test in TESTDATA:
        nsphere = welzl(test)
        print("For points: ", test)
        print("    Center is at: ", nsphere.center)
        print("    Radius is: ", np.sqrt(nsphere.sqradius), "\n")