{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Square Limit\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The above image shows a famous woodcut by [M.C. Escher](https://en.wikipedia.org/wiki/M._C._Escher) called [Square Limit](https://www.wikiart.org/en/m-c-escher/square-limit) composed of tesselating fish tiles. In this notebook, we will recreate this pattern using the HoloViews ``Spline`` element.\n", "\n", "The construction used here is that of Peter Henderson's [Functional Geometry](https://eprints.soton.ac.uk/257577/1/funcgeo2.pdf) paper and this notebook was inspired by Massimo Santini's [programming-with-escher](https://mapio.github.io/programming-with-escher/) notebook, itself inspired by [Haskell](https://github.com/micahhahn/FunctionalGeometry) and [Julia](https://shashi.github.io/ijulia-notebooks/funcgeo/) implementations.\n", "\n", "We start by importing HoloViews and NumPy and loading the extension:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import holoviews as hv\n", "import numpy as np\n", "hv.extension('matplotlib')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This notebook makes extensive use of the ``Spline`` element and we will want to keep equal aspects and suppress the axes:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%opts Spline [xaxis=None yaxis=None aspect='equal' bgcolor='white'] (linewidth=0.8)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "'Square Limit' is composed from the following fish pattern, over which we show the unit square:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "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),\n", "(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),\n", "(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),\n", "(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),\n", "(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),\n", "(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),\n", "(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),\n", "(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),\n", "(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),\n", "(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),\n", "(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),\n", "(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),\n", "(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),\n", "(-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),\n", "(-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),\n", "(0.02,0.16),(0.09,0.23),(0.16,0.3)]\n", "\n", "unitsquare = hv.Bounds((0,0,1,1))\n", "fish = hv.Spline((spline, [1,4,4,4]*34)) # Cubic splines\n", "fish * unitsquare" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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``!)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from matplotlib.path import Path\n", "from matplotlib.transforms import Affine2D\n", "\n", "# Define some Affine2D transforms\n", "rotT = Affine2D().rotate_deg(90).translate(1, 0)\n", "rot45T = Affine2D().rotate_deg(45).scale(1. / np.sqrt(2.), 1. / np.sqrt(2.)).translate(1 / 2., 1 / 2.)\n", "flipT = Affine2D().scale(-1, 1).translate(1, 0)\n", "\n", "def combine(obj):\n", " \"Collapses overlays of Splines to allow transforms of compositions\"\n", " if not isinstance(obj, hv.Overlay): return obj\n", " return hv.Spline((np.vstack([el.data[0] for el in obj.values()]),\n", " np.hstack([el.data[1] for el in obj.values()])))\n", " \n", "def T(spline, transform):\n", " \"Apply a transform to a spline or overlay of splines\"\n", " spline = combine(spline) \n", " result = Path(spline.data[0], codes=spline.data[1]).transformed(transform)\n", " return hv.Spline((result.vertices, result.codes))\n", "\n", "# Some simple transform functions we will be using\n", "def rot(el): return T(el,rotT)\n", "def rot45(el): return T(el, rot45T)\n", "def flip(el): return T(el, flipT)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "Here is a simple example of a possible tesselation:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fish * rot(rot(fish))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def beside(spline1, spline2, n=1, m=1):\n", " den = n + m\n", " t1 = Affine2D().scale(n / den, 1)\n", " t2 = Affine2D().scale(m / den, 1).translate(n / den, 0)\n", " return combine(T(spline1, t1) * T(spline2, t2))\n", "\n", "def above(spline1, spline2, n=1, m=1):\n", " den = n + m\n", " t1 = Affine2D().scale(1, n / den).translate(0, m / den)\n", " t2 = Affine2D().scale(1, m / den)\n", " return combine(T(spline1, t1) * T(spline2, t2))\n", "\n", "beside(fish, fish)* unitsquare + above(fish,fish) * unitsquare" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "One import tile in 'Square Limit' is what we will call ``smallfish`` which is our fish rotate by $45^o$ then flipped:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smallfish = flip(rot45(fish))\n", "smallfish * unitsquare" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can now build the two central tesselations that are necessary to build 'Square Limit' which we will call ``t`` and ``u`` respectively:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "t = fish * smallfish * rot(rot(rot(smallfish)))\n", "u = smallfish * rot(smallfish) * rot(rot(smallfish)) * rot(rot(rot(smallfish)))\n", "t *unitsquare + u * unitsquare" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "blank = hv.Spline(([(np.nan, np.nan)],[1])) # An empty Spline object useful for recursion\n", "\n", "def quartet(p, q, r, s):\n", " return above(beside(p, q), beside(r, s))\n", "\n", "def side(n):\n", " if n == 0: \n", " return hv.Spline(([(np.nan, np.nan)],[1]))\n", " else: \n", " return quartet(side(n-1), side(n-1), rot(t), t)\n", " \n", "def corner(n):\n", " if n == 0:\n", " return hv.Spline(([(np.nan, np.nan)],[1]))\n", " else:\n", " return quartet(corner(n-1), side(n-1), rot(side(n-1)), u)\n", " \n", "\n", "corner(2) + side(2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def nonet(p, q, r, s, t, u, v, w, x):\n", " return above(beside(p, beside(q, r), 1, 2),\n", " above(beside(s, beside(t, u), 1, 2),\n", " beside(v, beside(w, x), 1, 2)), 1, 2)\n", "\n", "args = [fish]* 4 + [blank] + [fish] * 4\n", "nonet(*args)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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':" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%output size=250\n", "def squarelimit(n):\n", " return nonet(corner(n), side(n), rot(rot(rot(corner(n)))),\n", " rot(side(n)), u, rot(rot(rot(side(n)))), \n", " rot(corner(n)), rot(rot(side(n))), rot(rot(corner(n))))\n", "squarelimit(3)" ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 2 }