Why bees build hexagons
A honeycomb divides a comb into cells of equal size, and its walls are wax that the bees have to make. Of the three regular polygons that tile the plane, the triangle, the square and the hexagon, the hexagon needs the least wall for a cell of a given area. The honeycomb conjecture, proved by Thomas Hales, says that no division of the plane into cells of equal area does better, not even one with curved walls.
The example builds the three tilings from cells of unit area. Glued, each wall between two cells counts once, and the wall per cell must come out as half the cell’s perimeter, which is 2.280 for the triangle, 2 for the square and 1.861 for the hexagon. Round cells have the shortest wall for one cell alone, but they leave gaps, \(1 - \pi/(2\sqrt{3})\) of the plane when they are packed as tightly as possible.

[1]:
import math
import planex as px
Cells of unit area
A regular polygon with \(n\) sides and the area 1 has the perimeter \(P = 2\sqrt{n \tan(\pi/n)}\). The three cells:
the hexagon
Hexagon(af), with the width across flats af = \(\sqrt{2/\sqrt{3}}\);the square
Rectangle(1, 1);the triangle
Polygon(r, 3), its corners on a circle of radius \(r = s/\sqrt{3}\), for the side \(s = 2/3^{1/4}\).
Each is a face around the origin, the square with its corner there (Figure 1):
[3]:
af = math.sqrt(2 / math.sqrt(3))
s = 2 / 3**0.25
r = s / math.sqrt(3)
cells = {
"triangle": px.QuickSketch().Polygon(r, 3).Face(),
"square": px.QuickSketch().Rectangle(1, 1).Face(),
"hexagon": px.QuickSketch().Hexagon(af).Face(),
}
for (name, cell), n in zip(cells.items(), (3, 4, 6)):
print(name, "area", cell.mass, "perimeter", cell.outer.mass, "formula", 2 * math.sqrt(n * math.tan(math.pi / n)))
triangle area 1.0 perimeter 4.559014113909555 formula 4.559014113909554
square area 1.0 perimeter 4.0 formula 3.9999999999999996
hexagon area 1.0000000000000002 perimeter 3.722419436408399 formula 3.7224194364083982
(a) The triangle, area 1
(b) The square, area 1
(c) The hexagon, area 1
Tilings
tiling places \(n \times n\) copies of its cells on a lattice of two vectors \(a\) and \(b\), and px.Glue joins them into one shape whose cells keep their own faces and share their walls. The squares lie on a lattice of 1 along \(x\) and 1 along \(y\). The hexagons lie on one of af up and af at 30°, since a hexagon’s neighbors lie across its flats. The triangles come in pairs of the cell, pointing along \(x\), and a copy turned by 180° and moved by \(r\),
which points the other way and shares a side with it. The pairs lie on a lattice of \(s\) up and \(s\) at 30° (Figure 2):
[5]:
def tiling(cells, lattice, n):
"""n x n copies of the cells, the copy (i, j) moved by i a + j b, glued into one shape"""
(ax, ay), (bx, by) = lattice
return px.Glue([cell.Translated((i * ax + j * bx, i * ay + j * by))
for i in range(n) for j in range(n) for cell in cells])
c, d = math.cos(math.radians(30)), math.sin(math.radians(30))
lattices = {"triangle": ((0, s), (s * c, s * d)), "square": ((1, 0), (0, 1)), "hexagon": ((0, af), (af * c, af * d))}
pair = [cells["triangle"], cells["triangle"].Rotated((0, 0), 180).Translated((-r, 0))]
combs = {
"triangle": tiling(pair, lattices["triangle"], 12),
"square": tiling([cells["square"]], lattices["square"], 16),
"hexagon": tiling([cells["hexagon"]], lattices["hexagon"], 16),
}
for name, comb in combs.items():
print(name, len(comb.faces), "cells of area", round(min(f.mass for f in comb.faces), 12), "to",
round(max(f.mass for f in comb.faces), 12))
triangle 288 cells of area 1.0 to 1.0
square 256 cells of area 1.0 to 1.0
hexagon 256 cells of area 1.0 to 1.0
(a) 288 triangles
(b) 256 squares
(c) 256 hexagons
Every cell’s perimeter counts each wall it shares twice and each wall on the outline once, so the walls of a patch of \(N\) cells add up to \((N P + B)/2\), with \(B\) the length of the patch’s outline, the boundary of the cells united. Without the outline, which an endless tiling does not have, each cell has \(P/2\) of wall:
[7]:
for name, comb in combs.items():
N, P = len(comb.faces), cells[name].outer.mass
walls = comb.Measure()["length"]
outline = px.Unify(comb.faces).Measure()["length"]
print(name, "walls", walls, "(N P + B) / 2", (N * P + outline) / 2)
print(" per cell without the outline", (walls - outline / 2) / N, "P / 2", P / 2)
triangle walls 709.6865303985809 (N P + B) / 2 709.6865303985875
per cell without the outline 2.2795070569547544 P / 2 2.2795070569547775
square walls 544.0 (N P + B) / 2 544.0
per cell without the outline 2.0 P / 2 2.0
hexagon walls 515.5550919425705 (N P + B) / 2 515.5550919425632
per cell without the outline 1.861209718204228 P / 2 1.8612097182041996
The hexagons need about 7 % less wall than the squares and 18 % less than the triangles.
Round cells
A circle encloses its area with the shortest boundary of all. For the area 1, its perimeter is \(2\sqrt{\pi} = 3.545\), less than the hexagon’s 3.722. But circles share no wall, they only touch, and they leave gaps. Packed as tightly as possible, each touching six others, circles leave \(1 - \pi/(2\sqrt{3})\) of the plane empty, about 9.3 %. In rows and columns, they leave \(1 - \pi/4\), about 21.5 %. The check subtracts the circles inscribed in the hexagons and in the squares, tiled on the same lattices, from the cells (Figure 3):
[8]:
gaps = {}
for name, center, radius, exact in (("hexagon", (0, 0), af / 2, 1 - math.pi / (2 * math.sqrt(3))),
("square", (0.5, 0.5), 0.5, 1 - math.pi / 4)):
circles = tiling([px.Circle(center, radius).Face()], lattices[name], 16)
gaps[name] = combs[name] - circles
print(name, "packing: empty", gaps[name].mass / combs[name].mass, "exact", exact)
hexagon packing: empty 0.09310031788289204 exact 0.09310031788289108
square packing: empty 0.21460183660255389 exact 0.21460183660255172

(a) Circles in the hexagons, the gaps shaded

(b) Circles in the squares, the gaps shaded
Round cells waste room in the comb, and every cell needs a wall of its own. The hexagon shares each of its walls with a neighbor and fills the plane. See Faces, shapes and compounds for px.Glue.