CS200: Computer Science, Spring 2002

Problem Set 3: LSystem Fractals Out: 4 February 2002
Due: 13 February 2002, before class
Turnin Checklist: On February 13, bring to class a stapled turn in containing:
 Your answer to Question 1.
 All the code you wrote for this problem set. Be sure to clearly mark the code for each question. You can put your code in a separate file, or edit lsystem.ss directly.
 Print outs of interesting fractals you produced for Question 7.
Collaboration Policy  Same as Problem Set 2
For this problem set, you may either work alone and turn in a problem set with just your name on it, or work with one other student in the class of your choice. If you work with a partner, you and your partner should turn in one assignment with both of your names on it.PurposeRegardless of whether you work alone or with a partner, you are encouraged to discuss this assignment with other students in the class and ask and provide help in useful ways. You may consult any outside resources you wish including books, papers, web sites and people. If you use resources other than the class materials, indicate what you used along with your answer.
 Learn to use lists
 Write recursive functions that manipulate lists
 Introduce data abstraction
 Make some better fractals than you could in PS2
Downloads
 curve.ss — updated from Problem Set 2
 lsystem.ss — partial code for LSystem Fractals
Background
In Problem Set 2, you created fractals by manipulating functions that represented curves. In this problem set, you will explore a different way of creating fractals known as the Lindenmayer system (or Lsystem). Aristid Lindemayer, a theoretical biologist at the University of Utrecht, developed the Lsystem in 1968 as a mathematical theory of plant development. In the late 1980s, he collaborated with Przemyslaw Prusinkiewicz, a computer scientist at the University of Regina, to explore computational properties of the Lsystem and developed many of the ideas on which this problem set is based.
The idea behind Lsystem fractals is we can describe a curve as a list of lines and turns, and create new curves by rewriting old curves. Everything in an Lsystem curve is either a forward line (denoted by Fn where n is a number representing the length of the line), or a right turn (denoted by Ra where a is an angle in degrees). We can denote left turns by using negative angles.
We create fractals by replacing all forward lines in a curve list with the original curve list. Suppose we wanted to model the growth of branches on a tree. After carefully examining the tree with our magnifying glass and industrial strength slide ruler, we discover an interesting pattern. There is a regularly repeating pattern in how the branches sprout from the trunk, and how branches grow out from yet other branches. Simplifying it as much as possible, we deduce that the pattern goes as follows: the base case (in this case the trunk of the tree) has branches at F1 O(R30 F1) F1 O(R60 F1) F1.
This translates as: the trunk goes up one unit (in this case the numbers can represent anything form inches to feet), a branch sprouts at an angle 30 degrees to the trunk and grows for one unit. The O means an offshoot  we draw the curve in the following parentheses, and then return to where we started before the offshoot. The trunk grows another unit and now another branch, this time at 60 degrees relative to the trunk grows for one units. Finally the trunk grows for one more unit. Upon further investigation we confirm that indeed even branches follow this pattern. If we were to write this out in an organized manner we might choose to write it out as such:
Start: (F1)Here are the commands this produces after two iterations:
Rule: F1 ::= (F1 O(R30 F1) F1 O(R60 F1) F1)Iteration 0: (F1)
Iteration 1: (F1 O(R30 F1) F1 O(R60 F1) F1)
Iteration 2: (F1 O(R30 F1) F1 O(R60 F1) F1 O(R30 F1 O(R30 F1) F1 O(R60 F1) F1) F1 O(R30 F1) F1 O(R60 F1) F1 O(R60 F1 O(R30 F1) F1 O(R60 F1) F1) F1 O(R30 F1) F1 O(R60 F1) F1)Here's what that looks like:
(Your drawings won't look quite like this, unless you do the optional Better Drawing part at the end of this assignment. You probably won't be able to draw much beyond iteration 5 without crashing DrScheme  LSystem fractals get really big.)
Iteration 0
Iteration 1
Iteration 2
Iteration 5
The Great Lambda Tree of KnowledgeNote that Lsystem command rewriting is similar to the replacement rules in a BNF grammar. The important difference is that with Lsystem rewriting, each iteration replaces all instances of Fn in the initial string instead of just picking one tow replace.
We can divide the problem of producing an Lsystem fractal into two main parts: (1) Producing a list of Lsystem commands that represents the fractal by rewriting according to the Lsystem rule; and (2) Drawing a list of Lsystem commands. We'll do the drawing part first, since it will make it easier to do the fractalproducing part if we can see our fractals as pictures instead of just as lists of Lsystem commands. First, we consider how to represent Lsystem commands.
Representing LSystem Commands
Here is a BNF grammar for Lsystem commands:
 CommandSequence ::= ( CommandList )
 CommandList ::= Command CommandList
 CommandList ::=
 Command ::= FDistance
 Command ::= RAngle
 Command ::= OCommandSequence
 Distance ::= Number
 Angle ::= Number
