L-Systems

Hide code cell content
import mmf_setup; mmf_setup.nbinit()
import os
from pathlib import Path
FIG_DIR = Path(mmf_setup.ROOT) / '../Docs/_build/figures/'
os.makedirs(FIG_DIR, exist_ok=True)
import logging; logging.getLogger("matplotlib").setLevel(logging.CRITICAL)
%matplotlib inline
import numpy as np, matplotlib.pyplot as plt
import mpld3
# mpld3.enable_notebook()  # Makes loading really slow - big?
try: from myst_nb import glue
except: glue = None

This cell adds /home/docs/checkouts/readthedocs.org/user_builds/iscimath-583-fractals/checkouts/latest/src to your path, and contains some definitions for equations and some CSS for styling the notebook. If things look a bit strange, please try the following:

  • Choose "Trust Notebook" from the "File" menu.
  • Re-execute this cell.
  • Reload the notebook.

---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[1], line 9
      7 get_ipython().run_line_magic('matplotlib', 'inline')
      8 import numpy as np, matplotlib.pyplot as plt
----> 9 import mpld3
     10 # mpld3.enable_notebook()  # Makes loading really slow - big?
     11 try: from myst_nb import glue

ModuleNotFoundError: No module named 'mpld3'

L-Systems#

Here we implement the L-system (named after Lindenmayer) discussed at the end of Chapter 11 in [Schroeder, 1991]. These are reminiscent of the turtle language Logo, and are expressed as strings of characters F, f, +, -, and subroutines denoted by [/] pairs. We generalize the description there slightly to allow the use of multiple letters (not just f and F). Capital letters will draw, while lowercase letters will only move.

def walk(prog, z0=0, dz0=1.0, delta=90):
    z = z0 + 0j  # Just in case z0 is mutable.
    dz = dz0 + 0j
    turn = np.exp(delta*1j/180*np.pi) 
    paths = []
    path = [z]
    stack = []
    for char in prog:
        if char == '[':
            stack.append((z, dz))
        elif char == ']':
            if not stack:
                raise ValueError("Unbalanced ']'")
            z, dz = stack.pop()
            if len(path) > 1:
                paths.append(path)
            path = [z]

        elif char == '+':
            dz *= turn
        elif char == '-':
            dz /= turn

        else:
            z += dz
            if char == char.upper():
                # Upper characters draw
                path.append(z)
            else:
                # Lower case move
                if len(path) > 1:
                    paths.append(path)
                path = [z]
    if len(path) > 1:
        paths.append(path)
    return z, dz, paths


class Lindenmayer:
    
    z0 = 0      # Initial position
    dz0 = 1.0   # Initial direction
    d = 1.0     # Walk distance
    delta = 90  # Turn angle (degrees)

    axiom = 'F' # Initial string
    # Rules: capital letters draw, lowercase letters move.
    rules = dict(
        f='f',
        F='F'
    )
    N = 1       # Levels
    
    def __init__(self, **kw):
        for key in kw:
            if not hasattr(self, key):
                raise ValueError(f"Unknown {key=}")
            setattr(self, key, kw[key])
        self.init()
    
    def init(self):
        s = self.axiom
        for n in range(self.N):
            s = ''.join([self.rules.get(c, c) for c in s])
        self.s = s
        z, dz, self.paths = walk(s, z0=self.z0, dz0=self.dz0, delta=self.delta)

    def draw(self, ax=None, ls='-', c='k', **kw):
        if ax is None:
            ax = plt.gca()
        ax.set(aspect=1);
        for path in self.paths:
            zs = np.asarray(path)
            ax.plot(zs.real, zs.imag, ls=ls, c=c, **kw)
        return ax
# Figure 12 from Chapter 11 of Schroeder:1991
ax = Lindenmayer(
    d=2, 
    delta=90, 
    axiom='F-F-F-F',
    rules=dict(f='ffffff', 
               F='F-f+FF-F-FF-Ff-FF+f-FF+F+FF+Ff+FFF'),
    N=2).draw()
../_images/004897de27bfcdda9efae9dd44fc8b4abd81743345bd8ab8eea3a0706832cc83.png
# Figure 14 from Chapter 11 of Schroeder:1991
ax = Lindenmayer(
    dz0=1j, 
    d=4, 
    delta=-22.5, 
    axiom='F',
    rules=dict(F='FF+[+F-F-F]-[-F+F+F]'),
    N=5).draw(lw=0.3)
../_images/56348f0f2f184c00626d5ea1c2fab19bf4e219998135cfd3ef5943400fa0c900.png
# Koch snowflake
ax = Lindenmayer(
    d=1, 
    delta=60, 
    axiom='F--F--F',
    rules=dict(f='f', 
               F='F+F--F+F'),
    N=10).draw(lw=0.3)
../_images/5fd4fefaed331aa3e2985e549d5eaf8b91f50fdb6e4367253ea53725484b1d09.png
# Sirpinski's gasket
ax = Lindenmayer(
    d=1, 
    delta=120, 
    axiom='F+F+F',
    rules=dict(f='ff', F='F+f-F-f+F'),
    N=5).draw()
../_images/5f28489383bad2cf4afe96f29f1432fa4914a757e89711995dcd54e67e3e4a47.png
# Sirpinski's gasket 2
ax = Lindenmayer(
    d=1,
    delta=60, 
    axiom='A',
    rules=dict(A='B+A+B', B='A-B-A'),
    N=7).draw()
../_images/903ad6319958dfb71df3cc06df54d6d77522c881defeedaac0a2e80cc05e59ad.png
# Dragon curve
ax = Lindenmayer(
    d=1, 
    delta=90,
    #axiom='F+F',
    axiom='F',
    rules=dict(F='F+G',
               G='F-G'), 
    N=10).draw()
../_images/ecea0be93d40641452a4103fb7f1eaecc48df6ecda2f3c30d7fa3681d239b373.png
# Gosper curve (flowsnake)
ax = Lindenmayer(
    d=1, 
    delta=60,
    axiom='A',
    rules=dict(A='A-B--B+A++AA+B-',
               B='+A-BB--B-A++A+B'), 
    N=4).draw(lw=0.3)
ax.set(title="Can you find the path to the middle?");
../_images/6a06dde5931f93c269db98fdcea3dd36c4641201c71d27e830827b29ae4c59c0.png