
NetLogo is one of my favourite simulation tools, as I have said in a previous tutorial. It is so easy to use that children can program in it, and very sophisticated functionality can be delivered with surprisingly little code.
However, this ease of use has attracted scorn from some quarters, with one blogger even suggesting that “using NetLogo signals that you don’t know how to do real programming.” A colleague of mine once expressed a similar sentiment. Nothing could be further from the truth – such comments are like suggesting that The Lord of the Rings is unsuitable for adults simply because children enjoy it.
In fact, NetLogo incorporates much of the power of Lisp. In this tutorial, I will highlight the power of the list operators foreach
, map
, and filter
, through two classic list-processing case studies: the Eight Queens puzzle (illustrated above) and the Sieve of Eratosthenes. NetLogo also includes a reduce
operator, which allows non-parallel versions of the MapReduce style of programming.
Eight Queens
The Eight Queens puzzle requires placing eight queens on a chessboard so that none threaten each other – i.e. so that none are in the same row, column, or diagonal. Following a relatively standard approach, we will place exactly one queen in each column, representing a solution as a list of row positions, indexed from 1. The picture above shows the solution [1 5 8 6 3 7 2 4], for example. Here the column positions [1 2 3 4 5 6 7 8] are implicit.
We can add a new queen to such a list with the lput
operator (fput
and lput
are equally efficient in NetLogo 5.0, so there is no longer any need to prefer fput
). To test the (column,row) pairs (i,j) and (m,n) for mutual threats, we check for queens in the same row or diagonal (our placement strategy will guarantee exactly one queen per column):
to-report add-queen [ lst n ]
report lput n lst
end
to-report threatens [ i j m n ]
report (j = n) or (i + j = m + n) or (i - j = m - n)
end
We can test a new row position n
for compatibility with an existing partial solution lst
by calculating the column the new queen will be in: ((length lst) + 1)
. We then use a foreach
loop to iterate through the existing solution. The command block inside this loop refers to the current list element as ?
. Simultaneously, we use i
to iterate through the column positions in the existing solution. The combination is safe if we find no threats:
to-report safe [ lst n ]
let m ((length lst) + 1)
let i 1
foreach lst [
if (threatens i ? m n) [ report false ]
set i (i + 1)
]
report true
end
The main reporter takes two arguments: the number of queens to place (n
) and the number of rows (usually 8). It returns a list of all possible solutions: a list of lists. If n = 0
, there is one way of placing zero queens (the sole solution is represented by the empty list). Otherwise, we recursively place n - 1
queens (giving lst
), and for each solution in that list try all possible rows in the next column (using a while
loop). All solutions that pass the safety check are accumulated in res
, which is the value returned:
to-report queen-solutions [ n num-rows ]
if-else (n = 0)
[ report [ [] ] ]
[ let lst (queen-solutions (n - 1) num-rows )
let res []
foreach lst [
let i 1
while [i <= num-rows] [
if (safe ? i) [ set res (lput (add-queen ? i) res) ]
set i (i + 1)
]
]
report res
]
end
At the starting level, we pass in the same number (usually 8) in both arguments. We also need a global variable to store a list of solutions:
to-report all-queen-solutions [ n ]
report (queen-solutions n n)
end
globals [
queens
scan
]
The Queens button executes the following code. For 8 queens, generating all the solutions takes about 0.1 seconds, and this is done only on the first button-press. Subsequent button-presses iterate through the solution list. We assume that the world is set up so that the bottom left patch is (0,0). The size of the world can be changed, as long as it remains square.
;; code for QUEENS button
to queens-go
;; remove any old turtles
ask turtles [ die ]
;; set up chessboard pattern
ask patches [
if-else ((pxcor - pycor) mod 2 = 0) [ set pcolor white ] [ set pcolor black ]
]
;; the first time the QUEENS button is pressed, calculate list of all solutions
if (queens = 0) [
reset-timer
set queens (all-queen-solutions world-width)
type (length queens) print " solutions"
type timer print " seconds taken"
set scan 0
]
;; every time the QUEENS button is pressed, display one solution
;; increment scan as an index into the list of all solutions
let q (item scan queens)
set scan (scan + 1)
type "solution #" type scan type ": " print q
if (scan = (length queens)) [ set scan 0 ]
;; display a solution by placing turtles in the world
;; convert coordinates assuming bottom left corner of world is (0,0)
let i 1
foreach q [
crt 1 [
setxy (i - 1) (world-width - ?)
set shape "face happy"
set color orange
set size 0.8
]
set i (i + 1)
]
end
;; RESET button erases everything
to reset
clear-all
reset-ticks
end
Sieve of Eratosthenes
The Sieve of Eratosthenes finds all prime numbers up to a specified maximum. We again show the result by placing turtles, so that the top row of the image above displays [2 3 5 7]. We begin with a utility function to generate a list of numbers from m
to n
:
to-report sequence [ m n ]
let res []
let i m
while [i <= n] [
set res (lput i res)
set i (i + 1)
]
report res
end
The main sieving code takes a (non-empty) list lst
(starting with the prime fst
) and a maximum number maxn
. If fst
is large enough, we are done. Otherwise, we use filter
to remove all multiples of fst
, by forming the list a
of entries satisfying [? mod fst > 0]
. We then recursively apply sieve
to that list, giving b
. The final result puts fst
back on the front of b
:
to-report sieve [ lst maxn ]
;; assume lst is not empty and that first element is prime (we will start with 2)
let fst (first lst)
if-else (fst * fst > maxn)
[ report lst ]
[ let a (filter [? mod fst > 0] lst)
let b (sieve a maxn)
report (fput fst b) ]
end
Two additional utility functions extract column and row positions for a prime, again assuming that the world is set up so that the bottom left patch is (0,0):
to-report latitude [ n ]
report ((n - 1) mod world-width)
end
to-report longitude [ n ]
report ((world-width - 1) - floor ((n - 1) / world-width))
end
The Primes button executes the following code. The main activity is a call to sieve
. We use the two utility functions to get a list of all column positions and a list of all row positions, using map
to apply a tranforming reporter to all elements of a list (we show two different ways of calling map
). Finally, when foreach
is wrapped in brackets it can iterate through multiple lists simultaneously. We use that here to iterate through the row and column positions for the primes:
;; code for PRIMES button
to primes-go
;; remove any old turtles
ask turtles [ die ]
;; set up chessboard pattern
ask patches [
if-else ((pxcor - pycor) mod 2 = 0) [ set pcolor 48 ] [ set pcolor 28 ]
]
;; calculate the primes
let maxn (world-width * world-height)
let numbers (sequence 2 maxn)
let primes (sieve numbers maxn)
type "primes " print primes
;; place turtles at prime positions
;; brackets identify the multi-argument version of foreach
;; we illustrate two different ways of using map
(foreach (map latitude primes) (map [longitude ?] primes) [
crt 1 [
setxy ?1 ?2
set shape "circle"
set color blue
set size 0.8
]
])
end
The complete NetLogo file can be found at Modeling Commons.
Like this:
Like Loading...