F# Flocking (boids)

A couple of years ago I was blogging about some simple boid behaviour I had implemented in C# (here). I came across this paper which had some interesting ideas I wanted to try out: “Self-organised complex aerial displays of thousands of starlings: a model” by H. Hildenbrandt, C. Carere, C-K. Hemelrijk.

As I’m getting more into F#, I thought this would be a simple reasonable sized application to create to get a feel for how F# works on a slightly larger project (though definitely not large-scale).

Before taking a look at the code, I’ll quickly describe which parts of the paper I implemented.

The video generated is available on http://www.youtube.com/watch?v=eil5K7Ir3i8

I haven’t implemented all of the ideas in the paper, so my simple application doesn’t exhibit the same realistic flocking behaviour, but it does have more realistic behaviour than my earlier efforts. The interesting behaviours were the desire to not stray too far from the roost, the attempt to maintain a steady cruise speed, and to use the nearest seven topological neighbours for cohesion and alignment. I implemented the simulation in 2D for simplicity.

I hardcoded a perception radius instead of implementing the continual adjustment of the perception radius for seven topological neighbours. I also have totally omitted the flight dynamic simulation (no gravity, no banked turns). The paper discusses that the banked turns gives a further air of realism to the simulation.  

The coding

This was a real pleasure to implement in F# - a lot less time was thinking about application design, classes and their interactions, the logic in the paper was easy to transfer directly to the keyboard. F# is very compact, so the core logic can be seen on just a couple of screens.

I didn’t feel the lack of intellisense too much when developing the core algorithm, I did miss the ability to refactor functions and navigate to usages. I especially missed intellisense when developing the WPF visualisation part of the app, when interacting with .NET objects; I did miss the ability for Visual Studio to automatically add the required namespaces. I must have being spoilt by Visual Studio and Resharper for too long!

The actual WPF application wasn’t such a good fit for F# – there’s no generated code behind file for the XAML, and I feel that using F# would be painful in a WPF or Silverlight application (but just for the view, it should be OK for the ViewModels down).

I implemented a F# Vector type, which can be specified with units of measure (or none). I used units of measure throughout – this was really powerful, and did let me quickly find a few bugs in the implementation.

(NOTE: I still haven’t found a code websnippet tool I’m happy with – you need to click in each of the regions below and scroll down and right to see the whole code). Alternately, the zipped up solution can be downloaded from http://www.taumuon.co.uk/jabuka/FSharpFlock.zip 

Vector3D.fs:

module Vector3D  
  
type Vector3D<[<Measure>] 'u>(x : float<'u>, y : float<'u>, z : float<'u>) =   
    static member Zero() = Vector3D<_>(0.0<_>, 0.0<_>, 0.0<_>)  
  
    member v.X = x  
    member v.Y = y  
    member v.Z = z  
  
    static member (+) (lhs:Vector3D<_>, rhs:Vector3D<_>) =  
      Vector3D(lhs.X + rhs.X, lhs.Y + rhs.Y, lhs.Z + rhs.Z)  
  
    static member (-) (lhs:Vector3D<_>, rhs:Vector3D<_>) =  
      Vector3D(lhs.X - rhs.X, lhs.Y - rhs.Y, lhs.Z - rhs.Z)  
  
    static member (*) (v:Vector3D<_>, a:float<_>) =  
      Vector3D(v.X * a, v.Y * a, v.Z * a)  
  
    static member (*) (a:float<_>, v:Vector3D<_>) =  
      Vector3D(a * v.X, a * v.Y, a * v.Z)  
        
    static member (/) (v:Vector3D<_>, a) =  
      Vector3D(v.X / a, v.Y / a, v.Z / a)  
  
    member v.DotProduct(rhs:Vector3D<_>) = (v.X * rhs.X) + (v.Y * rhs.Y) + (v.Z * rhs.Z)  
  
    member v.magnitude = sqrt(v.DotProduct(v)) * 1.0<_>  
  
    member lhs.CrossProduct(rhs:Vector3D<_>) =  
      Vector3D((lhs.Y * rhs.Z - lhs.Z * rhs.Y) * 1.0<_>,  
          (-lhs.X * rhs.Z + lhs.Z * rhs.X) * 1.0<_>,  
          (lhs.X * rhs.Y - lhs.Y * rhs.X) * 1.0<_>)  
  
    member v.normalise =   
      let magnitude = float v.magnitude  
      Vector3D<_>((v.X / magnitude), (v.Y / magnitude), (v.Z / magnitude))  
  
