Generate Random Points in a Circle


7 min, 1327 words

Categories: Maths Python


circle-random

Motivation

I found this problem on Leetcode while going through Geometry problems. So my first intuition was the most simple one, which everyone would get upon reading the problem for the first time. Unfortunately, that didn't work out, and I had no idea why. So I went on to extend the program to visualize my solution. Check out the source code here.

Try it out with your own ideas before checking out the correct approach!

Problem Statement

So the problem requires you to write a function that generates random points that are evenly distributed inside a circle of a given radius.

Suppose you are given a circle of radius R and the the center of the circle is (x_center, y_center). Your objective is to write a function randPoint which returns [random_x, random_y] when invoked and is then used to generate N points inside the circle which are distributed evenly.

You can try solving and visualizing this in a programming language of your choice but I would recommend Python given the availability of the Matplotlib library. If using Python you can use this template if you want to use my program to visualize your solution.

Just edit the randPoint function with your logic to generate the coordinates. The plot function is already written and the source code can be found here.

class Solution:
    def __init__(self, radius: float, x_center: float, y_center: float):
        self.radius = radius
        self.x_center = x_center
        self.y_center = y_center

    def randPoint(self) -> list[float]:
        x = "Expression to generate x coordinate"
        y = "Expression to generate y coordinate"
        return [x, y]

    def plot(self, description):
      # Calls randPoint func and plots your solution
      pass

You can use the following code to instantiate this class and generate the plot:

radius = 5
s = Solution(radius, 0, 0)
s.plot("• Plot description")

After you are done writing the logic you can run the script. Before running make sure you install the dependencies.

pip3 install numpy matplotlib
python3 circle.py

This is what a uniform distribution looks like:

circle-random

Give this a try before heading over to the solution.

Solution

So after reading this problem, my naive first thought was to select a random angle θ between [0, 2π] and then choose a distance r between [0, R]. That would give a random point on the circle.

Here's an illustration of the approach:

circle-random

Let's code up this approach:

def randPoint(self) -> list[float]:
    r = random.uniform(0, self.radius)
    theta = 2 * math.pi * random.uniform(0, 1)
    x = self.x_center + r * math.cos(theta)
    y = self.y_center + r * math.sin(theta)
    return [x, y]

The position of a chosen point relative to the center of the circle is (rcos(θ),rsin(θ)). Here (x,y) is returned relative to the origin.

Now let's try to plot the points generated this way:

naive-approach

We can already make out that this distribution isn't uniform. But what went wrong?

The second sub-figure shows the distribution of points with radius. We can see that this distribution is uniform which means that the density of points is same for all r from [0, R]. This is why we got more points near the center as illustrated in this figure:

points-illustration

So this approach didn't work out, which made me think of a totally different approach.

Let's start with what we can generate randomly. So I looked up the Python documentation for the random module and came across a function of interest:

random.uniform(a, b): Return a random floating point number N such that a <= N <= b for a <= b and b <= N <= a for b < a.

So geometrically we can generate a random point on a line segment of length l using:

x = l * random.uniform(0, 1)

We can extend this to generate a point inside a square of length l just by adding a y dimension:

x = l * random.uniform(0, 1)
y = l * random.uniform(0, 1)

random-square

Now, how can we use this to solve the original problem?

Let's generate points inside a square that encloses the circle of radius R. The side of this square will be 2R. You can already sense where we are trying to go with this. So after we generate a random point inside the enclosing square, we just check if it lies inside the circle. If it does, well and good. Otherwise, we keep generating new points until we get a valid point.

Let's code this up:

def randPoint(self) -> list[float]:
    while True:
        rand_y = random.uniform(-self.radius, self.radius)
        rand_x = random.uniform(-self.radius, self.radius)
        if rand_x**2 + rand_y**2 <= self.radius**2:
            break
    x = self.x_center + rand_x
    y = self.y_center + rand_y
    return [x, y]

The code is self-explanatory, we keep generating points unless it lies inside the circle. We check if a point is inside a circle by checking if the distance of that point from the center is less than or equal to the radius.

Now you must be wondering why this works. The answer lies in the previous diagram itself, we can see that if we select any smaller area than the square, the points that are uniformly distributed inside the square will be uniformly distributed over the smaller area provided that point lies within the smaller area.

Let's see how the plot looks now:

rejection-sampling

Perfect!

When I first came up with this solution I thought this was just a hack, but turns out it is an established statistical concept known as rejection sampling.

Rejection sampling is a Monte Carlo algorithm to sample data from a sophisticated (“difficult to sample from”) distribution with the help of a proxy distribution.

Now back to the generated plot, we can notice that the number of points grows linearly with radius. This confirms our hypothesis that the number of points should scale linearly with the circumference 2πr.

Turns out we could have used this knowledge to define a distribution function using a method called inverse transform sampling. This requires an intermediate knowledge of statistics and is thus kept out of the scope of this article. But you can read more about this approach here.

The code for this approach looks like this:

def randPoint(self) -> list[float]:
    theta = random.uniform(0, 2 * math.pi)
    R = math.sqrt(random.uniform(0, self.radius**2))
    return [
        self.x_center + R * math.cos(theta),
        self.y_center + R * math.sin(theta),
    ]

We get the same uniform plot:

pdf-approach

Conclusion

We went over two different approaches on how to solve this problem. While the rejection sampling method was more intuitive, it is not as performant as the distribution function approach. Let me know which one you liked more. If you’ve got a comment to share, reach out to me on any social media platform or email.

If you found this article valuable, share it with your friends. Stay curious!