Square Limit#
Square Limit#
The above image shows a famous woodcut by M.C. Escher called Square Limit composed of tesselating fish tiles. In this notebook, we will recreate this pattern using the HoloViews Spline
element.
The construction used here is that of Peter Henderson’s Functional Geometry paper and this notebook was inspired by Massimo Santini’s programming-with-escher notebook, itself inspired by Haskell and Julia implementations.
We start by importing HoloViews and NumPy and loading the extension:
import holoviews as hv
from holoviews import opts
import numpy as np
hv.extension('matplotlib')
This notebook makes extensive use of the Spline
element and we will want to keep equal aspects and suppress the axes:
opts.defaults(opts.Spline(xaxis=None, yaxis=None, aspect='equal', bgcolor='white', linewidth=0.8))
‘Square Limit’ is composed from the following fish pattern, over which we show the unit square:
spline=[(0.0,1.0),(0.08,0.98),(0.22,0.82),(0.29,0.72),(0.29,0.72),(0.3,0.64),(0.29,0.57),(0.3,0.5),
(0.3,0.5),(0.34,0.4),(0.43,0.32),(0.5,0.26),(0.5,0.26),(0.58,0.21),(0.66,0.22),(0.76,0.2),(0.76,0.2),
(0.82,0.12),(0.94,0.05),(1.0,0.0),(1.0,0.0),(0.9,0.03),(0.81,0.04),(0.76,0.05),(0.76,0.05),(0.69,0.04),
(0.62,0.04),(0.55,0.04),(0.55,0.04),(0.49,0.1),(0.4,0.17),(0.35,0.2),(0.35,0.2),(0.29,0.24),(0.19,0.28),
(0.14,0.31),(0.14,0.31),(0.09,0.35),(-0.03,0.43),(-0.05,0.72),(-0.05,0.72),(-0.04,0.82),(-0.02,0.95),(0.0,1.0),
(0.1,0.85),(0.14,0.82),(0.18,0.78),(0.18,0.75),(0.18,0.75),(0.16,0.74),(0.14,0.73),(0.12,0.73),(0.12,0.73),
(0.11,0.77),(0.11,0.81),(0.1,0.85),(0.05,0.82),(0.1,0.8),(0.08,0.74),(0.09,0.7),(0.09,0.7),(0.07,0.68),
(0.06,0.66),(0.04,0.67),(0.04,0.67),(0.04,0.73),(0.04,0.81),(0.05,0.82),(0.11,0.7),(0.16,0.56),(0.24,0.39),
(0.3,0.34),(0.3,0.34),(0.41,0.22),(0.62,0.16),(0.8,0.08),(0.23,0.8),(0.35,0.8),(0.44,0.78),(0.5,0.75),
(0.5,0.75),(0.5,0.67),(0.5,0.59),(0.5,0.51),(0.5,0.51),(0.46,0.47),(0.42,0.43),(0.38,0.39),(0.29,0.71),
(0.36,0.74),(0.43,0.73),(0.48,0.69),(0.34,0.61),(0.38,0.66),(0.44,0.64),(0.48,0.63),(0.34,0.51),(0.38,0.56),
(0.41,0.58),(0.48,0.57),(0.45,0.42),(0.46,0.4),(0.47,0.39),(0.48,0.39),(0.42,0.39),(0.43,0.36),(0.46,0.32),
(0.48,0.33),(0.25,0.26),(0.17,0.17),(0.08,0.09),(0.0,0.01),(0.0,0.01),(-0.08,0.09),(-0.17,0.18),(-0.25,0.26),
(-0.25,0.26),(-0.2,0.37),(-0.11,0.47),(-0.03,0.57),(-0.17,0.26),(-0.13,0.34),(-0.08,0.4),(-0.01,0.44),
(-0.12,0.21),(-0.07,0.29),(-0.02,0.34),(0.05,0.4),(-0.06,0.14),(-0.03,0.23),(0.03,0.28),(0.1,0.34),(-0.02,0.08),
(0.02,0.16),(0.09,0.23),(0.16,0.3)]
unitsquare = hv.Bounds((0,0,1,1))
fish = hv.Spline((spline, [1,4,4,4]*34)) # Cubic splines
fish * unitsquare
As you may expect, we will be applying a number of different geometric transforms to generate ‘Square Limit’. To do this we will use Affine2D
from matplotlib.transforms
and matplotlib.path.Path
(not to be confused with hv.Path
!).
from matplotlib.path import Path
from matplotlib.transforms import Affine2D
# Define some Affine2D transforms
rotT = Affine2D().rotate_deg(90).translate(1, 0)
rot45T = Affine2D().rotate_deg(45).scale(1. / np.sqrt(2.), 1. / np.sqrt(2.)).translate(1 / 2., 1 / 2.)
flipT = Affine2D().scale(-1, 1).translate(1, 0)
def combine(obj):
"Collapses overlays of Splines to allow transforms of compositions"
if not isinstance(obj, hv.Overlay): return obj
return hv.Spline((np.vstack([el.data[0] for el in obj.values()]),
np.hstack([el.data[1] for el in obj.values()])))
def T(spline, transform):
"Apply a transform to a spline or overlay of splines"
spline = combine(spline)
result = Path(spline.data[0], codes=spline.data[1]).transformed(transform)
return hv.Spline((result.vertices, result.codes))
# Some simple transform functions we will be using
def rot(el): return T(el,rotT)
def rot45(el): return T(el, rot45T)
def flip(el): return T(el, flipT)
Here we define three Affine2D
transforms (rotT
,rot45T
and flipT
), a function to collapse HoloViews Spline
overlays (built with the *
operator) in a single Spline
element, a generic transform function T
and the three convenience functions we will be using directly (rot
, rot45
and flip
). Respectively, these functions rotate the spline by \(90^o\), rotate the spline by \(45^o\) and flip the spline horizontally.
Here is a simple example of a possible tesselation:
fish * rot(rot(fish))
Next we need two functions, beside
and above
to place splines next to each other or one above the other, while compressing appropriately along the relevant axis:
def beside(spline1, spline2, n=1, m=1):
den = n + m
t1 = Affine2D().scale(n / den, 1)
t2 = Affine2D().scale(m / den, 1).translate(n / den, 0)
return combine(T(spline1, t1) * T(spline2, t2))
def above(spline1, spline2, n=1, m=1):
den = n + m
t1 = Affine2D().scale(1, n / den).translate(0, m / den)
t2 = Affine2D().scale(1, m / den)
return combine(T(spline1, t1) * T(spline2, t2))
beside(fish, fish)* unitsquare + above(fish,fish) * unitsquare
One import tile in ‘Square Limit’ is what we will call smallfish
which is our fish rotate by \(45^o\) then flipped:
smallfish = flip(rot45(fish))
smallfish * unitsquare
We can now build the two central tesselations that are necessary to build ‘Square Limit’ which we will call t
and u
respectively:
t = fish * smallfish * rot(rot(rot(smallfish)))
u = smallfish * rot(smallfish) * rot(rot(smallfish)) * rot(rot(rot(smallfish)))
t *unitsquare + u * unitsquare
We are now ready to define the two recursive functions that build the sides and corners of ‘Square Limit’ respectively. These recursive functions make use of quartet
which is used to compress four splines into a small 2x2 grid:
blank = hv.Spline(([(np.nan, np.nan)],[1])) # An empty Spline object useful for recursion
def quartet(p, q, r, s):
return above(beside(p, q), beside(r, s))
def side(n):
if n == 0:
return hv.Spline(([(np.nan, np.nan)],[1]))
else:
return quartet(side(n-1), side(n-1), rot(t), t)
def corner(n):
if n == 0:
return hv.Spline(([(np.nan, np.nan)],[1]))
else:
return quartet(corner(n-1), side(n-1), rot(side(n-1)), u)
corner(2) + side(2)
We now have a way of building the corners and sides of ‘Square Limit’. To do so, we will need one last function that will let us put the four corners and four sides in place together with the central tile:
def nonet(p, q, r, s, t, u, v, w, x):
return above(beside(p, beside(q, r), 1, 2),
above(beside(s, beside(t, u), 1, 2),
beside(v, beside(w, x), 1, 2)), 1, 2)
args = [fish]* 4 + [blank] + [fish] * 4
nonet(*args)
Here we see use nonet
to place eight of our fish around the edge of the square with a blank
in the middle. We can finally use nonet
together with our recursive corner
and side
functions to recreate ‘Square Limit’:
def squarelimit(n):
return nonet(corner(n), side(n), rot(rot(rot(corner(n)))),
rot(side(n)), u, rot(rot(rot(side(n)))),
rot(corner(n)), rot(rot(side(n))), rot(rot(corner(n))))
hv.output(squarelimit(3), size=250)