The Smallest Circle (#157)

The three rules of Ruby Q. 2:

  1. Please do not post any solutions or spoiler discussion for this
    quiz until 48 hours have passed from the time on this message.

  2. Support Ruby Q. 2 by submitting ideas as often as you can! (A
    permanent, new website is in the works for Ruby Q. 2. Until then,
    please visit the temporary website at <http://
    matthew.moss.googlepages.com/home>.

  3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem
helps everyone
on Ruby T. follow the discussion. Please reply to the original
quiz message,
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
=-=-=-

The Smallest Circle
by Matthew M.

Your task this week sounds simple enough, but may be more difficult
than it first appears. Given a set of points on a plane, your goal is
to find the smallest circle that encloses those points.

You are to provide a function, encircle, that takes an array of
points and returns the smallest circle surrounding those points.
Start with the following base code and extend as needed to solve the
problem:

 class Point < Struct.new(:x, :y)
     def self.random
         Point.new(rand, rand)
     end

     def to_s
         "(#{x}, #{y})"
     end
 end

 class Circle < Struct.new(:center, :radius)
     def to_s
         "{center:#{center}, radius:#{radius}}"
     end
 end

 def encircle(points)        # takes array of Point objects
     # returns a Circle object
 end

I will be running several tests on the submitted solutions, with
various point sets, to see how well they perform at this task. I
recommend you you test your algorithm with a variety of sample sets,
from small sets consisting of just 1-5 points, up to medium and
larger sets, containing a few thousand points.

To generate an array of random points, start with the above code and
add:

 def generate_samples(n)
     (1..n).map { Point.random }
 end

And then you may test your implementation like this:

 # encircle 10 random points
 puts encircle( generate_samples(10) )

As mentioned, this one may be more difficult than it seems. Feel free
to estimate the smallest circle, if you get stuck on getting the
exact solution.

If one of the points is on the enclosing circle, is that a valid
solution ?

Horea R. wrote:

If one of the points is on the enclosing circle, is that a valid solution ?

I wouldn’t care about such details :
the points are defined by floating point coordinates : I’d say that
defining a circle with a center and a radius in floating point which
passes exactly through such points is most often impossible.
There are obvious exceptions like circles with zero radius and some
values where there’s no approximation in the computations but these
cases aren’t likely to matter here.

Lionel

  • Horea R., 2008-02-16, 03:06:

If one of the points is on the enclosing circle, is that a valid
solution ?

This is necessarily so if the task is to make sense. Assume you have two
points A and B and construct M such that M is the middle of AB. Then the
smallest circle that encloses A, M, and B should be the one that has M
as its center and has a diameter equal to the length of AB because
otherwise you run into the fundamental issue that the infimum of the
set of the diameters of all circles that enclose A, B, and M is the
length of AB while a circle with precisely this diameter does not meet
the requirements.

In other words: Yes, it would make no sense not to allow this as a
valid solution because it is so easy to construct a case in that this
definition runs into fundamental trouble.

Just in case: The fundamental issue is that assuming the solution
‘point on circle’ is invalid would mean that the very basic case of
three equidistant points on a straight line had no valid solution at
all.

While mathematics is no natural science you will find very few cases in
which the definitions (like that of ‘enclosing circle’ in this case)
AIM AT being incompatible with common sense.

Just to give an example of alien-looking definitions that make perfect
sense: The field being defined by the set {0,1} with multiplication and
addition defined as

00 = 01 = 10 = 0, 11 = 1
0+0 = 1+1 = 0, 0+1 = 1+0 = 0

This can be interpreted as binary digits with the operations AND
(as multiplication) and XOR (as addition). Take a look at this table:

a b c s
0 0 0 0
0 1 0 1
1 0 0 1
1 1 1 0

a and b are the inputs, c is the result of AND and s is the result of
XOR. Read ‘c’ as ‘carry’ and ‘s’ as ‘sum’ and you see an addition of
two bits with carry.

Hope that was not to much math hacking :slight_smile:

Josef ‘Jupp’ Schugt

On Feb 15, 1:06 pm, “Horea R.” [email protected] wrote:

If one of the points is on the enclosing circle, is that a valid solution ?

Yes, points on the circle are considered enclosed.

Which also means that for an input set of one point, your function
should return a circle with that point as its center and a zero
radius.

Interesting, the numbering is continuing from Ruby Q. ?
Not starting at 1???

SUGGESTION: I don’t have time to do this quiz, but it looks like fun.
I for one would encourage anybody to do it with some visual
representation! (suggested libs: RMagick, Gosu, Rubygame, Ruby/SDL,
etc…)
An audio representation could be interesting as well (perhaps a sort
of sonar?)

On Feb 16, 2008 3:21 PM, John J. [email protected]
wrote:

Interesting, the numbering is continuing from Ruby Q. ?
Not starting at 1???

SUGGESTION: I don’t have time to do this quiz, but it looks like fun.

I also would love to do this one, but am on vacation :slight_smile:

I hope there’s someone out there that supplies a multidimensional
solution (something that gives the user a choice of the dimensional
space; 3-D, 4-D, etc).

I for one would encourage anybody to do it with some visual
representation! (suggested libs: RMagick, Gosu, Rubygame, Ruby/SDL,
etc…)
An audio representation could be interesting as well (perhaps a sort
of sonar?)

Those would be pretty interesting.

Todd

On Feb 15, 3:19 pm, Matthew D Moss [email protected] wrote:

Start with the following base code and extend as needed to solve the
problem:

 class Point < Struct.new(:x, :y)
     def self.random
         Point.new(rand, rand)
     end

May I propose a bit refined random method?

 class Point < Struct.new(:x, :y)
     def self.random
         Point.new(0.5 - rand, 0.5 - rand)
     end

This should give us negative coordinates as well. :slight_smile:

From: “John J.” [email protected]

SUGGESTION: I don’t have time to do this quiz, but it looks like fun.
I for one would encourage anybody to do it with some visual
representation! (suggested libs: RMagick, Gosu, Rubygame, Ruby/SDL,
etc…)

Here’s an OpenGL/GLUT-based visualizer for the quiz:

http://tastyspleen.net/~billk/ruby/quiz/157-smallest-circle/157_smallest_circle_visualization.rb

The visualizer is designed to run with any quiz solution that
provides the encircle() and generate_samples(n) methods at
outer scope.

A screenshot for those curious, but not curious enough to actually
try installing ruby OpenGL bindings …

NOTE: I’ve only tried this program on ruby 1.8.4 so far… I have
no idea if it might run on 1.9 …

Also… I believe I’m using yoshi’s opengl-0.32f bindings. I haven’t
tried it with the very latest ruby/OpenGL bindings on rubyforge.

Regards,

Bill

On Feb 17, 8:59 am, Bill K. [email protected] wrote:

http://tastyspleen.net/~billk/ruby/quiz/157-smallest-circle/157_small

I’m trying to utilize gnuplot for visualization. With any luck I can
post the visual part without spoiling the quiz. :wink:

Anyone interested?

May I propose a bit refined random method?

 class Point < Struct.new(:x, :y)
     def self.random
         Point.new(0.5 - rand, 0.5 - rand)
     end

This should give us negative coordinates as well. :slight_smile:

Sure, but your algorithm should work with any set of random points,
whether they fit in the unit square from 0 to 1, -0.5 to 0.5, or
coordinates outside the unit square.

Hi,

here’s my solution of this nice quiz. It’s heavily inspired by the last
week’s quiz, i.e. solving a non-linear equation. This time the program
solves the non-smooth convex optimization problem

min{max{|p_i-x|^2: p_i the given points}: x in R^k},

where k=1,2,3,… (dimension), using a simple subgradient algorithm.
Therefore, it works for an arbitrary number of points
(up to 10000 in about 40s on my 5 year old notebook) and an
arbitrary dimension (number of coordinates) of the points. The solutions
are usually a good approximation of the optimal ones.

================================================================

extends point-class with an arbitrary dimension

and simple vector-operations

class Point
def initialize( *coords )
@coords = coords.map{|x| x.to_f}
end

def size
    @coords.size
end

def []( index )
    @coords[index]
end

def []= ( index, x )
    @coords[index] = x
end

def self.random( n = 2 )
    Point.new( *(1..n).map{ 0.5 - rand } )
end

def +(p)
    return Point.new( *(0...size).map{|i| @coords[i] + p[i]} )
end

def -(p)
    return Point.new( *(0...size).map{|i| @coords[i] - p[i]} )
end

def *(a)
    return Point.new( *@coords.map{|x| x * a} )
end

def /(a)
    return Point.new( *@coords.map{|x| x / a} )
end

# calculate the inner product if this point with p
def ip(p)
    (0...size).inject(0) { |sum, i| sum + @coords[i] * p[i] }
end

def to_s
    "(#{@coords.join(', ')})"
end

end

class Circle < Struct.new(:center, :radius)
def to_s
“{center:#{center}, radius:#{radius}}”
end
end

def generate_samples(n, dim = 2)
(1…n).map { Point.random(dim) }
end

calculate value and subgradient of

f(x,y) = max{(x-x_i)^2 + (y-y_i)^2: (x_i,y_i) in points}

def evaluate( m, points )
y_big = nil
grad = nil
points.each do |p|
d = (m - p)
y = d.ip( d ) # y = sum (m_i-p_i)^2
if y_big.nil? or y > y_big
y_big = y
grad = d*2
end
end

return [y_big, grad]

end

perform simple subgradient algorithm

with given starting-point and number of iterations

def encircle( points,
x_start = Point.new( *([0]*points[0].size) ),
max_iter = 100 )
x = x_start
y, g = nil, nil

for k in 1..max_iter do
    y, g = evaluate( x, points )
    x = x - g/k
end

return Circle.new(x, Math.sqrt(y))

end

puts encircle( generate_samples(10000, 3) )

Bye,
Frank

Mine is here:

http://www.pastie.org/153343

Lifted mostly from Welcome to the Minimal Enclosing Circle Emporium.
Classic Convex Hull based solution that seems to scale O(n).

-Doug Seifert

On 2008-02-17, Justin E. [email protected] wrote:

Hello,

Had a lot of fun with this one :slight_smile: My solution works by taking the average of
all points as the center, and the farthest outlying point as the radius. An
optimal solution is then found by moving the circle center towards the
outlier as the radius is reduced, while all points still are within the
radius. The same principles could be applied for higher dimensions as well.

Nice solution, this was also one of my first ideas. The problem with
this approach is if many points are close to each other and only few
points are far away. In this case the center is near that mass of points
and the line-search may fail to find an optimal solution. For example:

puts encircle( ([[0.0,0.0]] * 100 + [[0.0,1.0], [1.0,0.0]]).map{|p|
Point.new(*p)})

(100 points at (0,0), one at (1,0) and one at (0,1)) gives a solution of
{center:(0.00980392156862745, 0.00980392156862745),
radius:0.990244611507173}
but optimal would be (1/2, 1/2) and radius 1/sqrt(2).

Frank

Hello,

Had a lot of fun with this one :slight_smile: My solution works by taking the
average of
all points as the center, and the farthest outlying point as the radius.
An
optimal solution is then found by moving the circle center towards the
outlier as the radius is reduced, while all points still are within the
radius. The same principles could be applied for higher dimensions as
well.

class Point < Struct.new(:x, :y)
def self.random
Point.new(rand, rand)
end

def to_s
    "(#{x}, #{y})"
end

end

class Circle < Struct.new(:center, :radius)
def to_s
“{center:#{center}, radius:#{radius}}”
end
end

Calculate distance between points

def distance(pt_a, pt_b)
Math.sqrt((pt_a.x - pt_b.x) * (pt_a.x - pt_b.x) +
(pt_a.y - pt_b.y) * (pt_a.y - pt_b.y))
end

Determine if given points are all within a circle

def inside_circle?(points, circle)
for point in points
dist = distance(point, circle.center)
return false if dist > circle.radius
end
true
end

def encircle(points) # takes array of Point objects

find the average midpoint of all points

mid = Point.new(
points.inject(0){|sum, pt| sum += pt.x} / (points.size * 1.0),
points.inject(0){|sum, pt| sum += pt.y} / (points.size * 1.0))

sort points by longest distance from midpoint

longest point (index 0) is the initial radius

points.sort!{|a,b| distance(mid, a) <=> distance(mid, b) }.reverse!

Taking the average midpoint does not necessarily work because the

points
may

be weighted more heavily to one side. We correct for this by sliding

the
circle

along the line from its original center to the outmost point,

decreasing
the

radius as we go. Keep doing this until the circle can be made no

smaller.
point = points[0]
slope = (mid.y - point.y) / (mid.x - point.x)
new_pt, delta, sign = mid, 0.0, 1.0
sign = -1.0 if mid.x > point.x
while inside_circle?(points, Circle.new(new_pt, distance(new_pt,
point)))
mid = new_pt
delta += 0.000001 * sign
new_pt = Point.new(mid.x + delta, mid.y + (slope * (delta)))
end

return Circle.new(mid, distance(mid, point))
end

def generate_samples(n)
(1…n).map { Point.random }
end

Here are Pasties of the program and a RMagick script to generate images:

Algorithm: Parked at Loopia
Graphics code: Parked at Loopia
Images:
http://justin.ethier.googlepages.com/157_jae_25_2.jpg
http://justin.ethier.googlepages.com/157_jae_100_0.jpg
http://justin.ethier.googlepages.com/157_jae_1000_3.jpg

Thanks,

Justin

Greetings,

Not sure if my solution is worth posting… I took a naive
axis-aligned bounding box approach.

http://tastyspleen.net/~billk/ruby/quiz/157-smallest-circle/157_naive_aabb.rb

I knew it would be inaccurate, but I was curious how bad it
would be, so I wrote an OpenGL/GLUT based visualizer:

http://tastyspleen.net/~billk/ruby/quiz/157-smallest-circle/157_smallest_circle_visualization.rb

(The visualizer will work with any quiz solution that
provides the encircle() and generate_samples(n) methods,
and Point#x and Point#y.)

Occasionally the naive approach lucks out with a dataset
that allows it to look good:

But more often it looks something like this:

I had planned to improve my solution by taking an approach
similar to Justin E.'s. . . . By iterating and “moving the
circle center towards the outlier as the radius is reduced,
while all points still are within the radius.”

I may still code that if I have time… but then again, it’s
already been done. :slight_smile:

Regards,

Bill

And again I reply to my own post. The test code requires a distance()
method:

def distance(p1, p2)
Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
end

In case somebody cares.

Had a lot of fun with this one :slight_smile: My solution works by taking the average of
all points as the center, and the farthest outlying point as the radius. An
optimal solution is then found by moving the circle center towards the
outlier as the radius is reduced

Well, I didn’t have the time to check out how to do this the right way
(as Douglas did it). I nevertheless tried several approaches. This one
has some imprecision built-in but it turns out (in comparison with
Douglas’ solution) that for random point sets, it performs more or
less well (for an incorrect semi-solution).

I also attach some code to run several solution against each other,
which is fun if you exclude the precise solutions. This requires the
script lib from Script.

Cool quiz BTW that made me refresh my knowledge on triangles etc. I
wish a had had more time so that I could also read up on appropriate
algorithms.

Regards,
Thomas.

My semi-solution that uses a similar approach as Justin:

def encircle(points) # takes array of Point objects
points = points.uniq
return if points.nil? or points.empty?

x  = points.inject(0.0) {|a, p| a += p.x} / points.size
y  = points.inject(0.0) {|a, p| a += p.y} / points.size
a  = Point.new(x, y)
return Circle.new(a, 0) unless points.size > 1

f  = points.sort_by {|p| distance(a, p)}[-1]
df = distance(f, a)
b  = med(f, a)
e  = 1e-12
1000.times do
    db = distance(f, b)
    if points.all? {|p| distance(b, p) <= db + e}
        da = distance(f, a)
        if (da - db).abs < e
            return Circle.new(b, db)
        else
            a, b = b, med(f, b)
        end
    else
        b = med(a, b)
    end
end
raise RuntimeError

end

def med(a, b)
Point.new((b.x - a.x) / 2 + a.x, (b.y - a.y) / 2 + a.y)
end

def distance(p1, p2)
Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
end

The code to run the tests:

#!/usr/bin/env ruby19

require ‘script’

files = [
‘s_tml.rb’,
‘s_frank.rb’,
‘s_justin.rb’,
‘s_douglas.rb’,
]

def generate_samples(n)
(1…n).map { [rand - 0.5, rand - 0.5] }
end

Numer of samples

N = 10

N = 50

N = 100

N = 200

Tolerance

E = 1e-10

Your favourite solution, you might want to change this

F = ‘s_tml.rb’

sets = Array.new(N) {|i| generate_samples(i + 2)}
solutions = Hash.new {|h, k| h[k] = {}}

Best choice per sample

winners = Hash.new {|h,k| h[k] = 0}

Number of invalid solutions

losers = Hash.new {|h,k| h[k] = 0}

puts
files.each do |f|
print f
script = Script.load(f)
sets.each_with_index do |ps, i|
if i % 10 == 0
print ‘.’; STDOUT.flush
end
ps = ps.map {|p| script::Point.new(*p)}
v = script.encircle(ps)
if ps.any? {|p| distance(v.center, p) > v.radius + E}
losers[f] += 1
print ‘x’; STDOUT.flush
v.radius += 1000.0
end
solutions[i][f] = v
end
puts
end
puts

f_dist = 0

puts " %s" % files.map {|f| ‘%10s’ % f}.join(’ ‘)
solutions.each do |i, s|
winner = files.sort_by {|f| s[f] ? s[f].radius : 100000000}[0]
winners[winner] += 1
f_d = (s[F].radius - s[winner].radius).abs
f_dist = f_d if f_d > f_dist
puts ‘%5d %10s %s (%6.4f)’ % [
i,
‘(%s)’ % winner,
files.map {|f| s[f] && ‘%10.4f’ % s[f].radius}.join(’ '),
f_d,
]
end
puts

puts “Winners (best circle):”
winners.each do |f, n|
puts ‘%10s %d’ % [f, n]
end
puts

puts “Losers (invalid solutions):”
losers.each do |f, n|
puts ‘%10s %d’ % [f, n]
end
puts

puts “Favourite vs Winner:”
puts ‘Max distance: %6.4f’ % f_dist
puts

Alex S. wrote:

http://tastyspleen.net/~billk/ruby/quiz/157-smallest-circle/157_small

I’m trying to utilize gnuplot for visualization. With any luck I can
post the visual part without spoiling the quiz. :wink:

Glad to see that I’m not the sole old-school minded :slight_smile:

Lionel

Hi,

here’s my solution. I initially thought that the best circle would be
either defined by a diameter (2 points) or one combinations of 3 points
through which it passes. The idea was that given three points defining a
circle a fourth point is either in the circle or defines a circle with 2
from the 3 originals that includes the remaining point.

This would be neat as I wasn’t so happy about a gradient walking
solution. In this case it can be proven that you can deterministically
get the best solution by gradient walking (I don’t think there are local
maximums) but it’s not the general case. Unfortunately it can be proven
that such a circle can encircle all points, but not that it’s the
smallest (and my experiments proved that it isn’t). Finally this is even
slower than my proposed solution below, …

So here’s a more brute force method : minimizing the maximum distance
with some optimizations (heuristic for initial values and a not so naive
2D-space walker). There’s probably some more optimizations for walking
the gradient but I’m happy with the current speed (~10s for 10000 points
on my 1.06GHz Core Duo).

This seems similar in spirit to Frank’s solution, but less clean from
the Math PoV and more optimized for speed.

It outputs the solution and creates a gnuplot plotting file for you to
verify (you have the path used by the algorithm to find the center). I’m
no Gnuplot expert so unfortunately fancy colors are out and crappy
legends in…

Good reading,

Lionel

=============================================
include Math

Uses 10 points by default

POINT_COUNT = (ARGV[0].to_i == 0) ? 10 : ARGV[0].to_i

class Point < Struct.new(:x, :y)
def self.random
Point.new(rand, rand)
end

def self.generate_samples(n)
(1…n).map { self.random }
end

def to_s
“(#{x}, #{y})”
end
end

class PotentialCenter < Struct.new(:x, :y, :points)

Square distance with a point

def square_distance(point)
((point.x - x) ** 2) + ((point.y - y) ** 2)
end

Maximum square distance with my points

It’s cached because it’s called several times and can be very costly

def max_square_distance
@sq_dist ||= (points.map { |point| square_distance(point) }.max)
end

Compute the best move with given distance (ro)

and angles (thetas)

returns nil if none is better than the current position

def best_move(ro, thetas)
potential_moves = thetas.map { |theta|
new_x = x + (ro * cos(theta))
new_y = y + (ro * sin(theta))
PotentialCenter.new(new_x,new_y,points)
}
choose_move_1(potential_moves)
end

Dumber, faster

def choose_move_1(potential_moves)
potential_moves.detect { |move|
max_square_distance > move.max_square_distance
}
end

Look for the shortest path, slower (~1.4x on average with many

points)
def choose_move_2(potential_moves)
potential_moves.reject! { |move|
max_square_distance <= move.max_square_distance
}
potential_moves.min { |c1,c2|
c1.max_square_distance <=> c2.max_square_distance
}
end

Check that the given offset is big enough to move

the current position

(floating point rounding errors prevent infinite precision…)

def can_be_moved_by(offset)
((x + offset) != x) || ((y + offset) != y)
end

def to_s
“(#{x}, #{y})”
end
end

class Circle < Struct.new(:center, :radius)
def to_s
“{center:#{center}, radius:#{radius}}”
end
end

def chose_original_center_and_move_amount(points)

Start with a good potential (around the middle of the point cloud)

max_x = points.map{|p| p.x}.max
min_x = points.map{|p| p.x}.min
x = (max_x + min_x) / 2
max_y = points.map{|p| p.y}.max
min_y = points.map{|p| p.y}.min
y = (max_y + min_y) / 2
center = PotentialCenter.new(x, y, points)

The original displacement value is based on the fact that a truly

random distribution gets a mean value with a precision of this

order.

The actual center is probably reachable with a few of these steps

move_amount = ((max_y - min_y) + (max_x - min_x)) / points.size
return [center, move_amount]
end

Look for the best center by minimizing

its maximum distance with the given points

This is the same as minimizing its square

(the maximum of the square distances)

This can be seen as a dichotomy on a 2-dimensional space

def encircle(points)
center, ro = chose_original_center_and_move_amount(points)

We’ll move with polar coordinates (distance + angle : ro/theta)

How many directions do we follow?

each new angle creates a new (costly) max_square_distance call

so we take the minimum needed to move around and climb the

square_distance gradient: 3

angle_count = 3
thetas = (1…angle_count).map { |c| (2 * c * Math::PI) / angle_count}
iter = 0

Trace our moves

center_list = []

loop do
center_list << center
iter += 1
puts “#{iter}: #{center.max_square_distance}”
# Is there a best move available ?
if new_center = center.best_move(ro, thetas)
center = new_center
else
ro /= 2
# We stop when ro becomes too small to move the center
break unless center.can_be_moved_by(ro)
end
end
puts “Circle found in #{iter} iterations”
return [ Circle.new(center, sqrt(center.max_square_distance)),
center_list ]
end

points = Point.generate_samples(POINT_COUNT)
t = Time.now
circle, center_list = encircle(points)
puts circle
puts “Found in: #{Time.now - t}s”

Generate a GnuPlot command file to see the result

f = File.open(‘plotcommand.cmd’, ‘w’)
f.print <<EOF
set parametric
set size square
set xrange [-0.2:1.2]
set yrange [-0.2:1.2]
set multiplot
plot “-”
#{points.map { |p| “#{p.x} #{p.y}” }.join("\n")}
end
plot “-” with lines
#{center_list.map { |p| “#{p.x} #{p.y}” }.join("\n")}
end
plot [0:2pi]
(sin(t)
#{circle.radius})+#{circle.center.x},(cos(t)*#{circle.radius})+#{circle.center.y}
set nomultiplot
EOF
f.close

puts <<EOF
use ‘gnuplot -persist plotcommand.cmd’ to see
the circle, points and path to the center
EOF