let sumVectors(vectors : Vector3D<_>[]) =   
  let initial = Vector3D<_>(0.0<_>, 0.0<_>, 0.0<_>)  
  Array.fold (+) initial vectors

BoidUtils.fs:

module BoidUtils  
  
open Microsoft.FSharp.Math  
open Vector3D  
open SI  
  
let radiusRoost = 150.0<m>  
let hardRadius = 2.0<m> // 0.2<m>  
let mass = 0.08<kg>  
let timeStep = 0.005<s>  
let relaxationTime = 0.05<s>  
let cruiseSpeed = 20.0<m/s>  
let horizontalRoostWeighting = 0.01<N/m>  
let weightingAlignment = 0.5<kg * s^-2>  
let weightingCohesion = 1.0<kg s^-2>  
let weightingSeparation = 2.0  
let perceptionRadius = 50.0<m>  
  
type BodyAxes =  
  { Forward:Vector3D<1>;  
    Side:Vector3D<1>;  
    Up:Vector3D<1> }  
  
type Boid =   
  { Position:Vector3D<m>;  
    Speed:float<m / s>;  
    Orientation:BodyAxes; }  
  
// All parameterless functions are evaluated once, just on module opening, so pass random in.  
let InitialiseRandomPosition(rand:System.Random) =   
  Vector3D<m>((300.0 * (rand.NextDouble()-0.5)) * 1.0<m>,  
             ((300.0 * (rand.NextDouble()-0.5)) * 1.0<m>),   
             0.0<m>)  
  
let InitialiseRandomVelocity(rand:System.Random) =   
  Vector3D<m/s>((100.0 * (-0.5 + rand.NextDouble()) * 1.0<m/s>),  
                (100.0 * (-0.5 + rand.NextDouble())) * 1.0<m/s>,  
                0.0<m/s>)  
  
let InitialiseRandomOrientation(rand:System.Random) =   
  {Forward=Vector3D(0.0, 1.0, 0.0);  
   Side=Vector3D(1.0, 0.0, 0.0);  
   Up=Vector3D(0.0, 0.0, 1.0)}  
  
let setOrientation(oldOrientation:BodyAxes, velocity:Vector3D<m/s>) =  
  let normalisedVelocity = velocity.normalise  
  let y = normalisedVelocity.CrossProduct(Vector3D<m / s>(0.0<m/s>, 0.0<m/s>, 1.0<m/s>))  
  {oldOrientation with Forward=normalisedVelocity * 1.0<m^-1 s>; Side=y*1.0<m^-2 s^2>}  
  
let calculateCruiseSpeedForce (boid:Boid) =  
  (mass / relaxationTime) * (cruiseSpeed - boid.Speed) * boid.Orientation.Forward  
  
let calculateRoostForce (boid:Boid) =  
  let horizontalPosition = Vector3D(boid.Position.X, boid.Position.Y, 0.0<_>)  
  let distanceFromOrigin = horizontalPosition.magnitude  
  match (distanceFromOrigin) with  
  | _ when distanceFromOrigin < radiusRoost -> Vector3D<N>(0.0<N>, 0.0<N>, 0.0<N>)  
  | _ -> let normalRoostingArea = horizontalPosition.normalise  
         let d = boid.Orientation.Forward.DotProduct normalRoostingArea  
         let distanceFromRoost = distanceFromOrigin - radiusRoost  
         let orientationRoostDotProduct = boid.Orientation.Side.DotProduct normalRoostingArea  
         let weightingFactor = match (orientationRoostDotProduct) with  
                               | _ when orientationRoostDotProduct > 0.0<m> -> -1.0  
                               | _ -> 1.0  
         weightingFactor * (radiusRoost * horizontalRoostWeighting * (0.5 + (0.5<m^-1> * d)) * (boid.Orientation.Side))  
  
let findDistanceBetweenBoids(boid:Boid, other:Boid) =   
  (boid.Position - other.Position).magnitude  
  
let findNearestNeighbours(boid:Boid, boids:Boid[]) =  
  let sortedByDistance = boids |> Array.sortBy(fun other -> findDistanceBetweenBoids(boid, other))  
  Array.sub sortedByDistance 0 7  
  
let findAverageForwardDirectionDifference(boid:Boid, boids:Boid[]) =   
  let differences = boids |> Array.map (fun i -> 1.0<m> * (i.Orientation.Forward - boid.Orientation.Forward))  
  let sumDifferences = sumVectors(differences)  
  (1.0 / (float sumDifferences.magnitude)) * sumDifferences  
  
