Inscribing a polygon in a circle

Suppose we are given a list of positive numbers whose sum is finite. The list might consist of only finite many numbers or they might be the terms of a convergent infinite series. When and how can we inscribe a polygon (potentially, with infinitely many sides) whose side lengths are exactly those numbers?

Polygons

Let's start with finitely many sides. I guess the question is then, how and when can we inscribe a polygon with given side lengths $\{x_1,x_2,\ldots,x_n\}$ into a circle? If we can, what radius $r$ of the circle will work?

Clearly, the largest $x_k$ must be smaller than the sum of the remaining $x_ks$, for otherwise we can't even make a polygon with the required side lengths. Furthermore, the largest $x_k$ must be smaller than $2r$ to fit that side alone into the circle.

Here's another useful observation and notation: Applying the law of cosines to a chord of length $x$ that subtends an angle of measure $\theta$ in a circle of radius $r$ we get $$x^2 = 2r^2-2r^2\cos(\theta).$$

Solving for $\theta$, we get $$\theta = \arccos\left(\frac{2r^2-x^2}{2r^2}\right).$$ We say that the length $x$ subtends the angle $\theta$ in a circle of radius $r$.

Case 1: The polygon contains the center of the circle

In this case, the sides of the polygon must subtend a total angle of measure $2\pi$. This yields an equation that the side lengths $x_1,x_2,\ldots,x_n$ and the radius $r$ must satisfy: $$\sum_k \arccos\left(\frac{2r^2-x_k^2}{2r^2}\right) = 2\pi.$$ As the $x_ks$ are specified, we can find the value of $r$ that works, if there is one.

In code, we're looking for a root of the function s(r) below. Note that s is generally decreasing, so if s(max(side_lengths)/2) is negative, there's no point in trying.

In [1]:
import numpy as np
side_lengths = [6,3,3,2,3,4,3]
def x(k): return side_lengths[k]
def theta(r,k): return np.arccos((2*r**2 - x(k)**2)/(2*r**2))
def s(r): return sum([theta(r,k) for k in range(len(side_lengths))]) - 2*np.pi
s(max(side_lengths)/2)
Out[1]:
3.1863266825587733

Let's make a simple plot of $s$ to find an interval to pass to numerical rootfinder brentq.

In [2]:
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt

x_pts = []
y_pts = []
for r in np.linspace(max(side_lengths)/2,max(side_lengths),20):
    try:
        y = s(r)
        y = float(y)
        x_pts.append(r)
        y_pts.append(y)
    except:
        pass
plt.plot(x_pts,y_pts)
Out[2]:
[<matplotlib.lines.Line2D at 0x1076d5dd8>]

Looks like there's a root between 3.5 and 6.

In [3]:
from scipy.optimize import brentq
r_crit = brentq(s,3.5,6)
r_crit
Out[3]:
4.022837750868895

Let's build the circle!

In [4]:
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Circle

r = r_crit
th = 0
xs = [0]
ys = [-r]
for k in range(len(side_lengths)):
    th = th + theta(r,k)
    ys.append(-r*np.cos(th))
    xs.append(r*np.sin(th))
plt.plot(xs,ys, linewidth=1.5, color='black')
plt.plot(xs,ys, 'o', color='black')

ax = plt.gca()
ax.add_patch(Circle((0,0),r, fill=None, alpha=0.5))
ax.set_aspect(1)
ax.set_xlim((-1.1*r,1.1*r))
ax.set_ylim((-1.1*r,1.1*r))
fig = plt.gcf()
fig.set_figwidth(8)
fig.set_figheight(8)
plt.show()

Case 2: The polygon does not contain the center

This case relies on similar ideas, but the total angle subtended need not be $2\pi$. However, the angle subtended by the greatest side length must be the same as the angle subtended by the remaining sidelengths, as you'll see in the picture we generate soon.

Note that the code below assumes that the largest number occurs first in the list of side lengths.

