Recursive Drawings ================== Applying simple rules recursively, we can draw remarkable pictures. For recursive images, the design of a GUI with canvas, scale, and button, can always be very similar. The action is defined by a callback function. We start with simple drawings, such as a regular n-gon or a Cantor set, before making more complicated plots. The more advanced mathematical computations are defined in separate functions, isolating the complexity to some critical spots. A Regular n-Gon --------------- For starters, let us draw a regular n-gon, as shown in :numref:`figmultigon`. .. _figmultigon: .. figure:: ./figmultigon.png :align: center A multigon drawn on a Tkinter canvas. The structure of the GUI to produce :numref:`figmultigon` is listed below. In the design of the class ``MultiGon``, we write the doc strings first. :: class MultiGon(object): """ GUI to draw a regular n-gon on canvas. """ def __init__(self, wdw): """ Determines the layout of the GUI. """ def draw_gon(self, val): """ Draws a regular n-gon. """ def main(): """ Instantiates the GUI object and launches the main event loop. """ The layout of the GUI consists of a horizontal scale, for the user to determine the number *n* of vertices of the *n*-gon. The cancase is the second widget of the GUI, as defined in the function below. :: from tkinter import Tk, IntVar, Scale, Canvas, ALL def __init__(self, wdw): """ Determines the layout of the GUI. """ wdw.title('regular n-gon') self.dim = 400 self.ngn = IntVar() self.scl = Scale(wdw, orient='horizontal', \ from_=1, to=20, tickinterval=1, \ length=self.dim, variable=self.ngn, \ command=self.draw_gon) self.scl.set(10) self.scl.grid(row=0, column=0) self.cnv = Canvas(wdw, width=self.dim, \ height=self.dim, bg='white') self.cnv.grid(row=1, column=0) The Scale triggers the command ``draw_gon()`` and determines the data attribute ``ngn`` of a MultiGon object. The second argument ``val`` is the value for ``ngn`` passed to ``draw_gon()`` via the Scale. :: def draw_gon(self, val): """ Draws a regular n-gon. """ xctr = self.dim/2 yctr = self.dim/2 radius = 0.4*self.dim self.cnv.delete(ALL) self.cnv.create_text(xctr, yctr, text=val, tags="text") ngn = int(val) The value ``val`` is passed as a string. Therefore, we convert to an integer in the statement ``ngn = int(val)``. After obtaining the value for *n*, the method can draw the vertices and the edges between the vertices. In a regular *n*-gon, all *n* points lie on a circle, equispaced using the angle :math:`2 \pi/n`. Therefore we add the following import statement. :: from math import cos, sin, pi Then the code for the method ``draw_gon()`` continues. :: # the method draw_gon() continued ... pts = [] for i in range(0, ngn): xpt = xctr + radius*cos(2*i*pi/ngn) ypt = yctr + radius*sin(2*i*pi/ngn) self.cnv.create_oval(xpt-6, ypt-6, xpt+6, ypt+6, \ width=1, outline='black', fill='SkyBlue2', \ tags="dot") pts.append((xpt, ypt)) for i in range(0, ngn-1): self.cnv.create_line(pts[i][0], pts[i][1], \ pts[i+1][0], pts[i+1][1], width=2) self.cnv.create_line(pts[ngn-1][0], pts[ngn-1][1], \ pts[0][0], pts[0][1], width=2) We will apply the template of the multigon to draw a sequence of recursive images, starting with the Cantor set in :numref:`figcantor`. This figure is then generalized into the Koch curve, in :numref:`figkoch` and the Koch flake in :numref:`figflake`. Two other self similar images are the Sierpinski gasket, shown in :numref:`figsierpinski` and Hilbert curves, drawn in :numref:`fighilbertgui`. .. _figcantor: .. figure:: ./figcantor.png :align: center A Cantor set on a Tkinter canvas. .. _figkoch: .. figure:: ./figkoch.png :align: center A Koch curve on a Tkinter canvas. .. _figflake: .. figure:: ./figflake.png :align: center A Koch flake on a Tkinter canvas. .. _figsierpinski: .. figure:: ./figsierpinski.png :align: center A Sierpinski gasket on a Tkinter canvas. .. _fighilbertgui: .. figure:: ./fighilbertgui.png :align: center A GUI for Hilbert curves. The Cantor set is defined by three rules: 1. Take the interval~$[0,1]$, 2. Remove the middle part third of the interval. 3. Repeat rule \textcolor{blue}{2} on the first and third part. The Cantor set is infinite, to visualize at level :math:`n`: * for :math:`n = 0`: start at :math:`[0,1]`, * for :math:`n > 0`: apply the rule :math:`2 n` times. In our definition of the class ``CantorSet``, we will write the doc strings first of the methods. :: class CantorSet(object): """ GUI to draw a Cantor set on canvas. """ def __init__(self, wdw, N): """ A Cantor set with N levels. """ def cantor(self, lft, rgt, hgt, txt, lvl): """ Draws a line from lft to rgt, at height hgt, txt is a string, int(txt) equals the number of times the middle third must be removed, lvl is the level of recursion. """ def draw_set(self, val): """ Draws a Cantor set. """ The constructor of the class defines the layout of the GUI. :: def __init__(self, wdw, N): """ A Cantor set with N levels. """ wdw.title('a cantor set') self.dim = 3**N+20 self.nsc = IntVar() self.scl = Scale(wdw, orient='horizontal', \ from_=0, to=N, tickinterval=1, length=self.dim, \ variable=self.nsc, command=self.draw_set) self.scl.grid(row=0, column=0) self.scl.set(0) self.cnv = Canvas(wdw, width=self.dim, \ height=self.dim/3, bg='white') self.cnv.grid(row=1, column=0) self.btt = Button(wdw, text="clear canvas", \ command=self.clear_canvas) self.btt.grid(row=2, column=0) The method ``clear_canvas()`` is triggered by the button. Its definition is below. :: def clear_canvas(self): """ Clears the entire canvas. """ self.cnv.delete(ALL) The method ``draw_set()`` is triggered by the scale. Its definition is below. :: def draw_set(self, val): """ Draws a Cantor set. """ nbr = int(val) self.cantor(10, self.dim-10, 30, val, nbr) The method ``cantor()`` is recursive. In designing a recursive function, more than half of the work goes into defining the right parameters. :: def cantor(self, lft, rgt, hgt, txt, lvl): """ Draws a line from lft to rgt, at height hgt; txt is a string, int(txt) equals the number of times the middle third must be removed; lvl is the level of recursion. """ The parameters ``lft``, ``rgt``, and ``hgt`` define the line segment from (``lft``, ``hgt``) to (``rgt``, ``hgt``). The parameter ``txt`` is the value passed via the Scale, as text string, ``txt`` is also put on Canvas. The most important parameter is ``lvl``. Initially: ``lvl = int(txt)``. With every recursive call, ``lvl`` is decremented by 1. The ``lvl`` in ``cantor(self, lft, rgt, hgt, txt, lvl)`` controls the recursion. * At ``lvl = 0``, the line segment from (``lft``, ``hgt``) to (``rgt``, ``hgt``) is drawn. * For ``lvl > 0``, we compute left and right limit of the middle third of [``lft``, ``rgt``], respectively denoted by ``nlf`` and ``nrg`` as .. math:: \begin{array}{c} {\tt nlf} = {\tt lft} + ({\tt rgt}-{\tt lft})/3 = (2 {\tt lft} + {\tt rgt}) / 3 \\ {\tt nrg} = {\tt rgt} = ({\tt rgt}-{\tt lft})/3 = ({\tt lft} + 2{\tt rgt}) / 3 \end{array} Then we make two recursive calls: :: self.cantor(lft, nlf, hgt+30, txt, lvl-1) self.cantor(nrg, rgt, hgt+30, txt, lvl-1) The code for the method ``cantor()`` is below: :: def cantor(self, lft, rgt, hgt, txt, lvl): """ Draws a line from lft to rgt, at height hgt; txt is a string, int(txt) equals the number of times the middle third must be removed; lvl is the level of recursion. """ if lvl == 0: # draw line segment self.cnv.create_line(lft, hgt, rgt, hgt, width=2) else: nlf = (2*lft+rgt)/3 nrg = (lft+2*rgt)/3 self.cantor(lft, nlf, hgt+30, txt, lvl-1) self.cantor(nrg, rgt, hgt+30, txt, lvl-1) if lvl == int(txt): # put text string xctr = self.dim/2 if txt == '0': self.cnv.create_text(xctr, hgt-10, text=txt) else: self.cnv.create_text(xctr, hgt+lvl*30, \ text=txt) The Koch curve is a Cantor set where the removed middle third of the interval is replaced by a wedge. The top of the wedge is above the midpoint of the removed middle interval. The slopes of the wedge make an angle of 60 degrees with respect to the rest of the line segment. To visualize a Koch curve at level :math:`n`: * For :math:`n = 0`: start at [0,1]. * For :math:`n > 0`: make wedges :math:`n` times. In developing the class for the GUI, we write the doc strings of the methods in the class ``KochCurve`` first. :: class KochCurve(object): """ GUI to draw a Koch curve on canvas. """ def __init__(self, wdw, N): """ A Koch curve with N levels. """ def koch(self, left, right, k): """ A Koch curve from left to right with k levels. """ def draw_curve(self, val): """ Draws a Koch curve. """ The constructor defines the layout of the GUI, as defined below. Our GUI has a scale, canvas, and button. :: def __init__(self, wdw, N): """ A Koch curve with N levels. """ wdw.title('a Koch curve') self.dim = 3**N+20 self.nvr = IntVar() self.scl = Scale(wdw, orient='horizontal', \ from_=0, to=N, tickinterval=1, \ length=self.dim, variable=self.nvr, \ command=self.draw_curve) self.scl.set(0) self.scl.grid(row=0, column=0) self.cnv = Canvas(wdw, width=self.dim, \ height=self.dim/3, bg='white') self.cnv.grid(row=1, column=0) self.btt = Button(wdw, text="clear canvas", \ command=self.clear_canvas) self.btt.grid(row=2, column=0) The method ``clear_canvas()`` is triggered by a button. Its definition is below. :: def clear_canvas(self): """ Clears the entire canvas. """ self.cnv.delete(ALL) The method ``draw_curve()`` is activated by a scale. Its definition is below. :: def draw_curve(self, val): """ Draws a Koch curve. """ nbr = int(val) left = (10, self.dim/3-20) right = (self.dim-10, self.dim/3-20) self.koch(left, right, nbr) The method ``koch()`` is recursive, to draw a Koch curve of ``nbr`` levels from ``left`` to ``right``. The parameter ``nbr`` in ``koch(left, right, nbr)`` controls the recursion. * For ``nbr = 0``, the line segment from ``left`` to ``right`` is drawn. * For ``nbr > 0``, we compute left ``nlf``, right ``nrg``, and midpoint ``mid`` of the middle third of line segment. Now we fill the middle with a wedge. The edges of the wedge make an angle of 60 degrees. Then the height is :math:`\sin(\frac{\pi}{3}) = \frac{\sqrt{3}}{2}`, multiplied with one third of the length of the segment. The ``peak`` of the wedge is relative to the size of the interval and the position of the midpoint ``mid``: .. math:: \begin{array}{l} {\tt top} = (\frac{\sqrt{3}}{6} ({\tt left}[1]-{\tt right}[1]), \frac{\sqrt{3}}{6} ({\tt right}[0]-{\tt left}[0])), \\ \\ {\tt peak} = ({\tt mid}[0]-{\tt top}[0],{\tt mid}[1]-{\tt top}[1]) \end{array} Recall that on the canvas: (0,0) is at the topmost left corner. The code for the method ``koch()`` is listed below. :: def koch(self, left, right, k): """ A Koch curve from left to right with k levels. """ if k == 0: self.cnv.create_line(left[0], left[1], \ right[0], right[1], width=2) else: nlf = ((2*left[0]+right[0])/3.0, (2*left[1]+right[1])/3.0) nrg = ((left[0]+2*right[0])/3.0, (left[1]+2*right[1])/3.0) mid = ((left[0]+right[0])/2.0, (left[1]+right[1])/2.0) ratio = sqrt(3)/6 top = (ratio*(left[1]-right[1]), ratio*(right[0]-left[0])) peak = (mid[0]-top[0], mid[1]-top[1]) self.koch(left, nlf, k-1) self.koch(nlf, peak, k-1) self.koch(peak, nrg, k-1) self.koch(nrg, right, k-1) In the development of the GUI for the Koch flake, we write the doc strings of the class ``KochFlake`` first. :: class KochFlake(object): """ GUI to draw a Koch flake canvas. """ def __init__(self, wdw): """ Determines the layout of the GUI. """ def koch(self, left, right, k): """ A Koch flake from left to right with k levels. """ def draw_flake(self, val): """ Draws a regular Koch flake. """ The scales of the GUI are defined by the constructor, defined below. :: def __init__(self, wdw): self.nbr = IntVar() self.k = IntVar() self.scn = Scale(wdw, orient='horizontal', \ from_=3, to=20, tickinterval=1, \ length=self.dim, variable=self.nbr, \ command=self.draw_flake) self.scn.set(10) self.scn.grid(row=0, column=1) self.sck = Scale(wdw, orient='vertical', \ from_=0, to=6, tickinterval=1,\ length=self.dim, variable=self.k,\ command=self.draw_flake) self.sck.set(0) self.sck.grid(row=1, column=0) The code for the method ``draw_flake()`` follows. :: def draw_flake(self, val): """ Draws a regular Koch flake. """ (xctr, yctr) = (self.dim/2, self.dim/2) radius = 0.4*self.dim self.cnv.delete(ALL) nbr = self.nbr.get() k = self.k.get() txt = '(' + str(nbr) + ',' + str(k) + ')' self.cnv.create_text(xctr, yctr, text=txt, tags="text") pts = [] for i in range(0, nbr): xpt = xctr + radius*cos(2*i*pi/nbr) ypt = yctr + radius*sin(2*i*pi/nbr) pts.append((xpt, ypt)) for i in range(0, nbr-1): self.koch(pts[i], pts[i+1], k) self.koch(pts[nbr-1], pts[0], k) Space Filling Curves and L-systems ---------------------------------- The first three Hilbert curves are shown in :numref:`fighilbertcurves`. .. _fighilbertcurves: .. figure:: ./fighilbertcurves.png :align: center The first three Hilbert curves. The recursion is determined by the application of four types of moves, labeled A, B, C, and D in :numref:`fighilbertmoves`. .. _fighilbertmoves: .. figure:: ./fighilbertmoves.png :align: center Four types of moves in the recursion to make Hilbert curves. In the development of the GUI for the Hilbert curves, we write the doc string first: :: class Hilbert: """ GUI for Hilbert's space filling curves. """ def __init__(self,wdw,dimension): """ Defines a canvas, buttons, and scale. """ def plot(self, nxp, nyp): """ Plots a line for the current position to the new coordinates (nxp, nyp), and sets the current position to (nxp, nyp). """ def draw(self, val): """ Draws the Hilbert curve on canvas. """ Then for each move in :numref:`fighilbertmoves` we have a method. Their doc strings are listed below. :: def amove(self, k): """ Turns counterclockwise from up right to down right. """ def bmove(self, k): """ Turns clockwise from down left to down right. """ def cmove(self, k): """ Turns counterclockwise from down left to up left. """ def dmove(self, k): """ Turns clockwise from up right to up left. """ The basic method ``plot()`` is defined below. :: def plot(self, nxp, nyp): """ Plots a line for the current position to the new coordinates (nxp, nyp), and sets the current position to (nxp, nyp). """ self.cnv.create_line(self.xpt, self.ypt, \ nxp, nyp, width=2) self.xpt = nxp self.ypt = nyp The top level method is ``draw()``, given next. :: def draw(self, val): """ Draws the Hilbert curve on canvas. """ nbr = int(val) self.hgt = self.dim/2**nbr self.ypt = 5 + self.dim - self.hgt/2 self.xpt = self.ypt self.amove(nbr) The move A (see :numref:`fighilbertmoves`) is defined in the method ``amove()``. :: def amove(self, k): """ Turns counterclockwise from up right to down right. """ if k > 0: self.dmove(k-1) self.plot(self.xpt - self.hgt, self.ypt) self.amove(k-1) self.plot(self.xpt, self.ypt - self.hgt) self.amove(k-1) self.plot(self.xpt + self.hgt, self.ypt) self.bmove(k-1) The move B (see :numref:`fighilbertmoves`) is defined in the method ``bmove()``. :: def bmove(self, k): """ Turns clockwise from down left to down right. """ if k > 0: self.cmove(k-1) self.plot(self.xpt, self.ypt + self.hgt) self.bmove(k-1) self.plot(self.xpt + self.hgt, self.ypt) self.bmove(k-1) self.plot(self.xpt, self.ypt - self.hgt) self.amove(k-1) The move C (see :numref:`fighilbertmoves`) is defined in the method ``cmove()``. :: def cmove(self, k): """ Turns counterclockwise from down left to up left. """ if k > 0: self.bmove(k-1) self.plot(self.xpt + self.hgt, self.ypt) self.cmove(k-1) self.plot(self.xpt, self.ypt + self.hgt) self.cmove(k-1) self.plot(self.xpt - self.hgt, self.ypt) self.dmove(k-1) The move D (see :numref:`fighilbertmoves`) is defined in the method ``dmove()``. :: def dmove(self, k): """ Turns clockwise from up right to up left. """ if k > 0: self.amove(k-1) self.plot(self.xpt, self.ypt - self.hgt) self.dmove(k-1) self.plot(self.xpt - self.hgt, self.ypt) self.dmove(k-1) self.plot(self.xpt, self.ypt + self.hgt) self.cmove(k-1) Exercises --------- 1. Make a GUI to visualize the Sierpinski gasket. 2. A variant of the Sierpinski gasket starts with a square and removes a smaller square at the center of the first square. This removal is repeated to the 8 remaining squares. Design a GUI for this Sierpinski carpet. Define the recursive drawing algorithm. 3. Make a GUI to visualize a Brownian bridge between two points. The rule is to replace a line segment from *A* to *B* by the segments :math:`[A,M]` and :math:`[M,B]`, where *M* is calculated as :math:`(A+B)/2` with random noise added to it. Repeat the rule to the new segments, etc. 4. Design a recursive algorithm to draw a tree, starting with a trunk and 3 branches. Put at the top of each branch again a trunk and 3 branches, and then again... What are the parameters of the recursive function to define the rule to generate the tree?