Context-sensitive L-systems

(back to contents)

In the second chapter it was said that L-systems are a variant of formal grammars. Like regular context-sensitive grammars, context-sensitive L-systems try to match text on both sides of a symbol before doing a rewrite. The difference is that matching is more elaborate compared to regular context-sensitive grammars. To quote "The Algorithmic Beauty of Plants" book (text refers to brackets in the sense of square brackets - turtle stack commands):

The introduction of context to bracketed L-systems is more difficult than in L-systems without brackets, because the bracketed string representation of axial trees does not preserve segment neighborhood. Consequently, the context matching procedure may need to skip over symbols representing branches or branch portions.
To illustrate differences let's consider an example. One of the uses of a context-sensitive L-system is signal propagation. If symbol S represents a signal (for example flower-inducing hormone) and symbol B represents a piece of stem, then signal propogation along such a stem could be modeled by the following L-system:
(with-debug
  (iteration-count 9)
  (set-axiom (S B B B B B B B B))
  (add-rule ((S < B     -> S)
             (    B     -> B)))
  (add-rule ((    S > B -> B)
	     (    S     -> S))))
This is how it reads:
If B has S on the left side, then become S, else stay as B.
If S has B on the right side, then become B, else stay as S.
This is how it rewrites:
axiom:(S B B B B B B B B)
p1: (B S B B B B B B B)
p2: (B B S B B B B B B)
p3: (B B B S B B B B B)
p4: (B B B B S B B B B)
p5: (B B B B B S B B B)
p6: (B B B B B B S B B)
p7: (B B B B B B B S B)
p8: (B B B B B B B B S)
p9: (B B B B B B B B S)
Now assume that the stem has branches. In that case the signal should be propagated in all sub branches and in the main trunk as well. The thing is that a sub branch must consider its parent branch as the part of its left context, while the main brach must ignore sub branches in its left context. Therefore L-system left context is matched differenty than that in regular grammars. For example in the word (A B [ C D [ X Y ] E [ O ]), symbol O will consider (A B C D E) as its left context. Notice that in the given example symbols + and - are ignored. In fact all predefined rules except [ and ] are ignored. To get a context matcher to ignore user defined symbols, its rule must start with the :nameless keyword. For example in the word (S B B B C B B B B B), symbol C would block signal S from propagating unless its rule is specified in the following way (:nameless C -> C).
axiom: (S B B B [ - B B B B B ] [ + B B B B B ] B B B B B)
p1: (B S B B [ - B B B B B ] [ + B B B B B ] B B B B B)
p2: (B B S B [ - B B B B B ] [ + B B B B B ] B B B B B)
p3: (B B B S [ - B B B B B ] [ + B B B B B ] B B B B B)
p4: (B B B B [ - S B B B B ] [ + S B B B B ] S B B B B)
p5: (B B B B [ - B S B B B ] [ + B S B B B ] B S B B B)
p6: (B B B B [ - B B S B B ] [ + B B S B B ] B B S B B)
p7: (B B B B [ - B B B S B ] [ + B B B S B ] B B B S B)
p8: (B B B B [ - B B B B S ] [ + B B B B S ] B B B B S)
p9: (B B B B [ - B B B B S ] [ + B B B B S ] B B B B S)
Graphically it looks something like this:
axiom p1 p2 p3 p4 p5 p6 p7 p8 p9
On the other hand, the right context is matched similarly like in regular grammars. For example the rule (A > [ B ] F -> ...) does match A in (A [ B ] F G), but does not match A in (A [ B C ] F G). However, there is a small feature to skip branches in the right context. It is a special symbol * that matches anything until the corresponding closing square bracket. For example (A > [ B * ] F -> ...) now matches A in (A [ B C [ D ] E ] F G).

(defconstant +fibonacci+ 137.50777)

(defun segment ()
  (turtle-forward 80.0))

(with-graphics
  (iteration-count 7)
  (add-macro (R -> roll((- +fibonacci+))))
  (set-axiom (R B(:active)))
  (add-option B (segment))
  (add-rule ((B(x) ? (eq x :active) -> 
	       B(:inactive) roll(+fibonacci+)
	       [ +(20) B(:active) ]		 
	       [ -(20) B(:active) ])
	     (B(x) -> B(x)))))

First example describes simple branching structure where each branch splits into two sub branches.

(defconstant +fibonacci+ 137.50777)
(defparameter *gravity* (create-point 0.0 -1.0 0.0))

(defun segment ()
  (turtle-tropism 80.0 *gravity* 0.3))

(with-graphics
  (iteration-count 7)
  (add-macro (R -> roll((- +fibonacci+))))
  (set-axiom (R B(:active)))
  (add-option B (segment))
  (add-rule ((B(x) ? (eq x :active) -> 
	       B(:inactive) roll(+fibonacci+)
	       [ +(20) B(:active) ]		 
	       [ -(20) B(:active) ])
	     (B(x) -> B(x)))))

Second example introduces new turtle manipulation function turtle-tropism. Tropism function takes three arguments: distance to go forward, tropism vector and tropism strength. Tropism function is similar to forward command in a way that it moves turtle forward, except that turtle-tropism biases turtle movement in a direction specified by tropism vector multiplied by tropism strength. In this particular example tropism vector points down hence the name *gravity*, but it can point sideways to simulate wind tropism or upwards to simulate sun tropism.

(defconstant +fibonacci+ 137.50777)
(defparameter *gravity* (create-point 0.0 -1.0 0.0))

(defun segment (s)
  (turtle-set-surface s)
  (turtle-tropism 80.0 *gravity* 0.3))

(with-graphics
  (iteration-count 14)
  (add-macro (R -> roll((- +fibonacci+))))
  (set-axiom (R B(:active 1.0)))
  (add-option B (segment s))
  (add-rule ((B(x s) > [ B(x1 s1) * ] [ B(x2 s2) * ]
	       ? (<= 7 *iterations-so-far*) -> B(x (+ s1 s2)))
	     (B(x s) ? (<= 7 *iterations-so-far*) -> B(x s))
	     (B(x s) ? (eq x :active) -> 
	       B(:inactive s) roll(+fibonacci+)
	       [ +(20) B(:active 1.0) ]		 
	       [ -(20) B(:active 1.0) ])
	     (B(x s) -> B(x s)))))

Third example introduces context-sensitive rule. Context-sensitive rule is used to associate weigth with branches in such way that each "leaf" branch has the weight of 1.0, but other branches have weight that is the sum of their children's weights. To achieve this, B rule is divided in two parts: grow part and propagate part. For the first half of iterations, third and fourth B rules are used to grow the plant. For the second half of iterations, first and second B rules are used to propagate weights from leaves to trunk. Weights are then used with turtle-set-surface function to set branch thickness which is similar to radius symbol, except that it takes branch cross section surface area as argument.

(defconstant +fibonacci+ 137.50777)
(defparameter *gravity* (create-point 0.0 -1.0 0.0))

(defun segment (s)
  (turtle-set-surface s)
  (turtle-tropism 80.0 *gravity* (- (* 0.02 s) 0.7)))

(with-graphics
  (iteration-count 14)
  (add-macro (R -> roll((- +fibonacci+))))
  (set-axiom (R B(:active 1.0)))
  (add-option B (segment s))
  (add-rule ((B(x s) > [ B(x1 s1) * ] [ B(x2 s2) * ]
	       ? (<= 7 *iterations-so-far*) -> B(x (+ s1 s2)))
	     (B(x s) ? (<= 7 *iterations-so-far*) -> B(x s))
	     (B(x s) ? (eq x :active) -> 
	       B(:inactive s) roll(+fibonacci+)
	       [ +(20) B(:active 1.0) ]		 
	       [ -(20) B(:active 1.0) ])
	     (B(x s) -> B(x s)))))

Fourth example illustrates that branch weight can be used to alter tropism strength. "Light" branches have negative tropism strength as if striving toward sun, while "heavy" branches have positive tropism strength as if being bent down by gravity due to their weigth.

(defconstant +fibonacci+ 137.50777)
(defparameter *gravity* (create-point 0.0 -1.0 0.0))
(defparameter *green* (create-color 0.0 1.0 0.0))

(defun segment (s)
  (turtle-set-surface s)
  (turtle-tropism 80.0 *gravity* (- (* 0.02 s) 0.7)))

(with-graphics
  (iteration-count 14)
  (add-macro (R -> roll((- +fibonacci+))))
  (set-background-color (create-color 0.0 0.2 0.4))
  (set-axiom (color(*green*) R B(:active 1.0)))
  (add-option B (segment s))
  (add-rule ((B(x s) > [ B(x1 s1) * ] [ B(x2 s2) * ]
	       ? (<= 7 *iterations-so-far*) -> B(x (+ s1 s2)))
	     (B(x s) ? (<= 7 *iterations-so-far*) -> B(x s))
	     (B(x s) ? (eq x :active) -> 
	       B(:inactive s) roll(+fibonacci+)
	       [ +(20) B(:active 1.0) ]		 
	       [ -(20) B(:active 1.0) ])
	     (B(x s) -> B(x s)))))

This and next example just add some decorations familiar from previous chapters.

(defconstant +fibonacci+ 137.50777)
(defparameter *gravity* (create-point 0.0 -1.0 0.0))
(defparameter *green* (create-color 0.0 1.0 0.0))

(defun segment (s)
  (turtle-set-surface s)
  (turtle-tropism 80.0 *gravity* (- (* 0.02 s) 0.7))
  (when (= s 1.0) (turtle-object "blossom" 5.0)))

(with-graphics
  (iteration-count 14)
  (add-macro (R -> roll((- +fibonacci+))))
  (set-background-color (create-color 0.0 0.2 0.4))
  (set-axiom (color(*green*) R B(:active 1.0)))
  (read-wavefront "blossom.obj" "blossom.png")
  (read-wavefront "leaf.obj")
  (add-option B (segment s))
  (add-macro (L -> obj("leaf" 10.0 *green*)))
  (add-rule ((B(x s) > [ B(x1 s1) * ] [ B(x2 s2) * ]
	       ? (<= 7 *iterations-so-far*) -> B(x (+ s1 s2)))
	     (B(x s) ? (<= 7 *iterations-so-far*) -> B(x s))
	     (B(x s) ? (eq x :active) -> 
	       B(:inactive s) roll(+fibonacci+)
	       [ +(20) B(:active 1.0) ]	 
	       [ -(20) B(:active 1.0) ]
	       [ roll((random 360)) +(60) L ])
	     (B(x s) -> B(x s)))))

(back to contents)