Skip to content
The Lumenary

Math laboratory

Counting symmetries: every way to move the corners

Tries every permutation of the corners of regular polygons with 3 to 7 sides and of six quadrilaterals, keeping those that preserve all corner-to-corner distances, and sorts them into turns and flips. Regular n-gons give n turns and n lines of symmetry; the rectangle gives 2 and 2. Exhaustive for these shapes only.

Back to the labExact computationExhaustive finite check
Counting symmetries: every way to move the cornersPythonExact computationExhaustive finite checkComputationally tested
"""Counting the symmetries of shapes by trying every way to move the corners.

A symmetry of a polygon sends corners to corners and keeps every distance
between corners the same. So the program tries *every* way of sending the
corners to the corners (every permutation) and keeps those that preserve all
distances. Each one it keeps is a symmetry: a turn if the corners keep their
order round the shape, a flip (a line of symmetry) if the order is reversed.

Regular polygons: the distance between two corners depends only on how many
steps apart they are round the polygon, so we compare steps exactly.
Quadrilaterals: corners have whole-number coordinates, so squared distances
are exact whole numbers.

Runs unchanged in the browser (Pyodide) using only the standard library.
"""

import math
import textwrap
from itertools import combinations, permutations

#: Quadrilaterals with whole-number corners, listed in order round the shape.
QUADRILATERALS = {
    "square": [(1, 1), (-1, 1), (-1, -1), (1, -1)],
    "rectangle": [(2, 1), (-2, 1), (-2, -1), (2, -1)],
    "rhombus": [(0, 2), (-1, 0), (0, -2), (1, 0)],
    "kite": [(0, 3), (-1, 0), (0, -1), (1, 0)],
    "parallelogram": [(0, 0), (3, 0), (4, 1), (1, 1)],
    "isosceles trapezium": [(-2, 0), (2, 0), (1, 2), (-1, 2)],
}

def steps_apart(n, i, j):
    """How many steps apart corners i and j are, round a regular n-gon."""
    k = (i - j) % n
    return min(k, n - k)

def cross(o, a, b):
    """Positive if o, a, b go anticlockwise, negative if clockwise."""
    return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])

def sq_dist(p, q):
    """Squared distance between two points with whole-number coordinates."""
    return (p[0] - q[0]) ** 2 + (p[1] - q[1]) ** 2

def symmetries(n, dist, orientation):
    """All distance-preserving permutations, split into (turns, flips).

    `dist(i, j)` is the distance between corners i and j (or anything that
    determines it), and `orientation(i, j, k)` is +1 or -1 according to
    whether corners i, j, k go anticlockwise or clockwise.
    """
    turns, flips = [], []
    pairs = list(combinations(range(n), 2))
    for perm in permutations(range(n)):
        if all(dist(perm[i], perm[j]) == dist(i, j) for i, j in pairs):
            same = orientation(*perm[:3]) == orientation(0, 1, 2)
            (turns if same else flips).append(perm)
    return turns, flips

def regular_polygon_symmetries(n):
    """Turns and flips of a regular polygon with n corners."""
    def orientation(i, j, k):
        return 1 if (j - i) % n < (k - i) % n else -1
    return symmetries(n, lambda i, j: steps_apart(n, i, j), orientation)

def quadrilateral_symmetries(corners):
    """Turns and flips of a quadrilateral given by its corners."""
    def orientation(i, j, k):
        return 1 if cross(corners[i], corners[j], corners[k]) > 0 else -1
    return symmetries(len(corners),
                      lambda i, j: sq_dist(corners[i], corners[j]),
                      orientation)

def main():
    names = {3: "equilateral triangle", 4: "regular 4-gon (square)",
             5: "regular pentagon",
             6: "regular hexagon", 7: "regular heptagon"}
    print("Shape                   tried   symmetries   turns   flips")
    for n in range(3, 8):
        tried = math.factorial(n)
        turns, flips = regular_polygon_symmetries(n)
        print(f"{names[n]:<22} {tried:>6}   {len(turns) + len(flips):>10}"
              f"   {len(turns):>5}   {len(flips):>5}")
    for name, corners in QUADRILATERALS.items():
        turns, flips = quadrilateral_symmetries(corners)
        print(f"{name:<22} {24:>6}   {len(turns) + len(flips):>10}"
              f"   {len(turns):>5}   {len(flips):>5}")
    print()
    print("turns = order of rotational symmetry (the 'no move' counts);")
    print("flips = number of lines of symmetry.")
    print("A regular polygon with n sides: n turns and n lines of symmetry.")
    print("A rectangle has only 2 lines of symmetry: its diagonals are not.")
    print()
    print(textwrap.fill(
        "Epistemic status: exhaustive and exact for these eleven shapes - "
        "every way of moving the corners was tried. It is evidence for the "
        "rule about regular polygons, not a proof for every n; the lesson "
        "explains why a regular n-gon always has 2n symmetries.",
        width=78,
    ))

if __name__ == "__main__":
    main()
Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.

Used in