Question 1: Show that (F1 O(R60 F1) F1) is a string in the language defined by our BNF grammar. To do this, you should start with CommandSequence, and show a sequence of replacements that follow the grammar rules that produce the target string. You can use the rule numbers above to identify the rules. We need to find a way to turn strings in this grammar into objects we can manipulate in a Scheme program. We can do this by looking at the BNF grammar, and converting the nonterminals into Scheme objects.
;;; CommandSequence ::= ( CommandList ) (define makelsystemcommand list) ;;; We represent the different commands as pairs where the first item in the ;;; pair is a tag that indicates the type of command: 'f for forward, 'r for rotate ;;; and 'o for offshoot. We use quoted letters  'f is short for ;;; (quote f)  to make tags  they evaluate to the letter after the quote. ;;; Command ::= FDistance (define (makeforwardcommand distance) (cons 'f distance)) ;;; Command ::= RAngle (define (makerotatecommand angle) (cons 'r angle)) ;;; Command ::= OCommandSequence (define (makeoffshootcommand commandsequence) (cons 'o commandsequence))
Question 2: It will be useful to have procedures that take Lsystem commands as parameters, and return information about those commands. Define the following procedures:
 (isforward? lcommand) — evaluates to #t if the parameter passed is a forward command (indicated by its first element being a 'f tag).
 (isrotate? lcommand)
 (isoffshoot? lcommand)
 (getdistance lcommand) — evaluates to the distance associated with a forward command. Produces an error if the command is not a forward command (see below for how to produce an error).
 (getangle lcommand) — evaluates to the angle associated with a rotate command. Produces an error if the command is not a rotate command.
 (getoffshootcommands lcommand) — evaluates to the offshoot command list associated with an offshoot command. Produces an error if the command is not an offshoot command.
You will find the following functions useful:
If you define these functions correctly, you should produce these evaluations:
 (car lst) — evaluates to the first element of the list parameter
 (eq? v1 v2) — evaluates to #t if v1 and v2 are exactly the same; otherwise evaluates to false. For example, (eq? 's 's) evaluates to #t and (eq? 's 't) evaluates to #f.
 (error message) — produces an error with message a string given as the first parameter. For example, (error "Yikes! Attempt to getdistance for a command that is not a forward command") would display the message in red and stop execution. It is useful to use error in your code so you will more easily identify bugs.
You should be able to make up similar test cases yourself to make sure the other procedures you defined work. In lsystem.ss we define:> (isforward? (makeforwardcommand 3)) #t > (isforward? (makerotatecommand 90)) #f > (getdistance (makeforwardcommand 3)) 3 > (getdistance (makerotatecommand 90)) Yikes! Attempt to getdistance for a command that is not a forward commandso you should get:(define (islsystemcommand? lcommand) (or (isforward? lcommand) (isrotate? lcommand) (isoffshoot? lcommand)))> (islsystemcommand? (makeforwardcommand 3)) #t > (islsystemcommand? (list 2 3 4)) #fDrawing an LSystem Curves
Now that we have a way of representing LSystem curves, we can produce procedures that display LSystem curves graphically. Since we already know how to display curves represented as functions graphically from Problem Set 2, a good approach is to reuse all the work from Problem Set 2. So, to draw an LSystem curve, we need a procedure that turns an LSystem Curve into a function curve that maps a value between 0.0 and 1.0 to a point.Below is code for converting a list of LSystem commands with some parts missing (it is explained below, but try to understand it yourself before reading further):
We are defining converttocurve recursively. If there are no more commands (the lcommands parameter is null), it evaluates to the leaf curve (for now, we just make a point  you may want to replace this with something more interesting to make a better fractal).(define (converttocurve lcommands) (if (null? lcommands) (lambda (t) (makepoint 0.0 0.0)) ; the leaves (just a point for now) (if (isforward? (car lcommands)) (connectends (makeverticalline (getdistance (car lcommands))) (converttocurve (cdr lcommands))) (if (isrotate? (car lcommands)) (rotatearoundorigin ;;; Question 3: fill in the first parameter to rotatearound origin ( (getangle (car lcommands))) ;; Lsystem turns are clockwise, so we need negate the angle ) (if (isoffshoot? (car lcommands)) (connectrigidly ;;; Question 4: ;;; fill in the first parameter to connectrigidly ;;; fill in the second parameter to connectrigidly ) (error "Bad lcommand!"))))))Otherwise, we need to do something different depending on what the first command in the command list is. If it is a forward command we draw a vertical line of the forward distance. The rest of the fractal is connected to the end of the vertical line using connectends:
The recursive call to converttocurve produces the curve corresponding to the rest of the Lsystem commands.(if (isforward? (car lcommands)) (connectends (makeverticalline (getdistance (car lcommands))) (converttocurve (cdr lcommands)))
Question 3: Fill in the missing code for handling rotate commands (marked as Question 3 in the template code). You can test your code by drawing the curve that results from any list of Lsystem commands that does not use offshoots. For example, evaluating
should produce a "V".(drawcurvepoints (translate (converttocurve (makelsystemcommand (makerotatecommand 150) (makeforwardcommand .5) (makerotatecommand 120) (makeforwardcommand 0.5))) 0.3 0.7) 10000)
Question 4: Fill in the missing code for handling offshoot commands (marked as Question 4 in the template code). We have provided a procedure to make it easier to fit fractals onto the graphics window:
The code for positioncurve is in curve.ss. You don't need to modify it, but should be able to understand it.
 (positioncurve curve startx starty) — evaluates to a curve that translates curve to start at (startx, starty) and scales it to fit into the graphics window maintaining the aspect ratio (the x and y dimensions are both scaled the same amount)
Now, you should be able to draw any lsystem command list using positioncurve and the converttocurve function you completed in Questions 3 and 4. Try drawing a few interesting Lsystem command lists before moving on to the next part.
Rewriting Curves
The power of the LSystem commands comes from the rewriting mechanism. Recall how we described the tree fractal:Start: (F1)To produce levels of the tree fractal, we need a procedure that takes a list of Lsystem commands and replaces each forward command with the list of Lsystem commands given by the rule.
Rule: F1 ::= (F1 O(R30 F1) F1 O(R60 F1) F1)
So, for every command in the list:
One difficulty is that the replacement commands are a list of Lsystem commands, and we want to end up with a flat list of LSystem commands.
 If the command is a forward command, replace it with the replacement commands
 If the command is a rotate command, keep it unchanged
 If the command is an offshoot command, recursively rewrite the every command in the offshoot's command list the same way
For example, consider a simple LSystem rewriting:
Start: (F1)We want to get:
Rule: F1 ::= (F1 R30 F1)Iteration1: (F1 R30 F1)but if we just replace F1's with (F1 R30 F1) lists, we would get:
Iteration2: (F1 R30 F1 R30 F1 R30 F1)Iteration1: ((F1 R30 F1))The easiest way to fix this problem is to flatten the result. Here's how (this code is provided in lsystem.ss:
Iteration2: ((F1 R30 F1) R30 (F1 R30 F1))(define (flattencommands ll) (if (null? ll) ll (if (islsystemcommand? (car ll)) (cons (car ll) (flattencommands (cdr ll))) (flatappend (car ll) (flattencommands (cdr ll)))))) (define (flatappend lst ll) (if (null? lst) ll (cons (car lst) (flatappend (cdr lst) ll))))
Question 5: Define a procedure rewritelcommands that takes a list of Lsystem commands as its first parameter. The second parameter is a list of Lsystem commands that should replace every forward command in the first list of commands in the result. Here's the easy part:
Complete the definition of rewritelcommands.(define (rewritelcommands lcommands replacement) (flattencommands (map ; Procedure to apply to each command lcommands)))
Question 6: Define a procedure makelsystemfractal that takes three parameters: replacecommands, a list of Lsystem commands that replace forward commands in the rewriting; start, a list of Lsystem commands that describes the starting curve; level, the number of iterations to apply the rewrite rule. Hint: the ntimes function you defined in PS2 might be useful.
You should be able to draw a tree fractal using maketreefractal and drawlsystemfractal (these and the treecommands list of Lsystem commands are defined in lsystem.ss):
(define (maketreefractal level) (makelsystemfractal treecommands (makelsystemcommand (makeforwardcommand 1)) level)) (define (drawlsystemfractal lcommands) (drawcurvepoints (positioncurve (converttocurve lcommands) 0.5 0.1) 50000))
Question 7: Draw some fractals by playing with the Lsystem commands. Try changing the rewrite rule, the starting commands, level and leaf curve (in converttocurve) to draw an interesting fractal. Turn in the code you used, as well as a printout of the display window. Better Drawing
Everything beyond here is optional, but makes it possible to draw better fractals with only a little more work.The problem we observe in Problem Set 2 with our connectcurverigidly (and also connectends) not distributing the t values well is much worse for our Lsystem fractals.
If we draw (maketreefractal 5), it has 1707 different curves! The way connectends is defined, we use tvalues 0.0 through 0.5 all on the first curve, and 0.5 through 1.0 on the other 1706 curves. This procedure evaluates to the approximate number of points for the n^{th} curve:
Evaluating (exact>inexact (numpoints 100000 100)) produces 7.888609052210118e026 meaning there is less than 1 in 10^{25} chance that a single point from the 100^{th} curve is drawn!(define (numpoints p n) (if (= n 0) p (numpoints (/ p 2) ( n 1))))To fix this, we need to distribute the tvalues between our curves in a more sensible way. We have provided a procedure connectcurvesevenly in curves.ss that connects a list of curves in a way that distributes the range of t values evenly between the curves. Its a bit complicated, but you should be able to understand its definition:
To use this to draw better Lsystem fractals, you will need to replace your converttocurve procedure with a converttocurvelist procedure that keeps all the curves separate instead of connecting them directly. Then,(define (getnth list n) (if (= n 0) (car list) (getnth (cdr list) ( n 1)))) ;;; Divide t evenly among a list of curves (define (connectcurvesevenly curvelist) (lambda (t) (let ((whichcurve (if (>= t 1.0) ( (length curvelist) 1) (inexact>exact (floor (* t (length curvelist))))))) ((getnth curvelist whichcurve) (* (length curvelist) ( t (* (/ 1 (length curvelist)) whichcurve)))))))will produce a nice tree fractal. You have to write the converttocurvelist code yourself, but don't hesitate to ask for help if you are stuck on this.(define (drawlsystemfractal lcommands) (drawcurvepoints (positioncurve (connectcurvesevenly (converttocurvelist lcommands)) 0.5 0.1) 50000))
Credits: This problem set was created by Dante Guanlao, Jon Erdman and David Evans.
University of Virginia Department of Computer Science CS 200: Computer Science 
David Evans evans@cs.virginia.edu 