let calculateAlignmentForce(boid:Boid, nearest:Boid[]) =  
  let averageDifference = findAverageForwardDirectionDifference(boid, nearest)  
  weightingAlignment * averageDifference  
  
let findAveragePosition(boid:Boid, boids:Boid[]) =  
  let positions = boids |> Array.map (fun i -> i.Position)  
  let sumPositions = sumVectors(positions)  
  (1.0 / float boids.Length) * sumPositions  
  
let findNeighboursInRadius(boid:Boid, boids:Boid[], radius:float<m>) =  
  boids |> Array.filter(fun other -> other <> boid && findDistanceBetweenBoids(boid, other) <= radius)  
  
let calculateCentrality(boid:Boid, boids:Boid[]) =   
  let separations = boids |> Array.map(fun i -> (i.Position - boid.Position).normalise)  
  let sumSeparations = sumVectors(separations)  
  let count = boids.Length  
  match (count) with  
  | 0 -> 1.0  
  | _ -> (1.0 / float count) * (sumSeparations.magnitude / 1.0<m>)  
  
let calculateCohesionForce(boid:Boid, nearest:Boid[], boidsInPerceptionRadius:Boid[]) =  
  let boidsOutsideHardRadius = nearest |> Array.filter(fun i -> abs ((boid.Position - i.Position).magnitude) > hardRadius)  
  let centrality = calculateCentrality(boid, boidsInPerceptionRadius)  
  let averagePosition = findAveragePosition(boid, nearest)  
  centrality * weightingCohesion * (averagePosition - boid.Position)  
  
let calculateSeparationForce(boid:Boid, boidsInPerceptionRadius:Boid[]) =   
  let nearest = boidsInPerceptionRadius  
  let separations = nearest |> Array.map(fun i -> i.Position - boid.Position)  
  let sigma = 1.8  
  let forcesToNeighbours = separations |> Array.map(fun i ->   
    let magnitude = i.magnitude  
    let multiplier =   
        match (magnitude) with  
        | _ when magnitude < hardRadius -> 1.0  
        | _ -> System.Math.Exp(-((magnitude - hardRadius)*(magnitude - hardRadius)/1.0<m^2>) / (sigma * sigma))  
    multiplier * magnitude * (i.normalise) * 1.0<kg * m^-1 * s^-2>)  
  let sumForces = sumVectors(forcesToNeighbours)  
  match (nearest.Length) with  
    | _ when (nearest.Length) = 0 -> Vector3D<N>.Zero()  
    | _ -> (-weightingSeparation / float nearest.Length) * sumForces  
  
let calculateSocialForce(boid:Boid, boids:Boid[]) =  
  let nearest = findNearestNeighbours(boid, boids)  
  let boidsInPerceptionRadius = findNeighboursInRadius(boid, boids, perceptionRadius)  
  calculateAlignmentForce(boid, nearest)  
  + calculateCohesionForce(boid, nearest, boidsInPerceptionRadius)  
  + calculateSeparationForce(boid, boidsInPerceptionRadius)  
  
let calculateForce (boid:Boid, boids:Boid[]) =  
  (boid |> calculateRoostForce)  
  + (boid |> calculateCruiseSpeedForce)   
  + (calculateSocialForce(boid, boids))  
  
let iterateBoid (boid:Boid, boids:Boid[]) =   
  let originalPosition = boid.Position  
  let originalVelocity = boid.Speed * boid.Orientation.Forward  
  let force = calculateForce(boid, boids)  
  let acceleration = force/mass  
  let velocity = originalVelocity + (acceleration * timeStep)  
  let position = originalPosition + (velocity * timeStep)  
  let newOrientation = setOrientation(boid.Orientation, velocity)  
  {Position=position;Speed=velocity.magnitude;Orientation=newOrientation}  

Program.fs:

module Boids  
  
open SI  
open Vector3D  
open BoidUtils  
  
open System  
open System.IO  
open System.Windows  
open System.Windows.Threading  
open System.Windows.Controls  
open System.Windows.Shapes  
open System.Windows.Media  
open System.Windows.Media.Imaging  
  
let window = Application.LoadComponent(new System.Uri("/FSharpFlock;component/MainWindow.xaml",  
                                           System.UriKind.Relative)) :?> Window  
  
let rand = new System.Random()  
let viewWidth = 480.0  
let viewHeight = 360.0  
  
