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
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 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':
%%output size=250
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))))
squarelimit(3)