Circle Packing Using Stochastic Search

All Processing code for this article, along with images and animated GIFs, can be found on Github

In this post I’m going to explain the idea behind stochastic search and a few examples of how it can be applied to circle packing.

“Stochastic search” is just a fancy way of saying that we’re going to perform a random search until some condition is met. This is particularly useful for problems where a systematic approach is far too computationally expensive.

Now we’re interested in stochastic search within the context of generative art where the biggest application is in packing shapes. The basic algorithm for stochastic search looks like this:

Start with an initial state
while (some exit condition is not met) {
  Generate a random candidate solution
  if (It is a valid solution in the current state) {
    Great! We now have a new state for the next iteration.
  }
}

This might seem vague at this point, so let’s actually implement some examples in Processing so we can play around with it.

Circle Packing

We’re going to apply stochastic search to the problem of packing circles of different sizes into a specific area.

Let’s start with a simple Circle class. Each circle has an (x, y) coordinate that specifies its center, and a radius r.

class Circle {
  float x;
  float y;
  float r;

  Circle(float x, float y, float r) {
    this.x = x;
    this.y = y;
    this.r = r;
  }

  void draw() {
    noStroke();
    colorMode(HSB, 360, 100, 100);
    fill(280 + random(-10, 10), 100, 100);
    ellipse(x, y, 2*r, 2*r);
  }
};

In the above code, we define two methods for our Circle class: a constructor to initialize a new object and a draw() method that draws it onto the screen using a random color.

We then define an ArrayList of Circle objects that we’re going to add to every time we find a “valid” circle to place on the screen.

ArrayList<Circle> circles;

void setup() {
  size(500, 500);
  circles = new ArrayList<Circle>();
  background(255);
}

Now we get to the draw() function which serves as one iteration of our stochastic search loop. Recall that what we want to do is (i) generate a random candidate solution, (ii) check if it’s valid, and (iii) trigger some kind of exit condition so the sketch stops at some point.

Here is a simple version below:

int failed_tries = 0;

void draw() {
  Circle nc = new Circle(random(width), random(height), 16);
  if (isValidCircle(nc)) {
    nc.draw();
    circles.add(nc);
  } else {
    failed_tries++;
    if (failed_tries > 16 * 1024) {
      println("Done!");
      noLoop();
    }
  }
}

In the above code, we first create a Circle object at a random location on screen, with some fixed radius (16 pixels). We then check if this circle satisfies certain conditions that we want using a yet-undefined isValidCircle() function. If it turns out to satisfy all our conditions, and isValidCircle() returns true, we draw this circle to the screen and add it to the list of all valid circles (you’ll see in a bit why we need this array in the first place).

If, on the other hand, some condition wasn’t met and isValidCircle() returned false, we increment a counter (failed_tries, initially set to 0) specifying how many unsuccessful tries we’ve had. If the number of failed tries exceeds some threshold (in the above code, I arbitrarily picked 16 * 1024 tries), then we call the noLoop() function which ends the sketch.

Now, we of course still need to define our isValidCircle() method! What should go in this? Well, it depends on what you want! Let’s me give you some examples to show you what I mean.

Tightly Packing Circles

Suppose we want to tightly pack circles, and when I say tightly packed, I just mean that we allow circles to be touching, placed right next to each other.

So what should our isValidCircle() function look like for this? Here it is:

class Circle {
  ...
  boolean collides(Circle c) {
    if (dist(x, y, c.x, c.y) < (r + c.r))
      return true;
    return false;
  }
  ...
};

boolean isValidCircle(Circle new_circle) {
  for (Circle c : circles)
    if (new_circle.collides(c))
      return false;

  return true;
}

Let’s first look at the isValidCircle() function itself. Recall that the parameter new_circle that is provied as a parameter is the randomly-create circle in our draw() function. We iterate over every currently-valid circle (see why we need the circles list now?) and if any of them collides with our circle, we return false. Otherwise, if the loop completes without any problems, which means no current circle collides with the newly-created one, we return true.

