Playing with, and visualising, L-systems
downloadHaskell
I was reading up on the Racket programming language and discovered that it had specific language for writing L-systems, of which I had not previously heard. According to Wikipedia, L-systems are:
a parallel rewriting system and a type of formal grammar. […] L-systems were introduced and developed in 1968 by Aristid Lindenmayer, a Hungarian theoretical biologist and botanist at the University of Utrecht. Lindenmayer used L-systems to describe the behaviour of plant cells and to model the growth processes of plant development. L-systems have also been used to model the morphology of a variety of organisms and can be used to generate self-similar fractals.
There was something very appealing about a simple set of rewriting rules which, in some way, provided a really good model for how organic systems grow. Since They are so simple, and naturally recursive, I wanted to play around with different L-systems myself.
Defining the L-system structure
An L-system essentially consists of an Alphabet
of possible symbols and a rewrite rule (I’m calling produce
) that, on each step, replaces a given symbol with some other symbol(s). There is also an initial state (which we’re calling the axiom
). Here we’re using context-free L-systems because it rewrites on a symbol-by-symbol basis rather than examining what came before or after (that’s a future area for exploration).
So, we can define that structure using a type class1:
class CFL (a) where
data Alphabet a
axiom :: Alphabet a
produce :: Alphabet a -> NonEmpty (Alphabet a)
We can then write a simple function which steps forward in “time”, rewriting each symbol using the produce
function:
step :: (CFL a) => NonEmpty (Alphabet a) -> NonEmpty (Alphabet a)
step (a :| rest) = sconcat $ produce a :| map produce rest
This is now enough for us to define a simple L-system. Let’s start with the “binary tree” example from Wikipedia:
data BinaryTree
instance CFL BinaryTree where
data Alphabet BinaryTree = Zero | One | LBrace | RBrace
axiom _ = Zero
-- 1 -> 11
produce One = One :| [One]
-- 0 -> 1[0]0
produce Zero = One <| LBrace <| Zero <| RBrace :| [Zero]
-- [] are constants
produce c = singleton c
As far as defining a context-free L-system, that’s it! With a simple Show
instance to make it slightly easier to read, we can see what happens as we step through the growth of this binary tree system:
λ: a = axiom @BinaryTree
λ: show a
"0"
λ: mconcat . map show . toList . step . singleton $ a
"1[0]0"
λ: mconcat . map show . toList . step . step . singleton $ a
"11[1[0]0]1[0]0"
λ: mconcat . map show . toList . step . step . step . singleton $ a
"1111[11[1[0]0]1[0]0]11[1[0]0]1[0]0"
Similarly, we can define the Koch L-system, also from the wiki page:
instance CFL Koch where
data Alphabet Koch = F | Plus | Minus
axiom = F
produce F = F <| Plus <| F <| Minus <| F <| Minus <| F <| Plus :| [F]
produce Plus = singleton Plus
produce Minus = singleton Minus
Visualising
Since L-systems can represent some growing system, starting from a root (or axiom
), Turtle Graphics provide a nice way of visualising these systems, since the Turtle takes a set of instructions and moves accordingly (it’s stateful in that sense).
In case you’re not familiar, Turtle graphics basically consists of a cursor (the Turtle) and a set of instructions like “forward 50” or “left 45” which tells the Turtle how to move. As it moves, the Turtle draws a path of its movement on the canvas.
We can translate our sets of symbols into instructions for the Turtle, and therefore make a diagram of our L-system as it evolves. Different L-system Alphabet
s will require different translations to make the visualisation effective, so let’s start with our binary tree system first (as per the translation in the wiki page).
-
0
means: draw a small line with a “fork” or “leaf” at the end -
1
means: draw a small line with nothing at the end -
[
means: turn anti-clockwise by 45° -
]
means: go back to the last[
and turn clockwise by 45°
You’ll notice, therefore, that pairs of []
represent sub-trees.
Defining a Turtle system
I decided to use SVG graphics for this (partly because my attempt to get OpenGL working via Gloss totally failed). In order to keep track of the cursor (or Turtle), we encapsulate this state in a State
monad, and define our render function, which translates a symbol to an SVG element, in terms of that State
monad:
-- Keep track of the position and direction representing a single cursor
data TurtleState = TurtleState
{ position :: (Float, Float),
angle :: Float
}
-- The state of our renderer is a stack of at least one 'TurtleState'
newtype RenderState = RenderState (NonEmpty TurtleState)
-- And now we can compute all our translation operations in the context of this 'RenderState'
type WorldM a = State RenderState a
To capture the general translation behaviour, we use a Turtle
type class which turns a symbol from our Alphabet
into an SVG element (from the SVG Builder library):
class Turtle a where
render :: a -> WorldM SVG.Element
Another reason to do this with the State
monad is that it gives us a nice algebra for sequencing our rendering instructions. Instead of having to think about how to handle the sequencing of the state changes, we can just traverse
over the instruction sets and get the end result.
Translating the BinaryTree to graphical instructions
If we want to turn our BinaryTree
symbols into graphical elements, we need some basic operations like moving the turtle, as well as operations on the state like pushing and popping states on stack (so we can go back to a previously-saved state as with our []
pairs), so let’s define some helpers:
-- | Modifies the topmost 'TurtleState'
turtleMod :: (TurtleState -> TurtleState) -> WorldM ()
turtleMod f = modify $ \(RenderState (t :| rest)) -> RenderState (f t :| rest)
-- | Views the topmost 'TurtleState' in the stack
viewTurtle :: WorldM TurtleState
viewTurtle = gets $ \(RenderState (t :| _)) -> t
-- | Takes off the topmost 'TurtleState', but if there is only one, it leaves it
-- in place as we can't have an empty stack of states
popTurtle :: WorldM ()
popTurtle = modify $ \(RenderState (t :| rest)) -> case NEL.nonEmpty rest of
Just states -> RenderState states
Nothing -> RenderState (t :| rest)
-- | Puts a 'TurtleState' on top of the stack
pushTurtle :: TurtleState -> WorldM ()
pushTurtle t = modify $ \(RenderState states) -> RenderState $ t <| states
-- | Move from one point some distance, at some angle, to another point
vecMove :: (Float, Float) -> Float -> Float -> (Float, Float)
vecMove (x1, y1) distance theta = (x1 + distance * sin theta, y1 + distance * cos theta)
Now we’re ready to write our translation from the Alphabet BinaryTree
to drawing instructions (or, really, SVG elements):
-- For the sake of making things uniform, let's define some basic sizes on the canvas
size :: Float
size = 1600
baseLength :: Float
baseLength = 10
baseWidth :: Float
baseWidth = 5
-- And, since the SVG builder library takes 'Text' values for literally
-- everything, we need a way to translate all our numbers to 'Text'
showT :: (Show a) => a -> Text
showT = pack . show
-- A leaf is just a small green circle
mkLeaf :: (Float, Float) -> SVG.Element
mkLeaf origin =
SVG.circle_
[ SVG.Cx_ <<- showT (fst origin),
SVG.Cy_ <<- showT (snd origin),
SVG.R_ <<- showT (baseLength / 2),
SVG.Fill_ <<- "green"
]
instance Turtle (Alphabet BinaryTree) where
render Zero = do
-- Let's get the current cursor position and angle
TurtleState origin theta <- viewTurtle
-- The move forward a small amount
let next = vecMove origin baseLength theta
-- and save this new cursor state
turtleMod (const $ TurtleState next theta)
pure $
mkLeaf next -- draw a leaf at the new position
<> SVG.line_ -- and a line from the previous position to the new position
[ SVG.X1_ <<- showT (fst origin),
SVG.Y1_ <<- showT (snd origin),
SVG.X2_ <<- showT (fst next),
SVG.Y2_ <<- showT (snd next),
SVG.Stroke_ <<- "black",
SVG.Stroke_width_ <<- showT baseWidth
]
render One = do
-- Very similar to how we render 'Zero', but we don't have a leaf
TurtleState origin theta <- viewTurtle
let next = vecMove origin baseLength theta
turtleMod (const $ TurtleState next theta)
pure $
SVG.line_
[ SVG.X1_ <<- showT (fst origin),
SVG.Y1_ <<- showT (snd origin),
SVG.X2_ <<- showT (fst next),
SVG.Y2_ <<- showT (snd next),
SVG.Stroke_ <<- "black",
SVG.Stroke_width_ <<- showT baseWidth
]
render LBrace = do
-- copy the cursor state
viewTurtle >>= pushTurtle
-- and turn anti-clockwise by 45° (π / 4)
turtleMod $ \turtle -> turtle {angle = angle turtle - (pi / 4)}
-- but we don't actually render anything
pure mempty
render RBrace = do
-- go back to the previously-saved state
popTurtle
-- and turn clockwise by 45° (π / 4)
turtleMod $ \turtle -> turtle {angle = angle turtle + (pi / 4)}
-- but we don't actually render anything
pure mempty
Results!
Running this we see some nice representations of our system’s growth:
We can do something similar for the Koch system
instance Turtle (Alphabet Koch) where
render F = do
TurtleState origin theta <- viewTurtle
let next = vecMove origin (baseLength * 2) theta
turtleMod (const $ TurtleState next theta)
pure $ -- Draw a small line
SVG.line_
[ SVG.X1_ <<- showT (fst origin),
SVG.Y1_ <<- showT (snd origin),
SVG.X2_ <<- showT (fst next),
SVG.Y2_ <<- showT (snd next),
SVG.Stroke_ <<- "blue",
SVG.Stroke_width_ <<- showT baseWidth
]
render Plus = do
-- Rotate 90° clockwise
turtleMod $ \turtle -> turtle {angle = angle turtle + (pi / 2)}
pure mempty
render Minus = do
-- Rotate 90° anticlockwise
turtleMod $ \turtle -> turtle {angle = angle turtle - (pi / 2)}
pure mempty
Wrapping up
Iterating over the world
To generate these images, and to make experimentation a bit easier, I wrote a script to step through the iterations of growth for a given system and save the results as SVG images. While most of this final part is fairly mechanical, the nice part is that, due to the structures we have used like NonEmpty
and State
, we can very easily run the production rule over all the symbols iteratively, and then traverse over the drawing actions to generate a final result quite neatly. You can see how all this fits together in the downloadable version of this script (see the link at the top), but here’s a snippet from how that works:
runWorld ::
forall a.
( (CFL a),
(Turtle (Alphabet a))
) =>
CLI ->
Proxy a ->
IO ()
runWorld options _ =
let start = RenderState (singleton $ TurtleState {position = (size / 2, 0), angle = 0})
in mapM_ (uncurry $ writeImage options)
. take (maxIterations options)
. zip [1 ..]
. map (mkImage . sconcat . fst . flip runState start . traverse render)
. iterate step
. singleton
$ axiom (Proxy @a)
Further experimentation
There are two directions that would be interesting to pursue: the first is context-aware L-systems – those that take more than just a single symbol at a time, but look to what is around that symbol to decide what to produce next; the second is writing my own systems with their own strange production rules, to see what one might come up with (sort of in the vein of playing with Game Of Life automata.
-
I’m using
Data.List.NonEmpty
to contain my lists of symbols, since I do not want to have to deal with the empty list cases over and over again.↩︎