In [5]:
import numpy as np
side_lengths = [8,3,3,2,1]
def x(k): return side_lengths[k]
def theta(r,k): return np.arccos((2*r**2 - x(k)**2)/(2*r**2))
def s(r): return sum([theta(r,k) for k in range(1,len(side_lengths))]) - theta(r,0)
s(max(side_lengths)/2)
Out[5]:
-0.84798938298694848

Again, a plot should help us locate a root.

In [6]:
x_pts = []
y_pts = []
for r in np.linspace(max(side_lengths)/2,2*max(side_lengths),20):
    try:
        y = s(r)
        y = float(y)
        x_pts.append(r)
        y_pts.append(y)
    except:
        pass
plt.plot(x_pts,y_pts)
plt.plot([x_pts[0],x_pts[len(x_pts)-1]],[0,0])
Out[6]:
[<matplotlib.lines.Line2D at 0x10878cbe0>]

Looks like it's around 5 - certainly in $[4.5,10]$.

In [7]:
from scipy.optimize import brentq
r_crit = brentq(s,4.5,10)
r_crit
Out[7]:
5.2448771814471895

Finally, we enjoy an illustration.

In [8]:
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Circle

r = r_crit
th = 0
xs = [0]
ys = [-r]
for k in range(len(side_lengths)):
    if k == 0:
        th = th + theta(r,k)
    else:
        th = th - theta(r,k)
    ys.append(-r*np.cos(th))
    xs.append(r*np.sin(th))
plt.plot(xs,ys, linewidth=1.5, color='black')
plt.plot(xs,ys, 'o', color='black')

ax = plt.gca()
ax.add_patch(Circle((0,0),r, fill=None, alpha=0.5))
ax.set_aspect(1)
ax.set_xlim((-1.1*r,1.1*r))
ax.set_ylim((-1.1*r,1.1*r))
fig = plt.gcf()
fig.set_figwidth(8)
fig.set_figheight(8)
plt.show()

Infinite series

The ideas above are largely applicable to polygons with infinitely many sides. The main computational complication is that need a way to approximate an infinite sum, as well as a numerical root finder. There is exactly such a tool in sympy's mpmath package.

Let's try it with the series $$\sum_{n=1}^{\infty} \left(\frac{3}{4}\right)^n.$$

In [9]:
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import sympy.mpmath as mp
def x(k): return 0.75**k
def theta(r,k): return mp.acos((2*r**2 - x(k)**2)/(2*r**2))
def s(r): return mp.sumap(lambda k: theta(r,k), [1,mp.inf]) - 2*mp.pi

x_pts = []
y_pts = []
for r in np.linspace(0,2,20):
    try:
        y = s(r)
        y = float(y)
        x_pts.append(r)
        y_pts.append(y)
    except:
        pass
plt.plot(x_pts,y_pts)
Out[9]:
[<matplotlib.lines.Line2D at 0x10b1bc0f0>]

Looks like there's a root around $0.5$ - let's find it!

In [10]:
from scipy.optimize import brentq
r_crit = brentq(s,0.4,1)
r_crit
Out[10]:
0.5020352914608361

Again, we'll plot the result.

In [11]:
from matplotlib.patches import Circle

r = r_crit
xs = [0]
ys = [-r]
th = 0
for k in range(1,50):
    th1 = th
    th = th + theta(r,k)
    ys.append(-r*mp.cos(th))
    xs.append(r*mp.sin(th))
plt.plot(xs,ys, linewidth=1.5, color='black')
plt.plot(xs,ys, 'o', color='black')


ax = plt.gca()
ax.add_patch(Circle((0,0),r, fill=None, alpha=0.5))
ax.set_aspect(1)
ax.set_xlim((-1.1*r,1.1*r))
ax.set_ylim((-1.1*r,1.1*r))
ax.axis('off');

fig = plt.gcf()
fig.set_frameon(False)
fig.set_figwidth(8)
fig.set_figheight(8)