1
$\begingroup$

I'm writing a computer program that needs to display circles in a particular way, but can't seem to figure out how to arrange them given the information I have available. The problem is as follows: (jump to the bottom to see a diagram)

  • There is one large "green" circle with radius $R_g$.
  • There is one smaller "blue" circle with radius $0 \leq R_b \leq G$.
  • There are $n \in \mathbb{Z}_{\geq 1}$ "red" circles, each with radius $0 \leq R_r \leq R_b$.
  • The blue and red circles must be placed on the inside circumference of the green circle.
  • The blue and red circles must touch each other in a specific way (see diagram).

The problem is, given the following:

  • $R_g$, the radius of the green circle,
  • $R_b$, the radius of the blue circle, and
  • $n$, the number of red circles

Calculate:

  • $R_r$, the radius of the red circles
  • $d$, the distance of the center of each of the red circles from the center of the green circle
  • $\theta_r$, the angle between each of the red circles, and
  • $\theta_b$, the angle between the blue circle and the nearest red circle.

I solved this problem when $R_b=R_r$ (when the radius of the blue circle is exactly the size of the red circles). In that case,

\begin{align*} R_r &= R_b && \text{(given)}\\ d &= \frac{R_g \cdot \sec(\frac{\pi}{2}-\frac{\pi}{n+1})}{\sec(\frac{\pi}{2}-\frac{\pi}{n+1})+1} && \text{solution in link} \\ &= \frac{R_g}{1+\sin(\frac{\pi}{1+n})} && \text{simplified solution} \\ \theta_b = \theta_r &= \frac{2\pi}{n+1} \end{align*}

A rather large diagram of this problem is here: diagram of problem with varying circle sizes and numbers

$\endgroup$
2
  • 1
    $\begingroup$ Try fixing $R_r$ and calculating $\theta_b$ and $\theta_r$. Then you can solve for $R_r$. $\endgroup$
    – 1Rock
    Feb 12 '21 at 2:01
  • $\begingroup$ Thanks, I'll give that a try. Will report back when I learn more. $\endgroup$ Feb 12 '21 at 3:52
3
$\begingroup$

Let $R_g$ be the radius of the green circumcircle, $R_b$ be the radius of the single blue circle, and there be $n \ge 1$ red circles. We wish to solve $\theta_r$, the angle between centers of each pair of consecutive red circles, and $R_r$, the radius of each of the red circles.

The radius of the centers of the red circles must be $R_g - R_b$. If we use a coordinate system where the origin is at the center of the green circle, the center of the blue circle is at $(R_b-R_g, 0)$.

  • If $n = 1$, then the one red circle has radius $R_r = R_g - R_b$ and is centered at $(R_b, 0)$.

  • If $n = 2$, then the two red circles have radius $R_r$ centered at $(x, \pm y)$, $$\left\lbrace\begin{aligned} R_r &= \displaystyle \frac{4 R_g R_b (R_g - R_b)}{(R_g + R_b)^2 } \\ x &= \displaystyle \frac{R_g ( 3 R_b - R_g )}{R_g + R_b} \\ y &= R_r \\ \theta_r &= 2 \operatorname{atan2}(y, x) = 2 \arctan\left(\frac{4 R_b (R_g - R_b)}{(3 R_b - R_g)(R_b + R_g)}\right)\\ \end{aligned}\right.$$

  • For $n \ge 3$, we need to solve $$\cos\left((n-1)\frac{\theta_r}{2}\right) - \frac{2 R_b}{R_g - R_b}\sin\left(\frac{\theta_r}{2}\right) + 1 = 0$$ for $\theta_r$; then $$R_r = \displaystyle R_g \frac{\sin\left(\frac{\theta_r}{2}\right)}{1 + \sin\left(\frac{\theta_r}{2}\right)}$$

    I recommend using a binary search in range $$0 \le \theta_r \le \frac{2 \pi}{n}$$as there is only one zero for $$f(\theta_r) = \cos\left((n-1)\frac{\theta_r}{2}\right) - \frac{2 R_b}{R_g - R_b}\sin\left(\frac{\theta_r}{2}\right) + 1$$ If $f(\theta_r) \lt 0$, $\theta_r$ is too large; if $f(\theta_r) \gt 0$, $\theta_r$ is too small.

    (The upper limit applies if $R_b = 0$, as then the red circles form a closed ring of circles.)

    For neighboring red circles to touch, $$R_r = (R_g - R_r) \sin\left(\frac{\theta_r}{2}\right) \quad \iff \quad R_r = R_g \frac{\sin(\theta_r/2)}{1 + \sin(\theta_r/2)}$$