let mutable frameCount = 0  
let mutable boids =  
  [|for i in 0 .. 300 ->  
    let position = InitialiseRandomPosition(rand)  
    let velocity = InitialiseRandomVelocity(rand)  
    let tempOrientation = {Forward=Vector3D(0.0, 1.0, 0.0);  
                           Side=Vector3D(1.0, 0.0, 0.0);  
                           Up=Vector3D(0.0, 0.0, 1.0)}  
    let orientation = setOrientation(tempOrientation, velocity)  
    {Position = position; Speed=velocity.magnitude; Orientation=orientation}  
    |];;  
  
let updateBoids(boids) =  
  boids |> Array.map (fun boid -> iterateBoid(boid, boids))  
  
let (?) (fe:FrameworkElement) name : 'T =  
  fe.FindName(name) :?> 'T  
  
let GetRotationForBoid(boid:Boid) =  
  let forward = boid.Orientation.Forward  
  let angleRadians = Math.Atan2(forward.X, forward.Y)  
  let angleDegrees = angleRadians * 180.0 / Math.PI  
  let rotateTransform = new Media.RotateTransform(angleDegrees, 0.0, 0.0)  
  rotateTransform  
  
let color = Media.Color.FromArgb(64uy, 255uy, 0uy, 0uy)  
  
let saveFrame(canvas : Canvas) =   
  let size = new Size(viewWidth, viewHeight)  
  canvas.Measure(size)  
  canvas.Arrange(new Rect(size))  
  let renderTargetBitmap = new RenderTargetBitmap(int viewWidth, int viewHeight, 96.0, 96.0, PixelFormats.Pbgra32)  
  let sourceBrush = new VisualBrush(canvas)  
  renderTargetBitmap.Render(canvas)  
  
  let bitmapFrame = BitmapFrame.Create(renderTargetBitmap)  
  let jpegEncoder = new JpegBitmapEncoder()  
  jpegEncoder.Frames.Add(bitmapFrame)  
  let filename = System.String.Format("C:Anim{0:0000}.jpg", frameCount)  
  use stream = new FileStream(filename, FileMode.CreateNew)  
  jpegEncoder.Save(stream)  
    
let createBoidGraphics(boid:Boid) =  
  let obj : Polygon = new Polygon()  
  obj.Points <- new Media.PointCollection( [|new Point(-10.0, -5.0);  
                                             new Point(0.0, 0.0);  
                                             new Point(-10.0, 5.0);  
                                             new Point(-5.0, 0.0)|] )  
  obj.RenderTransform <- GetRotationForBoid(boid)  
  obj.Fill <- new Media.SolidColorBrush(color)  
  obj.Stroke <- Media.Brushes.Black  
  obj.StrokeThickness <- 1.0  
  obj  
  
let drawBoids() =  
  let win : Window = window  
  let canvas : Canvas = win?canvas  
  canvas.Children.Clear()  
  for i = 0 to boids.Length - 1 do  
    let graphicalBoid = createBoidGraphics(boids.[i])  
    let unitlessPosition = boids.[i].Position * 1.0<m^-1>  
    System.Windows.Controls.Canvas.SetTop(graphicalBoid, (viewHeight / 2.0) + (unitlessPosition.X))  
    System.Windows.Controls.Canvas.SetLeft(graphicalBoid, (viewWidth / 2.0) + (unitlessPosition.Y))  
    canvas.Children.Add(graphicalBoid) |> ignore  
  // saveFrame(canvas)  
  frameCount <- frameCount + 1  
  
let timer = new DispatcherTimer();  
timer.Tick.Add(fun evArgs ->   
  [0..5]  
  |> Seq.iter(fun x -> boids <- boids |> updateBoids)  
  drawBoids()  
  )  
timer.Interval <- TimeSpan.FromMilliseconds(1.0);  
  
let setup(win: Window) =  
  win.Loaded.Add(fun _ -> timer.Start())  
  
[<STAThread>]  
[<EntryPoint>]  
let main args =  
  setup window  
  (new Application()).Run(window) |> ignore  
  0

MainWindow.xaml:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
        Title="F# Flock" Height="360" Width="480"\>  
    <Canvas Name="canvas" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"   
    Background="LightBlue"/>  
</Window>

The rest of the code I hope is simple to read and understand. Coding this up I had some of the most fun programming for a while. With F#, I found I was able to concentrate on the core of the algorithm and very quickly see results.

For this type of project, very algorithmic, F# was a perfect fit. I’d like to get a feel for how to implement F# on a large scale project, and to see how it feels to expose the functionality via an object oriented layer.