Smallest Enclosing Disc

Keith Vetter 2005-11-21 : I was perusing a book on Computational Geometry and I came across an interesting algorithm: consider a robot arm whose base is affixed to the work floor. The arm has to pick up various items and place them elsewhere. What would be a good position for the base of the arm? How about "somewhere in the middle" of the points it has to reach. If you locate it in the middle of the smallest enclosing disc then you minimize the maximum distance the arm has to travel.

The algorithm used below works by incrementally adding points to an existing solution, noting that if the new point is not in the current smallest disc, then the new disc will have that point on its perimeter. This produces an O(n) solution; there are faster ones out there but this was fun to program.


 ##+##########################################################################
 #
 # SmallestDisc.tcl -- Computes the smallest enclosing disc for a set of points
 # by Keith Vetter, Nov 21, 2005
 #

 package require Tk
 if {! [catch {package require tile}]} {         ;# Be tile friendly
    namespace import -force ::ttk::button
    namespace import -force ::ttk::label
 }

 # Random algorithm for computing smallest enclosing disk
 proc MiniDisc {PTS0} {
    set PTS [Shuffle $PTS0]

    set pre0 [lrange $PTS 0 1]
    set D [SegmentCenter [lindex $PTS 0] [lindex $PTS 1]]
    foreach pt [lrange $PTS 2 end] {
        if {! [Within $D $pt]} {
            set D [MiniDiscWithPoint $pre0 $pt]
        }
        lappend pre0 $pt
    }
    return $D
 }

 # Returns smallest enclosing disk for points PTS with q on perimeter
 proc MiniDiscWithPoint {PTS q} {
    set pre1 [lindex $PTS 0]
    set D [SegmentCenter $pre1 $q]
    foreach pt [lrange $PTS 1 end] {
        if {! [Within $D $pt]} {
            set D [MiniDiscWith2Points $pre1 $pt $q]
        }
        lappend pre1 $pt
    }
    return $D
 }

 # Returns smallest enclosing disk for points PTS with q1, q2 on perimeter
 proc MiniDiscWith2Points {PTS q1 q2} {
    set D [SegmentCenter $q1 $q2]
    foreach pt $PTS {
        if {! [Within $D $pt]} {
            set D [CircumCenter $pt $q1 $q2]
        }
    }
    return $D
 }

 # Returns circle that has P1-P2 as a diameter
 proc SegmentCenter {P1 P2} {
    set h12 [VAdd $P1 [VSub $P2 $P1] .5]
    foreach {rx ry} [XY [VSub $P1 $h12]] break  ;# Radius vector
    set radius [expr {hypot($rx,$ry)}]          ;# Radius magnitude
    return [concat $h12 $radius]
 }

 # compute circumcenter for three points
 proc CircumCenter {P1 P2 P3} {

    set h12 [VAdd $P1 [VSub $P2 $P1] .5]        ;# Midpoints of each side
    set h13 [VAdd $P1 [VSub $P3 $P1] .5]

    set n12 [VNormal [VSub $P2 $P1]]            ;# Normal to side p1-p2
    set n13 [VNormal [VSub $P3 $P1]]
    set O [IntersectV $h12 $n12 $h13 $n13]      ;# The circumcenter
    foreach {rx ry} [XY [VSub $P1 $O]] break    ;# Radius vector
    set radius [expr {hypot($rx,$ry)}]          ;# Radius magnitude

    return [concat $O $radius]
 }

 # returns true if pt is inside circle D
 proc Within {D pt} {
    foreach {ox oy} [XY [lindex $D 0]] break
    set r [lindex $D 1]
    foreach {x y} [XY $pt] break
    return [expr {(($x-$ox)*($x-$ox) + ($y-$oy)*($y-$oy)) <= $r*$r}]
 }

 # splits apart our point data structure
 proc XY {P} {
    return [split $P ","]
 }

 # adds to vectors, possibly scaling the second one
 proc VAdd {v1 v2 {scaling 1}} {
    foreach {x1 y1} [XY $v1] {x2 y2} [XY $v2] break
    return "[expr {$x1 + $scaling*$x2}],[expr {$y1 + $scaling*$y2}]"
 }
 proc VSub {v1 v2} { return [VAdd $v1 $v2 -1] }
 proc VNormal {v} {foreach {x y} [XY $v] break; return "$y,[expr {-1 * $x}]"}
 ##+##########################################################################
 #
 # IntersectV -- find where 2 point/vector intersect
 #
 # p1+K(v1) = p3+J(v3)
 # convert into and solve matrix equation (a b / c d) ( K / J) = ( e / f )
 #
 proc IntersectV {p1 v1 p3 v3} {
    foreach {x1 y1} [XY $p1] {vx1 vy1} [XY $v1] {x3 y3} [XY $p3] {vx3 vy3} [XY $v3] break

    set a $vx1
    set b [expr {-1 * $vx3}]
    set c $vy1
    set d [expr {-1 * $vy3}]
    set e [expr {$x3 - $x1}]
    set f [expr {$y3 - $y1}]

    set det [expr {double($a*$d - $b*$c)}]
    if {$det == 0} {error "Determinant is 0"}

    set k [expr {($d*$e - $b*$f) / $det}]
    #set j [expr {($a*$f - $c*$e) / $det}]
    return [VAdd $p1 $v1 $k]
 }
 proc Shuffle { l } {
    set len [llength $l]
    set len2 $len
    for {set i 0} {$i < $len-1} {incr i} {
        set n [expr {int($i + $len2 * rand())}]
        incr len2 -1

        set temp [lindex $l $i]                 ;# Swap elements at i & n
        lset l $i [lindex $l $n]
        lset l $n $temp
    }
    return $l
 }
 ################################################################
 #
 # GUI stuff below
 #
 proc DoDisplay {} {
    canvas .c -bg white -width 500 -height 500 -highlightthickness 0 -bd 2 -relief ridge
    bind .c <1> [list Click %x %y]

    label .l -justify l -relief ridge \
        -text "Click to create a new point\nRight click to delete point"
    button .reset -text "Reset" -command Reset -state disabled
    button .about -text "About" -command About
    pack .c -fill both -expand 1
    pack .l -side left -expand 1 -fill x
    pack .reset .about -side left -expand 1
    bind all <Key-F2> [list console show]
 }
 proc Reset {} {
    array unset ::P
    set ::P(id) 0
    .c delete all
    .c create text 250 10 -anchor n -font {Times 18 bold underline} \
        -text "Smallest Enclosing Disk Demo"
    ShowDisc
 }
 proc Click {x y} {
    global P

    set x [.c canvasx $x]
    set y [.c canvasy $y]
    set P(pt,[incr P(id)]) "$x,$y"

    set xy [Expand $x $y 5]
    .c create oval $xy -tag pt$P(id) -fill magenta -outline black
    .c bind pt$P(id) <3> [list DeletePoint $P(id)]
    ShowDisc
 }
 proc DeletePoint {id} {
    global P
    unset -nocomplain P(pt,$id)
    .c delete pt$id
    ShowDisc
 }
 proc Expand {x y dxy} {
    set x0 [expr {$x-$dxy}]
    set y0 [expr {$y-$dxy}]
    set x1 [expr {$x+$dxy}]
    set y1 [expr {$y+$dxy}]
    return [list $x0 $y0 $x1 $y1]
 }
 proc ShowDisc {} {
    global P

    .c delete disc

    set PTS {}
    foreach a [array name P pt,*] {
        lappend PTS $P($a)
    }
    .reset config -state [expr {$PTS eq {} ? "disabled" : "normal"}]
    if {[llength $PTS] < 2} return
    set D [MiniDisc $PTS]

    foreach {x y} [XY [lindex $D 0]] break
    set r [lindex $D 1]

    .c delete disc
    set xy [Expand $x $y $r]
    .c create oval $xy -tag {x disc} -fill turquoise1 -outline turquoise1
    set xy [Expand $x $y 3]
    .c create oval $xy -tag disc -fill red -outline black
    .c create line $x $y [expr {$x+$r}] $y -tag disc -fill red -arrow last
    .c lower disc
 }
 proc About {} {
    set txt "Smallest Enclosing Disc\nby Keith Vetter, Nov 2005\n\n"
    append txt "Consider a robot arm attached to the work floor. The arm\n"
    append txt "has to pick up various items scattered around it and place\n"
    append txt "them at other points. What would be the best position for\n"
    append txt "the robot arm? Somewhere 'in the middle' of the points it\n"
    append txt "has to reach. How about at the center of the smallest disc\n"
    append txt "that encloses all the points.\n\n"
    append txt "Formally, given a set P of n points in the plane (the points\n"
    append txt "the robot arm must be able to reach), find the SMALLEST\n"
    append txt "ENCLOSING DISC for P, that is, the smallest disc that\n"
    append txt "contains all the points in P\n"
    tk_messageBox -message $txt
 }
 DoDisplay
 Reset

The above code needs a "set P(id) 0" in there somewhere...

KPV It does it in Reset which gets called by the last line of code. I bet you cut and pasted the code into a console and the last line didn't get executed.


uniquename 2013jul29

This code could use an image to show what it produces. (I added a frame for the label and 2 buttons and packed them at the top of the GUI, instead of them appearing at the bottom of the GUI.)

vetter_smallestEnclosingDisk_screenshot_526x472.jpg