Here is a verified Python implementation, that returns $R_r$ and $\theta_r$ as a tuple:

from math import pi, atan2, sin, cos

def find_r_theta(rG, rB, n):
    n = round(n)
    if n < 1:
        raise ValueError("N must be at least 1, %s given" % n)

    if n == 1:
        return (rG - rB, 0)

    if n == 2:
        rR = 4*rG*rB*(rG-rB)/(rG+rB)**2
        return (rR, 2*atan2(rR, rG*(3*rB-rG)/(rG+rB)))

    theta_max = 2*pi/n
    theta_min = 0
    ct = 0.5*(n-1)
    cs = 2.0*rB / (rG - rB)
    # 53 bits of precision
    for k in range(0, 53):
        theta = 0.5*(theta_min + theta_max)
        d = cos(ct*theta) - cs*sin(0.5*theta) + 1
        if d > 0.0:
            theta_min = theta
        elif d < 0.0:
            theta_max = theta
        else:
            break

    s = sin(0.5*theta)
    rR = rG*s/(s+1)

    # Abort if the blue radius is smaller than the red
    if rB < rR:
        raise ValueError("Blue radius (%f) smaller than the red radius (%d)" % (rB, rR))

    return (rG*s/(s+1), theta)

If you add the following code, you can run it via python3 this.py rG rB N out.svg, specifying the green circle radius, blue circle radius, and the number of red circles, and it'll create an SVG image, out.svg, for illustration and verification; you can view those in any browser.

if __name__ == '__main__':
    from sys import argv, stdout, stderr, exit
    from math import ceil

    if len(argv) < 4 or len(argv) > 5:
        stderr.write("\n")
        stderr.write("Usage: %s [ -h | --help ]\n" % argv[0])
        stderr.write("       %s GREEN-RADIUS BLUE-RADIUS RED-CIRCLE-COUNT [ OUT.SVG ]\n" % argv[0])
        stderr.write("\n")
        exit(1)

    rG = float(argv[1])
    rB = float(argv[2])
    n  = round(float(argv[3]))

    if len(argv) > 4:
        svg = open(argv[4], mode="w", encoding="UTF-8")
    else:
        svg = stdout

    rR, theta = find_r_theta(rG, rB, n)

    center = int(ceil(rG+2))
    svg.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n')
    svg.write('<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewbox="0 0 %d %d">\n' % (2*center, 2*center))
    svg.write('<rect x="0" y="0" width="%d" height="%d" fill="#ffffff" stroke="none" />\n' % (2*center, 2*center))
    svg.write('<circle cx="%.3f" cy="%.3f" r="%.3f" fill="none" stroke="#00cc00" />\n' % (center, center, rG))
    svg.write('<circle cx="%.3f" cy="%.3f" r="%.3f" fill="none" stroke="#0000ff" />\n' % (center-rG+rB, center, rB))
    if (n & 1):
        svg.write('<circle cx="%.3f" cy="%.3f" r="%.3f" fill="none" stroke="#ff0000" />\n' % (center+rG-rR, center, rR))
        for i in range(1, int((n+1)/2)):
            a = i * theta
            x = (rG - rR) * cos(a)
            y = (rG - rR) * sin(a)
            svg.write('<circle cx="%.3f" cy="%.3f" r="%.3f" fill="none" stroke="#ff0000" />\n' % (center+x, center+y, rR))
            svg.write('<circle cx="%.3f" cy="%.3f" r="%.3f" fill="none" stroke="#ff0000" />\n' % (center+x, center-y, rR))
    else:
        for i in range(0, int(n/2)):
            a = (i + 0.5) * theta
            x = (rG - rR) * cos(a)
            y = (rG - rR) * sin(a)
            svg.write('<circle cx="%.3f" cy="%.3f" r="%.3f" fill="none" stroke="#ff0000" />\n' % (center+x, center+y, rR))
            svg.write('<circle cx="%.3f" cy="%.3f" r="%.3f" fill="none" stroke="#ff0000" />\n' % (center+x, center-y, rR))
    svg.write('</svg>\n')
    svg.flush()
    if svg != stdout:
        svg.close()

