fractal and the beauty of nature
TRANSCRIPT
DM 502 Programming A
Specification
This program is the implementation of the requirements specified by the “DM 502 Programming
A” document. It implements the functionalities described by the “Fractals and the Beauty of Nature”
section.
The goal is to produce a program that draws a fractal which characteristics are described in a .fld file.
This file describes the initial states of the iterative fractal procedure along with the core of the
computation loop. This loop defines the fractal process expanding from the current step from the previous
step.
After the final step completes, the last state describes the resulting fractal. Each state value is associated
to a given drawing command that is used to issue the fractal’s picture.
Here is a sample of the drawing resulting from the specification found in the “Koch.fdl” file:
Design
Using the same bottom-up approach as for testing, the program was carefully design in small
steps. Starting from the necessary tools needed to complete the task, it grew until all was combined with
the proper abstractions being in place.
Some simple data types have been introduced to encapsulate the necessary data.
1. Fractal data type
The Fractal data type holds all the information and behavior associated with the fractal computation
and settings.
It encapsulates the functions that are used to compute a given step from a previous step and the main
fractal loop. The two elements are clearly separated to properly abstract each computation unit.
It also contains the binding with the execution of the final “drawing commands” from the final state of
computation obtained after everything completed.
2. Command data type
The Command data type is responsible for interpreting a given command and dispatching it to the
necessary drawing commands to the turtle functions. It acts as a small interpreter for commands.
3. Rule data type
The Rule data type is responsible for encapsulating a given rule. It parses an initialization string that
is supposed to represent the text description of a rule and offers an “apply” function that from a given
input (list of single characters) produces a list of characters after the rule has be properly applied to each
list member.
A function “read_fdl” has also been added that is used to bridge the configuration file (.fdl file) with the
proper runtime environment. It parses the configuration file and properly initializes a Fractal object with
the corresponding rules, commands and settings.
Implementation
The program has been written by a careful partitioning of roles and responsibilities among different
classes.
1. Rule data type
The Rule data type is a class that takes a rule representation in its constructor. This representation
corresponds to the rule description taken from the configuration file e.g. 'F -> F L F L'.
The input string is split and the left and right side (from the arrow’s perspective) are kept into two
separate object variables for further reference.
The rule application function is the used with a list comprehension to generate a list based on the input list
with the characters for which the rule applies replaced by the right side of the rule.
2. Fractal data type
The Fractal data type is a class the basically wraps everything that is needed to compute a given
fractal given the description found in a configuration file.
It computes a given step from the previous one applying the necessary rule to all the elements of the
current state and contains the main loop.
On interesting trick is being used here.
The Fractal class actually contains a “length” parameter that is used by the commands to scale up and
down the drawings. This parameter is a plain value and has to be passed to command objects that might
actually change its value. In Python, parameters are passed by value (the value being the reference to the
object in case of an object being passed). So in order to allow the length parameter to be changed, a list of
size one is used. The corresponding object has his reference being passed by value to the commands and
the value that it contains can then be modified.
A dictionary is used to hole the mapping between a given fractal character and the associated command.
3. Command data type
The Command data type is responsible for making the connection between a given binding between a
command and the resulting turtle action.
Testing
A bottom-up approach has been chosen to perform the tests. Each function and individual piece
of functionality has been tested independently before being assembled as the end product.
For each piece of functionality, a careful set of input data has been chosen to check if the basic
functionality was working properly. Once these passed, a sample of tests has been chosen for the edge
cases to give a basic guarantee regarding the safety of the function.
For example, here is a function that expects a list of lists and flattens it:
def flatten (l):
flattened_list = []
for element in l:
flattened_list.extend (element)
return flattened_list
It has been tested against the following valid inputs:
> flatten ([[1], [1, 2, 3]])
[1, 1, 2, 3]
> flatten ([[1, 2, 3], [1, 2, 3]])
[1, 2, 3, 1, 2, 3]
Some edge cases have been used to further test the behavior of the function:
> flatten ([[], [1, 2, 3]])
[1, 2, 3]
> flatten ([[], []])
[]
The same idea has been applied to check that:
- the Fractal class was properly initialized from a given “.fdl” file,
- a given command was properly parsed and executed,
- a given rule was properly parsed and executed,
Conclusion
All the fractal shapes have been tested and the results are really satisfying. The program performs
properly and without crashes.
Here is a sample of the construction process for the “tree.fdl” file:
Figure 1 Result for "tree.fdl" file
Appendix # Project - Fractal and the beauty of nature
# basic turtle commands
def lt (turtle, n):
turtle.lt (n)
def fd (turtle, n):
turtle.forward (n)
def bk (turtle, n):
turtle.backward (n)
def rt (turtle, n):
turtle.rt (n)
# the rule class that exposes a way to apply a given rule
# to a list of elements
class Rule(object):
# initializing the rule based on a description string (e.g. 'F -> F L F L')
def __init__(self, repr):
# the left and right part of the rule in 'F -> F L F L'
self.left, self.right = [a.strip() for a in repr.split ('->')]
if self.left is None or self.right is None:
print ('Invalid rule description')
def apply_rule_on_elements (self, elements):
return [self._apply_rule_for_element (element) for element in elements]
# helper function that only works on one element at a time
def _apply_rule_for_element (self, element):
if element == self.left:
return self.right.split ()
return element
# a very simple helper function, handy in some cases
# this allows one to perform assignment multiple values & grouping
# e.g.
# a, b = split_command ('s', [1, 2, 3, 4])
def split_command (command, *args):
return command, args
# A command class tha wraps the execution of a given command
class Command (object):
def __init__(self, command):
# the name and number of arguments for the given command
self.name, self.args = split_command (*command.split ())
def execute (self, turtle, length):
if self.name == "lt":
lt (turtle, int (self.args[0]))
elif self.name == "scale":
length[0] = length[0] * float (self.args[0])
elif self.name == "fd":
fd (turtle, length[0])
elif self.name == "bk":
bk (turtle, length[0])
elif self.name == "rt":
rt (turtle, int (self.args[0]))
elif self.name == "nop":
pass
# the main Fractal class
class Fractal (object):
def __init__(self):
# initial & current state
self.state = []
# rules associated with the current fractal
self.rules = []
# commands associated with the current fractal
self.commands = {}
# current lenght argument
# a little trick here is used to allow the modification of the length
# variable outside of the Fractal class (by the command class)
# since values are immutable and passed by value, we use an array (passed by reference
value)
# to allow the modification of the variable
self.length = [0]
# current depth
self.depth = 0
# executes the command associated w/ the current states stored in the fractal
def execute_commands (self, turtle, states):
for state in states:
self.commands[state].execute (turtle, self.length)
# flattens a list
def _flatten (self, l):
flattened_list = []
for element in l:
flattened_list.extend (element)
return flattened_list
# compute the fractal, does the actual iteration work
# returns the state of the fractal after the computation
def compute (self):
current_depth = self.depth
current_state = self.state
while self.depth != 0:
current_state = self._compute_next_state (current_state)
self.depth -= 1
return current_state
def _compute_next_state (self, state):
for rule in self.rules:
state = rule.apply_rule_on_elements (state)
return self._flatten (state)
# parse the fdl file, creates a fractal and set it up with the values
# read in the fdl file
def read_fdl (filename):
import os
f = Fractal ()
if os.path.exists (filename):
lines = open (filename).readlines ()
for line in lines:
if not len (line.strip()) == 0:
name, arguments = split_command (*line.strip ().split ())
if name == "start":
f.state = arguments
elif name == "rule":
f.rules.append (Rule (" ".join (arguments)))
elif name == "length":
f.length = [int (arguments[0])]
elif name == "depth":
f.depth = int (arguments[0])
elif name == 'cmd':
f.commands[arguments[0]] = Command(" ".join (arguments[1:]))
else:
print ("File does not exist")
# no check is made, to see if we have a fracal that was completely initialized
return f
# run the program and expect the name of the fractal file (.fdl) as the first
# argument on the command line
import sys
import turtle
if len (sys.argv) > 1:
f = read_fdl (sys.argv[1])
f.execute_commands (turtle, f.compute ())