This document attempts to explain the logical structure of a Swarm experiment application. Starting with a very general outline of an idealized experimental procedure, we successively increase the level of specification of each stage of this idealized structure until we arrive at details of an actual running Swarm application.
Along the way, we introduce and describe (very briefly) some of the tools currently available in Swarm to help users build experiments. The tools presented here are only suggestive: the Swarm library documentation and example applications will give a more detailed view of using specific components of Swarm.
These tools are more fully documented in the appropriate library directories. This high-level overview skips much of the detail in an effort to illustrate the logical structure of a Swarm application and to provide some motivation for why things are done the way they are in the example applications.
In the following, we will start out with a very high-level outline/abstraction of the "generic form" of an experimental procedure. Then we will increase the magnification on each piece of the outline until we reach specific details of Swarm code. If you compare the gross structure of the example applications you will see that despite the differences in detail, the overall architecture of Swarm applications adheres to the general abstraction of an experimental procedure.
The application we will use as an example is the Heatbugs application. The source code for that example is a thoroughly commented application and intended to be read by new Swarm programmers. It can be a useful template to start from: so can the very-stripped-down "template" application. We recommend that you gradually ease yourself into the Swarm perspective by experimenting with the example applications and branching out by modifying them in various ways until you catch on to the conceptual style of a Swarm Experiment.
The important part of step 6) is that the published paper includes enough detail about the experimental setup and how it was run so that other labs with access to the same equipment can recreate the experiment and test the repeatability of the results. This is hardly ever done (or even possible) in the context of experiments run in computers, and the crucial process of independent verification via replication of results is almost unheard of in computer simulation. One goal of Swarm is to bring simulation writing up to a higher level of expression, writing applications with reference to a standard set of simulation tools.
First, let's look at what happens when we port the above stages into the world of a computer. In a computer, you don't just drag the pieces of your experiment in from the outside world and hook them up. You have to create a world with space and time, a bunch of objects in that world (stuff to study and stuff to look at it with), schedules of events over those objects, all sorts of computer widgetry to interact with that artificial world and to manage multiple experimental runs and the data that they generate, and so forth. In other words, in a computer, one usually has to first *create* from scratch all of the bits and pieces of the experimental setup - the virtual equivalent of beakers, bunsen burners, microscopes etc.
Perhaps the most important difference between an experiment in the "real" world and an experiment inside of a computer is the nature of time. In the real world, everything in one's experimental setup is moved forward in time via a very concurrency courtesy of the laws of physics. In a computer experiment, however, the experimenter has to explicitly move every object in his/her artificial universe forward in time, making sure that everything remains within some well-understood state of synchronization. Many fundamental problems in computer science have arisen in the course of trying to understand how to control and use concurrency. Furthermore, most people who implement computer simulations aren't even aware of the subtle, but quite-possibly dominating, impacts of assumptions that they aren't even aware that they are making about concurrency in their model when they code it up and run it.
Therefore, a very important aspect of setting up an experiment in a computer is how one weaves the multiple threads of time that must be woven together coherently in order to produce reliable, repeatable results. Much of our work on Swarm has been devoted to not only making the task of managing concurrency manageable, but towards mechanisms to make people aware that they are always making implicit assumptions about how multiple threads of time are interacting with one another in their experimental setups. Swarm forces experimenters to make their concurrency assumptions explicit, so that others can reproduce their results by implementing the same assumptions about the flow of time.
Swarm is implemented in the Object-Oriented Programming language Objective-C. Computation in a Swarm application takes place by having objects send messages to each other. The basic message syntax is
[targetObject message Arg1: var1 Arg2: var2]
Where "targetObject" is the recipient of the message, "messageArg1:Arg2:" is the message to send to that object, and "var1", "var2", etc, are arguments to pass along with the message. Objective C's messages are keyword/value oriented, which is why the message name "messageArg1:Arg2:" is interspersed with the arguments.
The whole idea of Swarm is to provide an execution context within which a large number of objects can "live their lives" and interact with one another in a distributed, concurrent manner. Furthermore, we wish to insulate the user from having to master all of the highly baroque computer-science wizardry usually required to implement such massively distributed systems of autonomous agents reliably and robustly.
In the context of the Swarm simulation system, the generic outline of an experimental procedure takes the following form:
Swarm applications are structured around the concept of the Swarm. Swarms are the basic building blocks of Swarm simulations: a Swarm is a combination of a collection of objects and a schedule of activity over those objects. The collection are like the matter of the Swarm and the schedule is like the arrow of time moving the objects forward.
In addition to the object collection, the model swarm also contains a schedule of activity on the model. The schedule defines the effect of passing time on the model. For the simple heatbugs schedule, the execution is simply to update the HeatSpace (diffusing heat across the world) and then telling each Heatbug agent to move itself.
Model swarms consist of a set of inputs and outputs. The inputs to the HeatbugModelSwarm are model parameters: things like the size of the world, the number of HeatBugs, and the diffusion rate of heat. The outputs of the HeatbugModelSwarm are the observables of the model: the individual Heatbugs, the distribution of heat across the world, etc.
The most important object in an observer swarm is the model swarm that is being studied. The model swarm is one component of the observer, kind of like a little world in a petri dish on the lab bench. Other observer objects can then input data into the model swarm (setting simulation parameters, for instance) and read data out of the model swarm (collecting statistics of the behavior of agents).
Just as in setting up a model swarm, an observer swarm has a collection of objects (the instrumentation), a schedule of activity, and a set of inputs and outputs. The activity of the observer schedule is to drive data collection - read this number out of the model, draw it on a graph. The inputs to the observer swarm are configurations of the observer tools: what sorts of graphs to generate, for instance. The outputs are the observations.
When running in graphics mode, the observer swarm objects are largely used to mediate user interface. For instance, in Heatbugs the HeatbugObserverSwarm creates Raster widgets, BLTGraphs, Averagers, and Probes. All of these objects are connected into the HeatbugModel swarm to read data, and to graphical interface objects so the human sitting in front of the computer can observe the world.
Interactive, graphical experimentation with models is useful for coming up with intuitions. But for serious experimentation it is necessary to collect statistics, which means doing many runs and storing data for analysis. As an alternative to a graphical observer swarm, you can also create batch swarms, observer swarms that are intended to be run without any interaction at all. Instead of putting up fancy graphics, batch swarms read data from files to control the model and write the data out to other files for analysis. The key here is that the model swarm used in the batch swarm is the exact same model as that used in a graphical observer swarm: the only difference is what tools the observer (batch or graphical) connects to the model.
@interface HeatbugModelSwarm : Swarm { int numBugs; // simulation parameters double evaporationRate; double diffuseConstant; int worldXSize, worldYSize; int minIdealTemp, maxIdealTemp; int minOutputHeat, maxOutputHeat; double randomMoveProbability; id modelActions; // scheduling data structures id modelSchedule; id heatbugList; // list of all the heatbugs Grid2d * world; // objects representing HeatSpace * heat; // the world } -getHeatbugList; // access methods into the -(Grid2d *) getWorld; // model swarm. These methods -(HeatSpace *) getHeat; // allow the model swarm to // be observed. +createBegin: (id) aZone; // extra methods you -createEnd; // provide for Swarms -buildObjects; -buildActions; -activateIn: (id) swarmContext;The first section of code says that a HeatbugModelSwarm is a kind of Swarm. HeatbugModelSwarm inherits a lot of behavior from generic Swarm, but also adds new variables and methods.
The new variables are enclosed in the braces in the definition of HeatbugModelSwarm. They are split into three general classes of things: simulation parameters, schedule data structures, and objects in the world. This is a typical sort of model swarm.
Finally, a HeatbugModelSwarm defines new methods. The first few methods are used to allow the model to be observed: a HeatbugModelSwarm will give out its list of Heatbugs, for instance, or its HeatSpace. Observers use these methods to monitor the model.
In addition to the observation methods, there are several
Swarm-specific methods for the building of Swarms. These are fairly
stereotyped. The createBegin
and createEnd
messages are used to create the Swarm object itself.
buildObjects
builds the model objects, and
buildActions
builds the model schedule - more on these
later. Finally, activateIn
arranges for the execution
machinery to execute the Swarm itself.
In the case of Heatbugs, agents are pretty simple. Here is their definition, from Heatbug.h:
@interface Heatbug: SwarmObject { double unhappiness; // my current unhappiness int x, y; // my spatial coordinates HeatValue idealTemperature; // my ideal temperature HeatValue outputHeat; // how much heat I put out float randomMoveProbability; // chance of moving randomly Grid2d * world; // the world I live in int worldXSize, worldYSize; // how big that world is HeatSpace * heat; // the heat for the world Color bugColor; // my colour (display) } -setWorld: (Grid2d *) w Heat: (HeatSpace *) h; // which world are we in? -createEnd; -(double) getUnhappiness; -setIdealTemperature: (HeatValue) i; -setOutputHeat: (HeatValue) o; -setRandomMoveProbability: (float) p; -setX: (int) x Y: (int) y; // bug's position -setBugColor: (Color) c; // bug's colour (display) -step; -drawSelfOn: (Raster *) r;Heatbug is a subclass of SwarmObject. SwarmObjects have very little behavior of their own - they are defined as the root class of most objects and control computer science aspects like memory allocation and probability.
Heatbug carry with them a variety of state variables. For instance, each Heatbug has a notion of its ideal temperature, which will affect it's behavior. In addition, Heatbugs have variables that let them know about the world: these agents are storing references to the HeatSpace object, for example.
Most of the Heatbug methods have to do with setting up the agents
state - the inputs to a Heatbug. Every heatbug must set up its world
and heat objects, via the setWorld:Heat:
method. In
addition when Heatbugs are created they have their ideal temperature
set, their output heat, etc. Heatbugs are also observable. Heatbugs
define a getUnhappiness
method - the unhappiness is the
major measurable aspect of a heatbug, how well optimized it is at the
moment. They also have a drawSelfOn
method that directs
the heatbug to draw itself on the specified graphics widget.
Finally, and most importantly, a Heatbug has a step
method. step
is where the Heatbugs behavior is defined:
every time the Heatbug is told to step it performs its internal
calculations, choosing where to move. Each heatbug is told to
step
when appropriate by the model schedule. The code for
step
is the real intellectual input into the model, and
is worth reading as an example of an agent's behavior.
buildObjects
method
on HeatbugModelSwarm.
// A loop to create a bunch of heatbugs. for (i = 0; i < numBugs; i++) { Heatbug * hbug; int idealTemp, outputHeat; // Choose a random ideal temperature, output heat from the specified // range (model parameters). idealTemp = [uniformRandom rMin: minIdealTemp Max: maxIdealTemp]; outputHeat = [uniformRandom rMin: minOutputHeat Max: maxOutputHeat]; // Create the heatbug, set the creation time variables hbug = [Heatbug createBegin: [self getZone]]; [hbug setWorld: world Heat: heat]; hbug = [hbug createEnd]; // Add the bug to the end of the list. [heatbugList addLast: hbug]; // Now initialize the rest of the heatbug's state. [hbug setIdealTemperature: idealTemp]; [hbug setOutputHeat: outputHeat]; [hbug setX: [uniformRandom rMax: worldXSize] // random position Y: [uniformRandom rMax: worldYSize]]; }The details of this code are best explained in reading the documentation for the libraries. Essentially, we first generate two random numbers: an ideal temperature and an output heat for the new Heatbug. We then create the Hheatbug itself with
createBegin
and fill in the required parameters of world and heat. Once those are
set, we can send createEnd
to the Heatbug and it is
finished being created. After it's done being created we add it into a
list of Heatbugs in the model and set a few parameters on it like the
ideal temperature and the initial position.
buildObjects
in the HeatbugModelSwarm
heat = [HeatSpace createBegin: [self getZone]]; [heat setSizeX: worldXSize Y: worldYSize]; [heat setDiffusionConstant: diffuseConstant]; [heat setEvaporationRate: evaporationRate]; heat = [heat createEnd];the object is created, a few parameters are set, and then the creation is finalized.
buildObjects
, the next task is to schedule them in the
method buildActions
.
modelActions = [ActionGroup create: [self getZone]]; [modelActions createActionTo: heat message: M(stepRule)]; [modelActions createActionForEach: heatbugList message: M(step)]; [modelActions createActionTo: heat message: M(updateLattice)]; modelSchedule = [Schedule createBegin: [self getZone]]; [modelSchedule setRepeatInterval: 1]; modelSchedule = [modelSchedule createEnd]; [modelSchedule at: 0 createAction: modelActions];The heatbug model schedule actually consists of two components: an ActionGroup called
modelActions
and a Schedule called
modelSchedule
. The ActionGroup is a tightly coupled list
of three messages: every time the action group is executed, it will
send three messages in a row:
[heat stepRule]; [heatbugList forEach: step]; [heat updateLattice];The ActionGroup alone specifies three messages to send - in order to put it in the simulation, that ActionGroup is then dropped into a Schedule. The Schedule itself only has one action - to execute
modelActions
itself. That action takes place at time 0.
But because we've set a repeat interval on the schedule of 1, the
schedule itself loops, executing every 1 time step. The final result
is that modelActions
is executed at time 0, time 1, etc.
@interface HeatbugObserverSwarm : GUISwarm { int displayFrequency; // one parameter: update freq id displayActions; // schedule data structs id displaySchedule; HeatbugModelSwarm * heatbugModelSwarm; // the Swarm we're observing // Lots of display objects. First, widgets XColormap * colormap; // allocate colours ZoomRaster * worldRaster; // 2d display widget BLTGraph * unhappyGraph; // graphing widget GraphElement * unhappyData; // data element on graph // Now, higher order display and data objects Value2dDisplay * heatDisplay; // display the heat Object2dDisplay * heatbugDisplay; // display the heatbugs Averager * unhappinessAverager; // average value ActiveGraph * unhappinessGrapher; // object that does graph }Again we have input parameters (display frequency), schedule data structures, and resident objects (model swarm, display widgets). The important exception is that HeatbugObserverSwarm is a subclass not just of the generic Swarm class, but specifically a GUISwarm. That implies that the HeatbugObserverSwarm will contain a control panel to allow the user to stop execution, and will also have a special
go
method to set everything running.
// Create the graph widget to display unhappiness. unhappyGraph = [BLTGraph create: [self getZone]]; [unhappyGraph title: "Average unhappiness of bugs vs. time"]; [unhappyGraph axisLabelsX: "time" Y: "average unhappiness"]; [unhappyGraph pack]; // Create one data element inside the graph for displaying unhappiness unhappyData = [unhappyGraph createElement]; [unhappyData setLabel: "u"]; // and build a datapipe: a combination of an averager and a grapher. // The averager object collects average statistics, the grapher draws them. unhappinessAverager = [Averager createBegin: [self getZone]]; [unhappinessAverager setList: [heatbugModelSwarm getHeatbugList]]; [unhappinessAverager setProbedSelector: M(getUnhappiness)]; unhappinessAverager = [unhappinessAverager createEnd]; [unhappinessGrapher = [ActiveGraph createBegin: [self getZone]]; [unhappinessGrapher setElement: unhappyData]; [unhappinessGrapher setDataFeed: unhappinessAverager]; // chain them up [unhappinessGrapher setProbedSelector: M(getAverage)]; unhappinessGrapher = [unhappinessGrapher createEnd];The first step is to build an instance of a BLTGraph itself and set its captions. Then one GraphElement, one dataset, is created inside that graph. An Averager object is constructed: it is told to use the message
getUnhappiness
on the model swarm's heatbugList
to construct its data. Finally, an ActiveGraph object serves to
connect the Averager to the GraphElement: it uses the
getAverage
method on the Averager to read in data, and
sends it to the given ActiveGraph. It takes four components to build
this graph, but the resulting tools are powerful and can be configured
to work in many different ways.
int main(int argc, char ** argv) { HeatbugObserverSwarm * observerSwarm; HeatbugBatchSwarm * batchSwarm; // Swarm initialization: all Swarm apps must call this first. initSwarm(argc, argv); // swarmGUIMode is set in initSwarm(). It's set to be 1 if your // DISPLAY environment variable is set (ie, you have an X server to // do graphics with). Otherwise, it's set to 0. if (swarmGUIMode == 1) { // We've got graphics, so make a full ObserverSwarm to get GUI objects observerSwarm = [HeatbugObserverSwarm create: globalZone]; [observerSwarm buildObjects]; [observerSwarm buildActions]; [observerSwarm activateIn: nil]; [observerSwarm go]; } else { // No graphics - make a batchmode swarm and run it. batchSwarm = [HeatbugBatchSwarm create: globalZone]; [batchSwarm buildObjects]; [batchSwarm buildActions]; [batchSwarm activateIn: nil]; [batchSwarm go]; } // The toplevel swarm has finished processing, so it's time to quit. return 0; }main() calls
initSwarm
(required in all Swarm
applications). It then detects if it should do graphics or not,
creates the appropriate top level Swarm to contain the model, and sets
it to running. Simple as that!