How did I find the solution for $n \ge 3$?

Consider the following illustration: The green circle is centered at origin, with radius $R_g$: Circles with odd and even n The blue circle has radius $R_b$ and is centered at $(R_b - R_g, 0)$. Note that its center $x$ coordinate is actually $c = -R_g + R_b$: blue radius in from the leftmost point on the green circle.

Angle $\theta$ forms an isosceles triangle, with sides $R_g - R_r$ and base $2 R_r$. If we split it into two right triangles, where the hypotenuse length is $R_g - R_r$, short side length is $R_r$, and the angle opposite the short side $\theta/2$. Thus, $$\sin\left(\frac{\theta}{2}\right) = \frac{R_r}{R_g - R_r} \tag{1a}\label{G1a}$$ Solving this for $R_r$ yields $$R_r = R_g \frac{\sin\left(\frac{\theta}{2}\right)}{1 + \sin\left(\frac{\theta}{2}\right)} \tag{1b}\label{G1b}$$ This means that the red circle radius $R_r$ is defined by $\theta$, and $\theta$ is our only free variable. And only positive $\theta$ make any sense.

If there was no blue circle at all, then the $n$ red circles would cover the full circle: $n \theta = 2 \pi$. This gives us the possible range for $\theta$, $$0 \lt \theta \lt \frac{2 \pi}{n} \tag{2}\label{G2}$$

The topmost red circle is always at angle $$\theta_T = \frac{(n - 1) \theta}{2}$$ counterclockwise from right. Its center is at $(x, y)$, $$\left\lbrace \begin{aligned} x = (R_g - R_r) \cos \theta_T &= (R_g - R_r) \cos\left(\frac{(n - 1) \theta}{2}\right) \\ y = (R_g - R_r) \sin \theta_T &= (R_g - R_r) \sin\left(\frac{(n - 1) \theta}{2}\right) \\ \end{aligned} \right. \tag{3a}\label{G3a}$$ For the topmost red circle and the blue circle to touch, we need the distance between their centers to match the sum of their radiuses. Squaring the distances, we have $$(x - b)^2 + y^2 = (R_b + R_r)^2 \tag{3b}\label{G3b}$$ Next, we subtract the right side from the left side (so we get a function of form $f(\theta)$ whose root we need to find), substitute $x$, $y$, $b$, and $R_r$.

At this point, the expression starts to sprawl, and I for one switch to a Computer Algebra System. I suggest wxMaxima or SageMath; both free and available for all operating systems. In Maxima:

declare(R_g,real, R_b,real, R_r,real, theta,real, n,integer) $
R_r : R_g * sin(theta/2) / (1 + sin(theta/2)) $
x   : (R_g - R_r) * cos((n-1)*theta/2) $
y   : (R_g - R_r) * sin((n-1)*theta/2) $
b   : -R_g + R_b $
EQ  : (x-b)^2 + y^2 - (R_b + R_r)^2 = 0;

and after applying rational and trigonometric simplifications (trigsimp(ratsimp(EQ));), we get

