Updated 2013-07-30 07:39:56 by uniquename

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
}

# 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

}

# 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}]
}
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
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
}
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.) 