Now let’s look at the collides() function we add to the Circle class. It takes as a parameter a circle, and checks if this circle collides with it. How do we check if two circles collide? It’s easy: if the distance between their centers is less than the sum of their radii, then they collide! We use Processing’s dist() function here.

And that’s it! Here’s how it looks, although it takes much longer to actually generate because of the failed tries. On my machine it took around 15 minutes! In general, the higher you set the threshold for failed_tries, the better a packing you get, but the longer it takes.

Packing Circles with Margins

Suppose we want to add some margins to separate circles by a small distance.

We can simply modify our collides() function to account for this! All we do is account for it in our distance calculation. We only consider a circle to collide with another if the distance between their centers is greater than the sum of their radii and a user-specified margin (in the MARGIN variable below).

int MARGIN = 10;

class Circle {
  ...
  boolean collides(Circle c) {
    if (dist(x, y, c.x, c.y) < (r + c.r + MARGIN))
      return true;
    return false;
  }
  ...
};

And here is the result. Note how the circles are no longer touching and are separated by a margin of at least 10 pixels!

Packing Circles of Different Size

We can also modify our loop to pack circles of different sizes!

We do this by reducing the radius of the randomly-created circle when the number of failed tries at a given radius exceeds some threshold. Here is an example loop I wrote for doing this.

float RADIUS_MAX = 16;
float RADIUS_MIN = 2;
float current_radius;
int failed_tries = 0;

void setup() {
  ...
  current_radius = RADIUS_MAX;
  ...
}

void draw() {
  Circle nc = new Circle(random(width), random(height), current_radius);
  if (isValidCircle(nc)) {
    ...
  } else {
    failed_tries++;

    if (failed_tries > 32 * 1024 / current_radius) {
      current_radius /= 2;
      failed_tries = 0;
      if (current_radius < RADIUS_MIN) {
        println("Done!");
        noLoop();
      }
    }
  }
}

In the above loop, the first change we make is that we create circles based on a radius stored in a current_radius variable (which is initialized to RADIUS_MAX in setup()).

Second we modify the exit condition of our loop. Recall that earlier we called noLoop() when the number of failed tried exceeded a threshold. Instead, now when we exceed a threshold, we reduce current_radius by half and reset failed_tries to zero.

Next, we change our threshold for failed_tries to be dependent on the current radius. Why? The reason is that we want more tries when the radius is smaller since the screen will already be packed with bigger circles, making it harder to find a valid circle.

Finally, we still need to finish the sketch at some point. In this case, we end the sketch if we hit our threshold of failed tries and the new, halved radius is less than a specified minimum radius (RADIUS_MIN).

And that’s it! We now have a circle packing algorithm that looks like this:

Packing Inside a Shape

The approach is also very flexible in that isValidCircle() can be modified to account for other conditions, such as whether it is inside a custom shape.

Here’s a simple version of isValidCircle() that additionally checks if it inside a radius of 200 pixels from the center of the screen (see the first two lines).

boolean isValidCircle(Circle nc) {
  if (dist(nc.x, nc.y, width/2, height/2) > 200)
    return false;

  for (Circle c : circles)
    if (nc.collides(c))
      return false;

  return true;
}

Of course, in order to improve this, we can also be a little smarter about how we generate our random circles, but I leave that to you as an exercise (read: I’m lazy!).

And here is the result:

Really, this can be extended to account for any condition you want. For example, you can check if the circle is inside a polygon (see here for Tyler Hobbs’ polygonContainsPoint() function)

Allowing Overlapping Circles

Here is another example showing the flexibility of this simple algorithm. We set the margin between circles to a negative number, therefore allowing the overlapping of circles.

int MARGIN = -5;

And here is the result!

I also did one with a much longer run to get the following image.

You can even try to modify the exit condition so that it stops the sketch when no more white pixels remain on the screen, although it would take significantly longer.

Next Steps

You can try implementing similar stuff for rectangles or arbitrary polygons. In a future post, we’ll also look at how we can speed this up significantly using spacial search with a QuadTree. Stay tuned!

In the meantime, you can subscribe to my list to stay informed on new posts! No spam, guaranteed.