(((2*R_g^2-2*R_b*R_g)*sin(theta/2)+2*R_g^2-2*R_b*R_g)*cos((n-1)*theta/2)
+(2*R_g^2-6*R_b*R_g)*sin(theta/2)
+4*R_b*R_g*cos(theta/2)^2+2*R_g^2-6*R_b*R_g) / (2*sin(theta/2) - cos(theta/2)^2 + 2) = 0

That divisor, $2\sin(\theta/2) - \cos(\theta/2)^2 + 2 \ge 1$ in this context, because we only do this for $n \ge 3$ and therefore $0 \lt \theta/2 \lt \pi/3$, per $\eqref{G2}$. So, we can just omit it, by multiplying the equation with it. There is also a common factor $R_g$, which we can divide out at the same time: trigsimp(ratsimp(% * (2*sin(theta/2) - cos(theta/2)^2 + 2) / R_g)); and we get

  ((2*R_g-2*R_b)*sin(theta/2)+2*R_g-2*R_b)*cos((n-1)*theta/2)
+  (2*R_g-6*R_b)*sin(theta/2)
+          4*R_b*cos(theta/2)^2
+    2*R_g-6*R_b = 0

Replacing $\cos(\theta)^2$ with $1 - \sin(\theta)^2$, we have

  ((2*R_g-2*R_b)*sin(theta/2)+2*R_g-2*R_b)*cos((n-1)*theta/2)
+  (2*R_g-6*R_b)*sin(theta/2)
+          4*R_b*(1 - sin(theta/2)^2)
+    2*R_g-6*R_b = 0;

and finally, if we ask Maxima to solve this for $\theta$, solve(%, theta);, it gives us two solutions:

[ theta = -%pi, cos((n-1)*theta/2) = -(2*R_b*sin(theta/2)-R_g+R_b)/(R_b-R_g) ]

The first one is garbage, so we grab the second one, cos((n-1)*theta/2) = -(2*R_b*sin(theta/2)-R_g+R_b)/(R_b-R_g);, and subtract right hand side from left hand side to get a nice function form: ratsimp(lhs(%) - rhs(%)) = 0;, so we get

((R_g-R_b)*cos((n-1)*theta/2) - 2*R_b*sin(theta/2) + R_g-R_b)/(R_g-R_b) = 0

i.e. $$\frac{(R_g - R_b) \cos\left(\frac{(n-1) \theta}{2}\right) - 2 R_b \sin\left(\frac{\theta}{2}\right) + R_g - R_b }{R_g - R_b} = 0$$ or, equivalently $$\cos\left(\frac{(n-1)\theta}{2}\right) - \frac{2 R_b}{R_g - R_b} \sin\left(\frac{\theta}{2}\right) + 1 = 0$$

$\endgroup$
9
  • $\begingroup$ Nice, but according to the diagram, the blue circle can't be smaller than the red circles. $\endgroup$
    – PM 2Ring
    Feb 12 '21 at 20:39
  • $\begingroup$ @PM2Ring: The blue circle radius is specified as a parameter. To exclude $R_r \gt R_b$, it's just two lines of Python: if (rR > rB): raise ValueError("Blue circles can't be smaller than the red circles"). Should I add it to the example code? $\endgroup$
    – Glärbo
    Feb 12 '21 at 20:43
  • $\begingroup$ I think you should, although it is only a minor adjustment. And I'm sure the OP could figure it out. ;) $\endgroup$
    – PM 2Ring
    Feb 12 '21 at 21:06
  • $\begingroup$ @PM2Ring: Done. And thanks for the suggestion! $\endgroup$
    – Glärbo
    Feb 12 '21 at 21:10
  • 1
    $\begingroup$ @MichaelMacLeod: I added an addendum describing exactly how I found the solution, using free computer algebra system Maxima (available for all OSes, really). I'm hoping that the illustration gives the general idea, and those really interested can follow the discovery step by step; and verify the logic and derivation for themselves, and maybe gain some Maxima skills too. $\endgroup$
    – Glärbo
    Feb 13 '21 at 3:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.