Penrose tiles are any of a set of plane figures which can be combined to tile the plane aperiodically (without translational symmetry). They were described by the British mathematician Roger Penrose in the 1970s.

The code described in this post is available under the GPL licence on github. Some examples are given in the next blog post.

One pair of Penrose tiles, known as P3 are the following rhombuses:

To ensure an aperiodic tiling, the tiles must be arranged so that the coloured arcs match up. For the purposes of constructing such a tiling, each rhombus may be considered to consist of a pair of mirror-image *Robinson triangles*:

The larger triangle on the left, referred to here as B_{L} has sides in the ratio $1:1:\phi$. The smaller one on the right, B_{S} has sides in the ratio $1:1:\psi$ where $\phi = (\sqrt{5}+1)/2$ and $\psi = 1/\phi = (\sqrt{5}-1)/2$ are the Golden ratio and its inverse respectively. To build a tiling one starts with an arrangement of such triangles and repeatedly applies the process of "inflation", replacing each triangle with the following arrangement of smaller triangles in the indicated orientation:

That is, a B_{L} triangle, ABC, is replaced with two scaled-down B_{L} triangles (EDA and CEB) and a B_{S} triangle (DEB); in the lefthand above figure, D divides AC in the ratio $1:(\phi-1)\equiv 1:\psi$ and E divides AB in the ratio $\psi:(1-\psi) \equiv \psi:\psi^2$. Each B_{S} triangle, A'B'C', is replaced with a B_{L} triangle (C'D'B') and a B_{S} triangle (D'C'A'); in the righthand figure, D' divides A'B' in the ratio $(1-\psi):\psi\equiv \psi^2:\psi$.

The code defines a class, `Penrose`

, which can be used to create SVG images of Penrose tilings. The base class, `RobinsonTriangle`

defines a generic Robinson triangle with its vertices given as complex numbers, $x + iy$ (note that the orientation of the SVG canvas (increasing $y$-index down the page) results in the $y$-axis appearing flipped in the resulting image). Methods are defined for producing the SVG paths for the rhombus associated with a Robinson triangle and for drawing the matching arcs if requested. We also define two more useful methods for returning the centre of the rhombus and for returning a "conjugate" triangle from the existing one with its $y$-coordinates inverted.

class RobinsonTriangle: """ A class representing a Robinson triangle and the rhombus formed from it. """ def __init__(self, A, B, C): """ Initialize the triangle with the ordered vertices. A and C are the vertices at the equal base angles; B is at the vertex angle. """ self.A, self.B, self.C = A, B, C def centre(self): """ Return the position of the centre of the rhombus formed from two triangles joined by their bases. """ return (self.A + self.C) / 2 def path(self): """ Return the SVG "d" path element specifier for the rhombus formed by this triangle and its mirror image joined along their bases. """ AB, BC = self.B - self.A, self.C - self.B xy = lambda v: (v.real, v.imag) return 'm{},{} l{},{} l{},{} l{},{}z'.format(*xy(self.A) + xy(AB) + xy(BC) + xy(-AB)) def get_arc_d(self, U, V, W): """ Return the SVG "d" path element specifier for the circular arc between sides UV and UW, joined at half-distance along these sides. """ start = (U + V) / 2 end = (U + W) / 2 # ensure we draw the arc for the angular component < 180 deg cross = lambda u, v: u.real*v.imag - u.imag*v.real US, UE = start - U, end - U if cross(US, UE) > 0: start, end = end, start # arc radius r = abs((V - U) / 2) return 'M {} {} A {} {} 0 0 0 {} {}'.format(start.real, start.imag, r, r, end.real, end.imag) def arcs(self): """ Return the SVG "d" path element specifiers for the two circular arcs about vertices A and C. """ D = self.A + self.C - self.B arc1_d = self.get_arc_d(self.A, self.B, D) arc2_d = self.get_arc_d(self.C, self.B, D) return arc1_d, arc2_d def conjugate(self): """ Return the vertices of the reflection of this triangle about the x-axis. Since the vertices are stored as complex numbers, we simply need the complex conjugate values of their values. """ return self.__class__(self.A.conjugate(), self.B.conjugate(), self.C.conjugate())

From this base class we derive a class for each of the two types of P3 triangle, `BtileL`

and `BtileS`

:

class BtileL(RobinsonTriangle): """ A class representing a "B_L" Penrose tile in the P3 tiling scheme as a "large" Robinson triangle (sides in ratio 1:1:phi). """ def inflate(self): """ "Inflate" this tile, returning the three resulting Robinson triangles in a list. """ # D and E divide sides AC and AB respectively D = psi2 * self.A + psi * self.C E = psi2 * self.A + psi * self.B # Take care to order the vertices here so as to get the right # orientation for the resulting triangles. return [BtileL(D, E, self.A), BtileS(E, D, self.B), BtileL(self.C, D, self.B)] class BtileS(RobinsonTriangle): """ A class representing a "B_S" Penrose tile in the P3 tiling scheme as a "small" Robinson triangle (sides in ratio 1:1:psi). """ def inflate(self): """ "Inflate" this tile, returning the two resulting Robinson triangles in a list. """ D = psi * self.A + psi2 * self.B return [BtileS(D, self.C, self.A), BtileL(self.C, D, self.B)]

The method `inflate`

simply returns a list of new triangles with vertices determined by the "inflation" process described above. Note that the orientation of these triangles is important: the vertices are given in the order A, B, C using the notation of the above figures.

Finally, the main `Penrose`

class is responsible for generating the tiling out of the P3 rhombuses. The appearance can be altered by passing key,value pairs to the `config`

dictionary on initialization.

class PenroseP3: """ A class representing the P3 Penrose tiling. """ def __init__(self, scale=200, ngen=4, config={}): """ Initialise the PenroseP3 instance with a scale determining the size of the final image and the number of generations, ngen, to inflate the initial triangles. Further configuration is provided through the key, value pairs of the optional config dictionary. """ self.scale = scale self.ngen = ngen # Default configuration self.config = {'stroke-colour': '#fff', 'base-stroke-width': 0.05, 'margin': 1.05, 'tile-opacity': 0.6, 'random-tile-colours': False, 'Stile-colour': '#08f', 'Ltile-colour': '#0035f3', 'Aarc-colour': '#f00', 'Carc-colour': '#00f', 'draw-tiles': True, 'draw-arcs': False, 'reflect-x': True } self.config.update(config) self.elements = [] def set_initial_tiles(self, tiles): self.elements = tiles def inflate(self): """ "Inflate" each triangle in the tiling ensemble.""" new_elements = [] for element in self.elements: new_elements.extend(element.inflate()) self.elements = new_elements def remove_dupes(self): """ Remove triangles giving rise to identical rhombuses from the ensemble. """ # Triangles give rise to identical rhombuses if these rhombuses have # the same centre. selements = sorted(self.elements, key=lambda e: (e.centre().real, e.centre().imag)) self.elements = [selements[0]] for i, element in enumerate(selements[1:], start=1): if abs(element.centre() - selements[i-1].centre()) > TOL: self.elements.append(element) def add_conjugate_elements(self): """ Extend the tiling by reflection about the x-axis. """ self.elements.extend([e.conjugate() for e in self.elements]) def make_tiling(self): """ Make the Penrose tiling by inflating ngen times. """ for gen in range(self.ngen): self.inflate() self.remove_dupes() if self.config['reflect-x']: self.add_conjugate_elements() self.remove_dupes() def get_tile_colour(self, e): if self.config['random-tile-colours']: return '#' + hex(random.randint(0,0xfff))[2:] if isinstance(e, BtileL): return self.config['Ltile-colour'] return self.config['Stile-colour'] def make_svg(self): """ Make and return the SVG for the tiling as a str. """ xmin = ymin = -self.scale * self.config['margin'] width = height = 2*self.scale * self.config['margin'] viewbox ='{} {} {} {}'.format(xmin, ymin, width, height) svg = ['<?xml version="1.0" encoding="utf-8"?>', '<svg width="100%" height="100%" viewBox="{}"' ' preserveAspectRatio="xMidYMid meet" version="1.1"' ' baseProfile="full" xmlns="http://www.w3.org/2000/svg">' .format(viewbox)] # The tiles' stroke widths scale with ngen stroke_width = str(psi**self.ngen * self.scale * self.config['base-stroke-width']) svg.append('<g style="stroke:{}; stroke-width: {};">' .format(self.config['stroke-colour'], stroke_width)) for e in self.elements: if self.config['draw-tiles']: svg.append('<path fill="{}" opacity="{}" d="{}"/>' .format(self.get_tile_colour(e), self.config['tile-opacity'], e.path())) if self.config['draw-arcs']: arc1_d, arc2_d = e.arcs() svg.append('<path fill="none" stroke="{}" d="{}"/>' .format(self.config['Aarc-colour'], arc1_d)) svg.append('<path fill="none" stroke="{}" d="{}"/>' .format(self.config['Carc-colour'], arc2_d)) svg.append('</g>\n</svg>') return '\n'.join(svg) def write_svg(self, filename): """ Make and write the SVG for the tiling to filename. """ svg = self.make_svg() with open(filename, 'w') as fo: fo.write(svg)

The method responsible for generating the tiling is `make_tiling`

and it simply inflates the initial configuration of triangles `ngen`

times. Since the rhombuses are generated from Robinson triangles, it is possible for the inflation of adjacent triangles to produce the same rhombus: the method `remove_dupes`

removes such duplicate rhombuses from the ensemble: two rhombuses are the same if they have the same centre (this is established using the `RobinsonTriangle`

`centre`

method).

Finally, a careful choice of initial conditions (such that the initial triangles all have edges along the $x$-axis) enables the tiling area to be doubled simply by reflection in the $x$-axis (equivalent to taking the complex conjugate of all triangles' vertex coordinates). This functionality is implemented with the method `add_conjugate_elements`

.

## Comments

There are currently no comments

## New Comment