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
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.
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 (
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
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
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
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.
Are you aware of any efforts to combine NetLogo with GPU parallel processing? NVIDIA’s CUDA comes to mind as a potential candidate; free, easily available and runs on many graphics cards (though mostly NVIDIA cards).
I would love to be able to runs my models faster or w/more agents/turtles
Not that I’m aware of. However, as my other set of posts discusses, Java can be used to speed up a model substantially.
Pingback: Agent goals in NetLogo: a list tutorial | Scientific Gems
Pingback: NetLogo post summary | Scientific Gems
Pingback: List processing in NetLogo: some more examples | Scientific Gems