About a week ago Rick Minerich made this blog post about an ant colony simulation in F#. I downloaded the code and played with it, I liked the simulation a lot but wasn’t too keen on the implementation – it used lots of thread and lots of mutable data. So I decided to dig around a tweak a little here and there, but before I knew it I’d rewritten the whole thing using immutable data structures. I also end up breaking the thing and having to mail Rick for help about why it was broken, anyway with a little help from him I managed to figure out the problem and finish the implementation.
The key to making it work with immutable data types was to use F# record types. These have a nice property that you can recreate them using a special “with” syntax, for example if we have a record type defined as:
/// represents an ant that moves within the territory
type Ant =
{ foodCarried: int
xloc: int
yloc: int
oldloc:list<int*int>
homeward: bool }
We can and we just want to changes its xloc and yloc we can do this by saying:
let ant = { ant with xloc = i; yloc = j; }
Because our structures are immutable we can no longer update a value at the top level, to get round this problem we end up passing all state into each function call. This is actually less incovient than it sounds; we defined an “environment” record to contain all the state we need so we have less identifiers to pass round:
type Env =
{ ants: seq<Ant>;
territory: Map<int*int, TerritoryNode> }
We also now rely heavily on the fact we can return more than one item from a function, as shown in this section taken from the “ANTHuntByPheromone” function:
let _,env,ant = ANTMoveRandomly env ant
So what’s the advantage in this approach? Now the algorithm no longer uses any shared mutable data we can run it in parallel very easily and have several ant simulations running at the same time:
So what’s the next step? I’ve been talking to Rick about an implementation use asynchronous workflows to move the ants and a number of “mailboxes” to represent the nodes that the ants live on. I think this could be very interesting stuff, so expect to see more in the future.
Here is a copy of the code of the current version:
#light
open Microsoft.FSharp.Control.CommonExtensions
open Microsoft.FSharp.Control.Mailboxes
open System
open System.Drawing
open System.Windows.Forms
open System.Collections.Generic
//Program constants
let antHome = 8;; //X by X in Size
let maxFoodPerSquare = 1000
let chanceFoodPerSquare = 80
let antMaxFoodCarried = 50
let antPheromoneDrop = 200
let antPheromoneDropNoFood = 0
let tickLength = 50
let antAdventurousness = 3 //Lower is more adventurous
let antWanderlust = 4 //Lower is lustier
let territoryHeight = 80
let territoryWidth = 80
let pixelWidth = 6
let pixelHeight = 6
let colonyTiles = 5
let _RandomGen = new System.Random();;
/// represents a single node in the ants territory
type TerritoryNode =
{ food: int;
pheromone: int;
isHome: bool
hasAnt: bool }
with
/// creates a new node with the default values
static member newNode home =
{ food = 0;
pheromone = 0;
isHome = home;
hasAnt = home }
/// creates a new node with food
static member newNodeFood f home =
{ food = f;
pheromone = 0;
isHome = home;
hasAnt = home; }
/// test if the node has food
member n.hasFood = n.food > 0
/// update where the pheromone value
member n.updatePheromone food =
if food then
{ n with pheromone = n.pheromone + antPheromoneDrop }
else
{ n with pheromone = n.pheromone + antPheromoneDropNoFood }
/// an ant arrives in the node
member n.antArrive () =
{ n with hasAnt = true }
/// an ant leaves the node
member n.antLeave () =
{ n with hasAnt = false }
/// represents an ant that moves within the territory
type Ant =
{ foodCarried: int
xloc: int
yloc: int
oldloc:list<int*int>
homeward: bool }
with
/// creates an new instance of an ant with the default values
static member newAnt x y =
{xloc = x; yloc = y; foodCarried = 0; oldloc = []; homeward = false; }
/// The environment - represents both the ants and the nodes they move within
type Env =
{ ants: seq<Ant>;
territory: Map<int*int, TerritoryNode> }
with
/// initalize the environment with the default values
static member initalize() =
//create a list of points to represent the grid
let points =
seq { for i in [0 .. territoryWidth - 1] do
for j in [0 .. territoryHeight - 1] do
yield i,j }
// randomly creat a node
let createNode i j =
let randomFoodDropValue = _RandomGen.Next( 0, chanceFoodPerSquare ) in
let home = i <= antHome && j <= antHome
if randomFoodDropValue = 5 then
TerritoryNode.newNodeFood ( _RandomGen.Next( 0, maxFoodPerSquare ) ) home
else
TerritoryNode.newNode home
// create a map of points to node values
let teritory = Seq.fold (fun acc (i,j) -> Map.add (i,j) (createNode i j) acc) Map.empty points
// create the list of ants
let antList =
seq { for i in [0 .. antHome] do
for j in [0 .. antHome] do
yield Ant.newAnt i j }
// return the environment
{ ants = antList;
territory = teritory }
/// replaces a node in the teritory with a new one
member x.replaceNode loc newNode =
if x.territory.ContainsKey(loc) then
let newTerr = x.territory.Remove(loc)
{ x with territory = newTerr.Add(loc, newNode) }
else
{ x with territory = x.territory.Add(loc, newNode) }
/// the direction that the ants travel in
type Direction = North | East | South | West
/// make an ant drop food some food
let TNDropFood node ant =
if (node.food = maxFoodPerSquare) then
false, node, ant
else
let space = maxFoodPerSquare - node.food in
if (space <= ant.foodCarried) then
true, { node with food = node.food + space }, { ant with foodCarried = ant.foodCarried - space}
else
true, { node with food = node.food + ant.foodCarried }, { ant with foodCarried = 0 }
/// remove the appropreate amount of food from a node
let TNGetFood (node:TerritoryNode) f =
if node.hasFood then
let food = node.food in
if food >= f then
{ node with food = food - f },
f
else
let retFood = node.food in
{ node with food = 0 },
retFood
else node, 0
/// test that a node is within the grid
let GTIsNodeReal i j =
0 < i && i < territoryWidth &&
0 < j && j < territoryHeight
/// attempt to move ant to a new node and return false if we can't
let ANTGoToNode env ant i j =
if GTIsNodeReal i j then
let node = env.territory.[(i,j)]
if not node.hasAnt then
let oldNode = env.territory.[(ant.xloc,ant.yloc)]
//printfn "i %i j %i xloc %i yloc %i " i j ant.xloc ant.yloc
//if not oldNode.hasAnt then
// System.Diagnostics.Debugger.Break()
let env = env.replaceNode ((ant.xloc,ant.yloc)) (oldNode.antLeave())
let env = env.replaceNode ((i,j)) (node.antArrive().updatePheromone(ant.foodCarried > 0))
true, env, { ant with xloc = i; yloc = j; oldloc = (ant.xloc,ant.yloc) :: ant.oldloc }
else
false, env, ant
else
false, env, ant
/// send an ant off in a given direction, trying the opersite direction if we can't
let ANTGoInDirection (env:Env) ant d =
let x1,y1, x2, y2 =
match d with
| North -> ant.xloc, ant.yloc - 1, ant.xloc, ant.yloc + 1
| South -> ant.xloc, ant.yloc + 1, ant.xloc, ant.yloc - 1
| East -> ant.xloc - 1, ant.yloc, ant.xloc + 1, ant.yloc
| West -> ant.xloc + 1, ant.yloc, ant.xloc - 1, ant.yloc
let moved,env,ant = ANTGoToNode env ant x1 y1
if not moved then
ANTGoToNode env ant x2 y2
else true, env, ant
/// move an ant in a random direction
let ANTMoveRandomly (env:Env) ant =
let dir =
match _RandomGen.Next( 0, 4 ) with
| 0 -> North | 1 -> East | 2 -> South | 3 -> West | _ -> assert false
ANTGoInDirection env ant dir
/// make the ant hunt according to the pheromone surrounding it
let ANTHuntByPheromone (env:Env) ant =
let rnd = _RandomGen.Next( 0, antAdventurousness ) in
let env, ant =
if ( rnd = 0 ) then
let _,env,ant = ANTMoveRandomly env ant
env, ant
else env, ant
let southPheromone =
if GTIsNodeReal ant.xloc (ant.yloc + 1) then
env.territory.[(ant.xloc,ant.yloc + 1)].pheromone
else 0
let westPheromone =
if GTIsNodeReal (ant.xloc + 1) ant.yloc then
env.territory.[(ant.xloc + 1,ant.yloc)].pheromone
else 0
//printfn "%i %i" southPheromone westPheromone
if (southPheromone = westPheromone ) then
//printfn "random"
ANTMoveRandomly env ant
else
if (southPheromone > westPheromone ) then
//printfn "sout"
ANTGoInDirection env ant South
else
//printfn "west"
ANTGoInDirection env ant West
/// main encoding of ants behavior
let ANTBehave (env:Env) (ant: Ant) =
let x, y = ant.xloc, ant.yloc
let curNode = env.territory.[(x,y)] in
if ant.foodCarried > 0 then
if curNode.isHome then
let isDropped, curNode, ant = TNDropFood curNode ant in
let env = env.replaceNode ((x, y)) curNode
let _, env, ant =
if not isDropped then
ANTMoveRandomly env ant
else
false, env, ant
env, ant
else
let rnd = _RandomGen.Next( 0, antWanderlust ) in
let _,env,ant =
if rnd = 0 then
ANTGoInDirection env ant North
else if rnd = 1 then
ANTGoInDirection env ant East
else
if ant.xloc > ant.yloc then
ANTGoInDirection env ant East
else
ANTGoInDirection env ant North
env, ant
else
let _, env, ant =
if curNode.hasFood then
if curNode.isHome then
ANTMoveRandomly env ant
else
let tillMaxFood = antMaxFoodCarried - ant.foodCarried in
let curNode,foodGot = (TNGetFood curNode tillMaxFood) in
true,
env.replaceNode ((x,y)) curNode,
{ ant with foodCarried = ant.foodCarried + foodGot }
else
//printfn "hunt by pheromone"
ANTHuntByPheromone env ant
env, ant
/// loop though all the ants trying to move them
let ANTLoopBehave env =
let doAnts (env, acc) ant =
let envNew, antNew= ANTBehave env ant
envNew, antNew:: acc
let env, newAnts = Seq.fold (fun env ant -> doAnts env ant) (env, []) env.ants
{ env with ants = Seq.of_list newAnts }
// start of the worker mailbox statemachine ala game of life sample
type msg =
| Run
| Exit
| Pause
| Step
/// A worker automaton is a reactive automaton running on a dedicated thread of its
/// own.
type Worker() =
// Capture the synchronization context of the thread that creates this object. This
// allows us to send messages back to the GUI thread painlessly.
let callerCtxt =
match System.Threading.SynchronizationContext.Current with
| null -> null // System.ComponentModel.AsyncOperationManager.SynchronizationContext
| x -> x
//do if callerCtxt = null then failwith "Couldn't detect the synchronization context of the calling thread"
let runInGuiCtxt f =
match callerCtxt with
| null ->
// callerCtxt is null on Mono. This is a bug. System.Threading.SynchronizationContext.Current doesn't return a useful
// result. This is a little unfortunate. System.ComponentModel.AsyncOperationManager.SynchronizationContext returns
// an inconsistent result.
//
// So here we works around, where we finds the open form and sends to it.
if System.Windows.Forms.Application.OpenForms.Count > 0 then
System.Windows.Forms.Application.OpenForms.Item(0).BeginInvoke(new System.Windows.Forms.MethodInvoker(fun _ -> f())) |> ignore
| _ -> callerCtxt.Post((fun _ -> f()),null)
// This events are fired in the synchronization context of the GUI (i.e. the thread
// that created this object)
let fireUpdates,onUpdates = IEvent.create()
// Updates are generated very, very quickly. So we keep a queue, and every time we push
// into an empty queue we trigger an event in the GUI context that empties the queue and invokes
// the update event in the GUI context.
let updateQueue = new Queue<_>()
let enqueueUpdates(update) =
let first =
lock (updateQueue) (fun () ->
let first = (updateQueue.Count = 0)
updateQueue.Enqueue(update);
first)
if first then
runInGuiCtxt(fun _ ->
let updates =
lock (updateQueue) (fun () ->
[ while updateQueue.Count > 0 do
yield updateQueue.Dequeue() ])
fireUpdates(updates))
/// Compute one step of the game and call the
/// NotifyUpdates callback. That is, this function provides
/// glue between the core computation and the computation of that algorithm
let oneStep(env) =
let env = ANTLoopBehave env
let current = Seq.map (fun ant -> ant.xloc,ant.yloc) env.ants
let old = env.ants |> Seq.map (fun ant -> ant.oldloc) |> Seq.concat
let foodAnt =
env.territory.ToList()
|> Seq.of_list
|> Seq.map (fun (loc, node) -> loc, node.food)
|> Seq.filter (fun (_, food) -> food > 0)
//enqueueUpdates(old, current, foodAnt)
runInGuiCtxt(fun _ -> fireUpdates([ old, current, foodAnt]))
// clean out oldloc
let ants = Seq.map (fun ant -> {ant with oldloc = [] } ) env.ants
{ env with ants = ants }
// The control logic is written using the 'async' non-blocking style. We could also write this
// using a set of synchronous recursive functions, or using a synchronous workflow,
// but it's more fun to use the asynchronous version, partly because the MailboxProcessor type gives us a
// free message queue.
//
// Wherever you see 'return!' in the code below you should interpret
/// that as 'go to the specified state'.
let mailboxProcessor =
Mailboxes.MailboxProcessor.Create(fun inbox ->
/// This is the States of the worker's automata using a set of
/// tail-calling recursive functions.
let rec Running(s) =
async { let! msgOption = inbox.TryReceive(timeout=0)
match msgOption with
| None -> return! StepThen (SleepThen Running) s
| Some(msg) ->
match msg with
| Pause -> return! Paused s
| Step -> return! Running s
| Run -> return! Running s
| Exit -> return! Finish s }
and StepThen f s =
async { let s = oneStep(s)
return! f s }
and SleepThen f s =
async { // yield to give the GUI time to update
do! System.Threading.Timer.SleepAsync(20);
// Requeue in thread pool - strictly speaking we dont have to
// do this, but it ensures we reclaim stack on Mono and other
// platforms that do not take tailcalls on all architectures.
do! Async.SwitchToThreadPool()
return! f(s) }
and Paused(s) =
async { let! msg = inbox.Receive()
match msg with
| Pause -> return! Paused s
| Step -> return! StepThen Paused s
| Run -> return! Running s
| Exit -> return! Finish s }
and Finish(s) =
async { return () }
// Enter the initial state
Running (Env.initalize()))
/// Here is the public API to the worker
member w.RunAsync () = mailboxProcessor.Post(Run)
member w.StopAsync() = mailboxProcessor.Post(Pause)
member w.ExitAsync() = mailboxProcessor.Post(Exit)
member w.StepAsync() = mailboxProcessor.Post(Step)
member w.Updates = onUpdates
member w.Start() = mailboxProcessor.Start()
/// make and show the form
let main() =
let form = new Form(Visible=true,Text="Ant Colony",Width=800, Height=600)
form.Visible <- true
for x in [ 0 .. 1 ] do
for y in [ 0 .. 1 ] do
let bitmap = new Bitmap(territoryWidth, territoryHeight, System.Drawing.Imaging.PixelFormat.Format24bppRgb)
let pb = new PictureBox(SizeMode=PictureBoxSizeMode.Zoom)
pb.Size <- new Size(300, 300)
pb.Location <- new Point( x * 310, y * 310)
pb.Image <- bitmap
form.Controls.Add( pb )
let worker = new Worker()
worker.Updates.Add(fun updates ->
for (old, current, foodAnt) in updates do
old |> Seq.iter (fun (x,y) -> bitmap.SetPixel(x,y, Color.Black) )
current |> Seq.iter (fun (x,y) -> bitmap.SetPixel(x,y, Color.Red))
foodAnt |> Seq.iter (fun ((x,y), food) ->
let c = Color.FromArgb( food / maxFoodPerSquare * 255, Color.Green)
bitmap.SetPixel(x,y, c))
pb.Invalidate() )
worker.Start()
form.Activate()
Application.Run(form)
[<STAThread>]
do main()
Feedback:
Feedback was imported from my only blog engine, it's no longer possible to post feedback here.
re: Ants everywhere! An ant colony simulation in F# - Derek
I'm working on a simulation in F# and have been struggling to come to grips with immutability vs my old oop way of thinking.
I originally started out with mutable Lists and Dictionaries, but then stopped and tried to figure out how to do this with immutability in mind. I couldn't come up with anything I thought was reasonable, and arrived at a similar solution to what you've done here. A big global structure that gets recreated every time something changes.
But isn't this pretty inefficient, aren't we making a copy of the whole environment every time an ant moves or does something? Is there some optimization of using 'with' that reduces all the copying of everything that didn't change?
I'm still pretty new to F# and the functional way of thinking.
Email directly if you feel like it.
thanks, Derek
updated version - ray glover
Hi there! I'm learning F# too. I updated your simulation for F# 2.0, with some small additions. Keep on coding!
re: Ants everywhere! An ant colony simulation in F# - Robert Pickering
You can find the latest version here: https://github.com/Rickasaurus/F--Ant-Colony