--- jupyter: description: 'The problem of Apollonius with planex2d: circles tangent to three given circles from a constraint sketch, the two of Descartes'' theorem for three circles that touch, and all eight for three separate circles.' --- # The problem of Apollonius Which circles touch three given circles? Apollonius of Perga posed the problem, and in general it has eight solutions, one for each choice of which of the three given circles the new circle encloses. For three circles that touch each other, two of the solutions are given by Descartes' theorem on their curvatures $k = 1/r$, $$k_4 = k_1 + k_2 + k_3 \pm 2\sqrt{k_1 k_2 + k_2 k_3 + k_3 k_1},$$ where a negative curvature stands for a circle that encloses the others. In a planex2d sketch, a circle with three tangency constraints is a problem of Apollonius, and the solver finds the solution near the drawn circle. ![The eight circles that touch three given circles](../_static/gallery/problem-of-apollonius.png){.result} ```python import math import numpy as np import planex as px ``` ```python nbsphinx="hidden" from docs_figures import Row ``` ## A circle tangent to three `touching` draws the given circles, fixed with their radii, and a free circle near `start`, then makes it tangent to each given circle. A tangency between two circles is a contact from outside or from inside, and the sketch takes the kind that the circle is nearest to when the constraint is added: ```python def touching(given, start, r): sk = px.Sketch() for k, (center, radius) in enumerate(given): sk.Circle(center, radius, id=f"given_{k}") sk.Constrain(f"given_{k}.center", px.Fix) sk.Constrain(f"given_{k}", px.Radius, radius) sk.Circle(start, r, id="touching") for k in range(len(given)): sk.Constrain("touching", f"given_{k}", px.Tangent) m = sk.Measure("touching") return sk, np.array(m["center"][:2]), m["radius"] ``` ## Descartes' circles Three circles of radii 1, 2 and 3 touch each other when their centers lie 3, 4 and 5 apart, at the corners of a right triangle. Descartes' theorem gives $k_4 = 11/6 \pm 2$: a small circle of radius $6/23$ in the gap between them and a large one of radius 6 around them, which a rough circle in the gap and one around all three find (Figure 1): ```python tangent_three = [((0, 0), 1), ((3, 0), 2), ((0, 4), 3)] k = [1 / r for _, r in tangent_three] root = 2 * math.sqrt(k[0] * k[1] + k[1] * k[2] + k[2] * k[0]) small, inner, r_inner = touching(tangent_three, (0.8, 0.9), 0.3) large, outer, r_outer = touching(tangent_three, (1, 1.5), 5.5) print("inner | radius", r_inner, "| Descartes", 1 / (sum(k) + root), "| center", inner) print("outer | radius", r_outer, "| Descartes", -1 / (sum(k) - root), "| center", outer) ``` ```python nbsphinx="hide-input" Row((small, "The inner circle"), (large, "The outer circle"), caption="Descartes' two circles around three that touch") ``` ## Eight circles Three separate circles have all eight solutions. Each starts from a rough circle near the one wanted, as a drawing would place it, and the solver takes it to the exact contact with all three (Figure 2): ```python given = [((0, 0), 1), ((6, 0), 1.5), ((2, 5), 0.8)] rough = {"all outside": ((2.5, 2), 2.5), "the first inside": ((1.5, 1), 3), "the second inside": ((4.5, 1.5), 3.5), "the third inside": ((2.5, 3), 3), "the first and second inside": ((3.5, 0), 4), "the first and third inside": ((1.5, 2), 3.5), "the second and third inside": ((4.5, 2.5), 4.5), "all inside": ((3.5, 1.5), 4.5)} eight = {} for name, (start, r) in rough.items(): eight[name], center, radius = touching(given, start, r) gaps = [abs(abs(np.linalg.norm(center - np.array(c)) - radius) - rk) for c, rk in given] print(name, "| center", center.round(4), "| radius", round(radius, 4), "| off the contacts by", max(gaps)) ``` ```python nbsphinx="hide-input" names = list(eight) Row(*[(eight[n], n.capitalize(), {"fill": False}) for n in names[:4]], more=True) Row(*[(eight[n], n.capitalize(), {"fill": False}) for n in names[4:]], start="e", caption="The eight circles that touch three given circles") ``` ```python nbsphinx="hidden" import sys sys.path.insert(0, "..") import matplotlib.pyplot as plt from docs_figures import FACE, INK, PLANEX from gallery import save_figure fig, ax = plt.subplots(figsize=(6, 4)) for sk in eight.values(): m = sk.Measure("touching") ax.add_patch(plt.Circle(m["center"][:2], m["radius"], fill=False, color=PLANEX, lw=1.6)) for c, r in given: ax.add_patch(plt.Circle(c, r, fill=True, fc=FACE, ec=INK, lw=2.2, zorder=3)) ax.set_xlim(-5, 12.5) ax.set_ylim(-4.6, 7.1) ax.set_aspect("equal") ax.axis("off") save_figure(fig, "problem-of-apollonius", bare=True) ```