codex/past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2016/iii/paper-117-propellers.py

past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2016/iii/paper-117-propellers.py
"""Draw reflected seven-triangle propellers; PNG is written only to cwd.
Tested with Python 3.14.4, numpy 2.3.5 and matplotlib 3.10.7.
"""
from pathlib import Path
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon

PAIRINGS = [
    [[(0, 1), (2, 5)], [(0, 2), (4, 3)], [(0, 4), (1, 6)]],
    [[(0, 4), (2, 3)], [(0, 1), (4, 6)], [(0, 2), (1, 5)]],
]
COLOURS = ['#cc5533', '#2477ad', '#35854b']

def reflected_tiles(edges):
    tiles = {0: np.array([[0., 0.], [1.45, 0.], [.32, 1.18]])}
    while len(tiles) < 7:
        old_size = len(tiles)
        for colour, pairs in enumerate(edges):
            for i, j in pairs:
                if i not in tiles and j in tiles:
                    i, j = j, i
                if i in tiles and j not in tiles:
                    tile = tiles[i]
                    u, v = [tile[k] for k in range(3) if k != colour]
                    direction = (v - u) / np.linalg.norm(v - u)
                    delta = tile[colour] - u
                    reflected = tile.copy()
                    reflected[colour] = u + 2 * np.dot(delta, direction) * direction - delta
                    tiles[j] = reflected
        assert len(tiles) > old_size
    return tiles

def main():
    fig, axes = plt.subplots(1, 2, figsize=(10, 4.8), dpi=100, facecolor='white')
    for ax, pairings, name in zip(axes, PAIRINGS, ['Left propeller', 'Right propeller']):
        tiles = reflected_tiles(pairings)
        for i in range(7):
            tile = tiles[i]
            ax.add_patch(Polygon(tile, facecolor='#fff1bf' if i == 0 else '#eaf0f5', edgecolor='none'))
            for colour in range(3):
                u, v = [tile[k] for k in range(3) if k != colour]
                internal = any(i in pair for pair in pairings[colour])
                ax.plot([u[0], v[0]], [u[1], v[1]], color=COLOURS[colour], lw=1.8, ls='--' if internal else '-')
            xy = tile.mean(axis=0)
            ax.text(*xy, str(i), ha='center', va='center', fontsize=14, weight='bold')
        cloud = np.concatenate(list(tiles.values()))
        ax.set_xlim(cloud[:, 0].min() - .18, cloud[:, 0].max() + .18)
        ax.set_ylim(cloud[:, 1].min() - .18, cloud[:, 1].max() + .18)
        ax.set_aspect('equal')
        ax.axis('off')
        ax.set_title(name, fontsize=14)
    from matplotlib.lines import Line2D
    handles = [Line2D([0], [0], color=c, lw=2, label=f'Side {s}') for c, s in zip(COLOURS, 'abc')]
    fig.legend(handles=handles, loc='lower center', ncol=3, frameon=False)
    fig.subplots_adjust(left=.03, right=.98, top=.89, bottom=.10, wspace=.11)
    fig.savefig(Path.cwd() / 'paper-117-propellers.png', facecolor='white', transparent=False)
    plt.close(fig)

if __name__ == '__main__':
    main()