The Sierpiński triangle

How can a shape lose all its area while its boundary grows without end? The Sierpiński triangle starts from a triangle, cuts out the middle triangle between the midpoints of its sides and does the same to each of the three triangles left, again and again. Each step keeps three quarters of the area and adds half to the boundary. After \(n\) steps, \(3^n\) triangles of side \(s / 2^n\) are left, with

\[A_n = \left(\tfrac{3}{4}\right)^n A_0, \qquad L_n = 3 \left(\tfrac{3}{2}\right)^n s,\]

so the limit has no area and an endless boundary. The Sierpiński carpet cuts the middle ninth out of squares instead and stays in one piece, full of holes.

The example cuts both with planex2d’s Boolean subtraction and checks the number of faces and holes, the areas and the boundary lengths against the formulas.

This page is part of a collection of geometry and design examples using planex2d.

The Sierpiński triangle after six steps and the Sierpiński carpet after four

[1]:
import math

import planex as px

Cutting out the middle

The example starts from an equilateral triangle with the side \(s = 100\). A step cuts the middle triangle out of every triangle at once, with one Boolean subtraction px.Subtract, and returns the corners of the three triangles left of each:

[3]:
s = 100
first = [((0, 0), (s, 0), (s / 2, s * math.sqrt(3) / 2))]
triangle = px.FromPolyline(first[0], closed=True).Face()


def middle(p, q):
    return ((p[0] + q[0]) / 2, (p[1] + q[1]) / 2)


def step(shape, corners):
    """cuts the middle triangle out of each triangle of corners; the corners of the triangles left"""
    middles, smaller = [], []
    for a, b, c in corners:
        ab, bc, ca = middle(a, b), middle(b, c), middle(c, a)
        middles.append(px.FromPolyline([ab, bc, ca], closed=True).Face())
        smaller += [(a, ab, ca), (ab, b, bc), (ca, bc, c)]
    return px.Subtract([shape] + middles), smaller

Area and boundary

After each step, the faces are counted and the area is compared with the triangle’s. The triangles left touch at their corners only, so they share no edges, and the boundary is the sum of their perimeters (Figure 1):

[4]:
shape, corners = triangle, first
steps = {}
for n in range(1, 7):
    shape, corners = step(shape, corners)
    steps[n] = shape
    boundary = sum(edge.mass for edge in shape.edges)
    print("n", n, "| faces", len(shape.faces), "3^n", 3**n, "| area", shape.mass / triangle.mass, "(3/4)^n", 0.75**n,
          "| boundary", boundary, "3 (3/2)^n s", 3 * 1.5**n * s)
# px.Plot(shape)
n 1 | faces 3 3^n 3 | area 0.75 (3/4)^n 0.75 | boundary 450.0 3 (3/2)^n s 450.0
n 2 | faces 9 3^n 9 | area 0.5624999999999999 (3/4)^n 0.5625 | boundary 675.0 3 (3/2)^n s 675.0
n 3 | faces 27 3^n 27 | area 0.42187499999999994 (3/4)^n 0.421875 | boundary 1012.5 3 (3/2)^n s 1012.5
n 4 | faces 81 3^n 81 | area 0.31640624999999983 (3/4)^n 0.31640625 | boundary 1518.75 3 (3/2)^n s 1518.75
n 5 | faces 243 3^n 243 | area 0.23730468749999964 (3/4)^n 0.2373046875 | boundary 2278.125 3 (3/2)^n s 2278.125
n 6 | faces 729 3^n 729 | area 0.1779785156249998 (3/4)^n 0.177978515625 | boundary 3417.1875 3 (3/2)^n s 3417.1875
After 1 step: 3 triangles
(a) After 1 step: 3 triangles
After 2 steps: 9 triangles
(b) After 2 steps: 9 triangles
After 4 steps: 81 triangles
(c) After 4 steps: 81 triangles
After 6 steps: 729 triangles
(d) After 6 steps: 729 triangles
Figure 1: The Sierpiński triangle step by step

After six steps, 729 triangles hold less than a fifth of the area, and the boundary is more than eleven times the triangle’s perimeter.

The carpet

The Sierpiński carpet does the same with squares. Each square is cut into nine and the middle one is taken out. The squares left share their sides, so the carpet stays one face, and every square taken out is a hole in it. After \(n\) steps it has \((8^n - 1)/7\) holes and \((8/9)^n\) of the area (Figure 2):

[6]:
def carpet_step(face, cells):
    """cuts the middle ninth out of each square (x, y, side) of cells; the squares left"""
    holes, smaller = [], []
    for x, y, w in cells:
        w3 = w / 3
        holes.append(px.QuickSketch().Move((x + w3, y + w3)).Rectangle(w3, w3).Face())
        smaller += [(x + i * w3, y + j * w3, w3) for i in range(3) for j in range(3) if (i, j) != (1, 1)]
    return px.Subtract([face] + holes), smaller


square = px.QuickSketch().Rectangle(s, s).Face()
carpet, cells = square, [(0, 0, s)]
for n in range(1, 5):
    carpet, cells = carpet_step(carpet, cells)
    print("n", n, "| faces", len(carpet.faces), "| holes", len(carpet.faces[0].wires) - 1, "(8^n - 1)/7",
          (8**n - 1) // 7, "| area", carpet.mass / square.mass, "(8/9)^n", (8 / 9)**n)
# px.Plot(carpet)
n 1 | faces 1 | holes 1 (8^n - 1)/7 1 | area 0.8888888888888888 (8/9)^n 0.8888888888888888
n 2 | faces 1 | holes 9 (8^n - 1)/7 9 | area 0.7901234567901234 (8/9)^n 0.7901234567901234
n 3 | faces 1 | holes 73 (8^n - 1)/7 73 | area 0.7023319615912215 (8/9)^n 0.7023319615912207
n 4 | faces 1 | holes 585 (8^n - 1)/7 585 | area 0.6242950769699701 (8/9)^n 0.624295076969974
After four steps: one face with 585 holes
Figure 2: The Sierpiński carpet

See Many faces for px.Subtract.