Schelling Tutorial
Schelling Tutorial
MASON is a discrete-event simulator designed for very large "swarm-" style simulations. It is written in
Java and is free open source, available at the MASON home page. MASON has grids, continuous regions,
networks, hexagonal spaces, etc., all in 2D or 3D, plus the flexibility to create your own "space" data
structures. Any object can be inspected ("probed") in 2D or in 3D, and tracked with histograms, time series
charts, etc. Among the simulators you may be familiar with, MASON is most similar to RePast.
MASON was designed from first principles for large numbers of simulations on back-end clusters. To this
end it has a number of architecture features which make it somewhat different from other simulation
packages:
MASON's models are separated from its visualizers. You can dynamically attach visualizers,
detach them and run on the command line, or attach different ones. This allows us to checkpoint
(serialize, or "freeze-dry") a running MASON simulation to a file, move it to a different architecture,
and continue to run it. For example, we can run a MASON simulation with no visualization tools at
all on a back-end Linux machine, checkpoint it, and examine/tweak the simulation mid-run on a
front-end Mac under visualization. We can then put it back and continue to run from there. Among
compiled-code multiagent simulators, MASON is unique in this respect.
You'll see this architecture in the tutorial's separate Schelling.java (Model) and
SchellingWithUI.java (Visualizer) files.
MASON is written in highly portable Java, backward-compatible with Java 1.3.1, so as to run on
as many legacy cluster systems as possible.
MASON pays very careful attention to speed. It has special data structures which are designed to
be hacked at a very low (and potentially unsafe) level if speed is called for. In this tutorial, such
structures include Bag, IntBag, DoubleBag, and ObjectGrid2D. MASON also uses a highly efficient
implementation of the Mersenne Twister random number generator, and has both "fast" and "slow
but flexible" versions of much of its visualization toolkit.
In the tutorial, we will be using these data structures and visualizers but will not be taking
advantage of their faster procedures; rather we'll stick with slow-but-safe approaches. As a
result, our simulation model will run at about 1/3 (or less) the speed it could run, and our visualizer
will likewise run at about 1/5 the speed. But it will make the tutorial easier to understand.
MASON is compatible with ECJ, our open-source evolutionary computation and stochastic search
library, and presently among the very best available.
MASON is highly modular. MASON was designed to be easily modified, extended, and otherwise
bent any which way. We have tried hard to make the simulator core cleanly structured. A great many
elements of MASON can be separated and used by themselves independent of the reset of the
simulation package.
We will develop a simple version of the Schelling Segregation model. In this version of Schelling, Red and
Blue agents live in a non-toroidal grid world. Each agent computes its happiness as a function of the sum of
like-colored agents around it, weighted by distance from those agents. If an agent is not happy enough, it
picks a random empty location to move to. All agents have an opportunity to move once per time tick.
As mentioned above, our example tutorial has emphasized tutorial brevity and simplicity over speed; thus
we use slow-but-safe mechanisms whenever. At the very end of the simulation, we do show how to use a
"faster" drawing procedure, which you may be interested in.
The mason directory. This holds the MASON code and documentation.
The jcommon-1.0.0.jar file. This holds utility code used by JFreeChart.
The jfreechart-1.0.1.jar file. This is the primary code for JFreeChart, which MASON employs
to draw charts and graphs.
The itext-1.2.jar file. This holds the iText PDF document generation library, which MASON
uses to output charts and graphs in publication-quality fashion.
The jmf.jar file. This holds Sun's Java Media Framework, which MASON uses to produce moves.
You can also download an operating system-specific version of the framework with extra movie
export options if you like.
The quaqua-colorchooser-only.jar file. (OS X Users only). Java's color picker is poor for OS
X: this library provides a nice replacemen. I have no idea if it works well for Windows or not -- try it
and see!
Additionally, you'll need to have Java3D installed -- though not used for this tutorial, it'll make compiling
MASON much simpler.
None of the additional libraries are actually required to run MASON: without them it will simply refuse to
make a given operation available (such as generating charts and graphs). However to compile a MASON
simulation, these libraries are required unless you go in and manually remove the MASON code which
relies on them. This can be reasonably easily done, but it's inconvenient.
A Minimum Simulation
We begin with a minimum simulation that does nothing at all. Create a file called Schelling.java, stored
in a directory called wcss located in the sim/app directory of MASON. The file looks like this:
package sim.app.wcss;
import sim.engine.*;
import ec.util.*;
import sim.util.*;
An entire MASON simulation model is hung somewhere off of a single instance of a subclass of
sim.engine.SimState. Our subclass is going to be called Schelling. A SimState has two basic instance
variables: an ec.util.MersenneTwisterFast random number generator called random, and a
sim.engine.Schedule called schedule. We create these and give them to SimState through super().
MersenneTwisterFast is the fastest existing Java implementation of the Mersenne Twister random number
generator. I wrote it :-) and it's used in lots of production code elsewhere, including NetLogo. The generator
is essentially identical to java.util.Random in its API, except that it is not threadsafe.
start() is called by MASON whenever a new simulation run is begun. You can do whatever you like in this
method, but be sure to call super.start() first. There's also a finish().
MASON models typically can be run both from the command line and also visualized under a GUI.
Command-line model runs are usually done by calling main(String[] args) on your SimState subclass.
Usually all that needs to be done here is to call the convenience method doLoop and then call
System.exit(0).
doLoop(...) is a convenience method which runs a MASON simulation loop. A basic loop -- which you
could do yourself in main() if you wanted -- starts a MASON simulation (calling start()), steps its Schedule
until the Schedule is out of events to fire, and then cleans up (calling finish()) and quits. doLoop does a little
bit more: it also optionally prints out a bit of statistics information, checkpoints to file and/or recovers from
checkpoint, handles multiple jobs, and stops the simulation in various situations, among others. It does most
of what you'd want.
Very exciting.
Add a Field
So far our model only has a representation of time (Schedule). Let's add a representation of space. MASON
has many representations of space built-in: they're called fields. In our Schelling model, we'll use a simple
two-dimensional grid of Objects. MASON can do a lot more representations than this, but it'll serve for our
purposes in the tutorial. In the Schelling.java file, add:
public int neighborhood = 1;
public double threshold = 3;
public int gridHeight = 100;
public int gridWidth = 100;
threshold is the minimum number of like-us agents (ourselves excluded) in our neighborhood before we
begin to feel "comfortable".
gridHeight and gridWidth are going to be the width and height of our Schelling grid world.
emptySpaces is a sim.util.Bag of locations in the grid world where no one is located. This will help our
agents find new places to move to rapidly. A Bag is a MASON object that is very similar to an ArrayList or
Vector. The difference is that a Bag's underlying array is publicly accessible, so you can do rapid scans over
it and changes to it. This allows Bag to be three to five times the speed of ArrayList.
grid is a sim.field.grid.ObjectGrid2D which will store our Schelling agents. It'll represent the world in
which they live. This class is little more than a wrapper for a publicly accessible 2D array of Objects, plus
some very convenient methods. We initialize it with a width and a height.
Each agent will both live in the grid world and be scheduled on the Schedule to move itself about when
stepped. It's important to understand that this isn't a requirement. In MASON, the objects that live in Space
may have nothing to do with the Steppable objects that live on the Schedule (in "Time", so to speak). We
define Agents as those objects which live on the Schedule and exist to manipulate the world. MASON
commonly calls them "Steppable"s, since they adhere to the Steppable interface so the Schedule can step
them. Agents may not actually have a physical presence at all: though ours will in this example.
Create a new file next to Schelling.java called Agent.java. In this file, add:
package sim.app.wcss;
import sim.util.*;
import sim.engine.*;
loc is a sim.util.Int2D. This is a simple class which holds two integers (x and y). Our agent will use Int2Ds
to store the current location of the agent on the grid. Java already has similar classes, such as
java.awt.Point, but Int2D is immutable, meaning that once its x and y values are set, they cannot be
changed. This makes Int2D appropriate for storing in hash tables -- which MASON uses extensively --
unlike Point. Non-mutable objects used as keys in hash tables are dangerous: they can break hash tables.
step(SimState) is called by the Schedule when this agent's time has come. We'll fill it in with interesting
things later.
Our Red and Blue agents will both subclass from Agent. In this example, our Agent will store its own
location in the world so it doesn't have to scan the world every time to find it out (an expensive prospect).
Additionally, each Agent will define a method called isInMyGroup(Agent) which returns true if the
provided agent is of the same "kind" as the original Agent. Let's make the Red and Blue agents. Create a
file called Blue.java and add the following code:
package sim.app.wcss;
import sim.util.*;
import sim.engine.*;
package sim.app.wcss;
import sim.util.*;
import sim.engine.*;
Here's the code to add to the Schelling.java file, replacing the existing empty start() method.
public double redProbability = 0.333;
public double blueProbability = 0.333;
// add the agents to the grid and schedule them in the schedule
for(int x=0 ; x<gridWidth ; x++)
for(int y=0 ; y<gridHeight ; y++)
{
Steppable agent = null;
double d = random.nextDouble();
if (d < redProbability)
{
agent = new Red(new Int2D(x,y));
schedule.scheduleRepeating(agent);
}
else if (d < redProbability + blueProbability)
{
agent = new Blue(new Int2D(x,y));
schedule.scheduleRepeating(agent);
}
else // add this location to empty spaces list
{
emptySpaces.add(new Int2D(x,y));
}
grid.set(x,y,agent);
}
}
Now, at the beginning of the simulation, our agents are placed into the grid and scheduled to be stepped
each timestep.
MASON Version 12. For further options, try adding ' -help' at end.
Job: 0 Seed: 1155675734985
Starting sim.app.wcss.Schelling
Steps: 500 Time: 499 Rate: 412.20115
Steps: 1000 Time: 999 Rate: 425.17007
Steps: 1500 Time: 1499 Rate: 423.72881
Steps: 2000 Time: 1999 Rate: 424.44822
Quit
This tells us that MASON (on my laptop) is running Schelling at a rate of about 150 ticks per second. In
each tick, MASON is stepping the Schedule once. When a Schedule is stepped, it advances to the
minimally-scheduled timestep, selects all the agents scheduled for that timestep, shuffles their order (if they
have no user-defined orderings among them) and steps each one.
In our simulation, all the red and blue agents are being shuffled and stepped once per Schedule tick. We
have about 6500 such agents on the (100x100) board, so that comes to just about a million steps per second
on my very slow laptop.
Of course, our agents aren't yet doing anything when they're stepped. We'll get to that in a bit. But first, let's
visualize the agents not doing anything. :-)
A typical MASON GUI centers on a subclass you will write of sim.display.GUIState. The GUIState
instance will hold onto your SimState model instance, loading it, checkpointing it, etc. Additionally the
GUIState will house a sim.display.Console, the GUI widget that allows you to manipulate the Schedule,
and one or more displays which describe GUI windows displaying your fields. The displays draw and
manipulate the fields using portrayal objects designed for the various fields.
We begin with a minimal GUI that doesn't have any displays at all. Create a new file called
SchellingWithUI.java and add to it the following:
package sim.app.wcss;
import sim.engine.*;
import sim.display.*;
public class SchellingWithUI extends GUIState
{
public SchellingWithUI() { super(new Schelling(System.currentTimeMillis())); }
public SchellingWithUI(SimState state) { super(state); }
When we run main(...), it creates an instance of the SchellingWithUI class, then creates a Console attached
to the class. The Console is then set visible (it's a window).
Constructors. The standard constructor is passed a SimState object. This is your model (in our case,
Schelling), and it will be stored in the instance variable state. Our default empty constructor calls the
standard constructor with a Schelling model created with a random number seed. You can create a Schelling
model in some other way if you wish.
init() This method is called by the Console when it attaches itself to the GUIState (in our case,
SchellingWithUI). The Controller is the Console itself (it's a subclass of Controller). By default we just call
super.init(...). We'll flesh out this method to set up the displays etc. later.
start() This method is called when the user presses the PLAY button. There's also a finish() method when
the STOP button is pressed, but it's rarely used. By default we call super.start(), which calls start() on our
underlying Schelling model. We'll flesh out this method to reset the portrayals drawing the model later on.
load(SimState) This method is called when the user loads a previously checkpointed simulation. It's
typically more or less the same code as start(), as we'll soon see.
getName() returns a short name for the simulation, which appears in the title bar of the Console.
If you press PLAY, you'll see the simulation running but nothing being displayed. Exciting!
Add a Display
First we need to add a few new import statements to SchellingWithUI.java:
import sim.portrayal.*;
import sim.portrayal.grid.*;
import sim.portrayal.simple.*;
import java.awt.*;
import javax.swing.*;
The portrayal import statements will allow us to create portrayals which draw grid world and its respective
objects. In general, here's what we're going to set up:
1. A sim.display.Display2D will provide the window and scrollable, zoomable drawing surface.
2. A sim.portrayal.grid.ObjectGridPortrayal2D, designed to draw ObjectGrid2Ds, will be attached
to the Display2D to display our model's world.
3. The ObjectGridPortrayal2D will rely on various Simple Portrayals to draw each of the objects in the
world. Specifically:
1. A red sim.portrayal.simple.OvalPortrayal2D will draw the Red objects.
2. A blue sim.portrayal.simple.RectanglePortrayal2D will draw the Blue objects.
3. An empty sim.portrayal.SimplePortrayal2D will draw the null spaces.
Here's the revised versions of init, start, and load, plus some extra code:
public Display2D display;
public JFrame displayFrame;
ObjectGridPortrayal2D gridPortrayal = new ObjectGridPortrayal2D();
// specify the backdrop color -- what gets painted behind the displays
display.setBackdrop(Color.black);
displayFrame.setVisible(true);
}
init(Controller c) has been extended to create a Display2D object and register it with the Console, allowing
you to hide and show the Display2D from the Console, and also close the Display2D cleanly when the
Console closes. We then attach an ObjectGridPortrayal2D to the display, calling it "Agents". We set the
background color of the Display2D to be black, and display it. This sets up the display and grid portrayal to
be used for multiple model instantiations.
start() and load(...) both have roughly the same code, so we have grouped that code into a method called
setupPortrayals(). Here is code called whenever a new model is begun or loaded from checkpoint. This
method tells the grid portrayal that it's supposed to portray the grid we created in our Schelling model.
Notice that we can access our Schelling model through the state variable. The method then attaches three
simple portrayals that the grid portrayal will call upon to draw objects in the grid. An OvalPortrayal2D
draws our Red instances, a RectanglePortrayal2D draws our Blue instances, and a SimplePortrayal2D
(which does nothing) "draws" our null regions. Last, we tell the display to reset it self (which causes it to
reschedule itself to be updated each timestep) and draws it one time.
To do this, we change the step(...) method and add a few variables to the Agent.java file:
IntBag neighborsX = new IntBag(9);
IntBag neighborsY = new IntBag(9);
double happiness = 0;
// get all the places I can go. This will be slow as we have to rely on grabbing neighbors.
sch.grid.getNeighborsMaxDistance(loc.x,loc.y,sch.neighborhood,false,neighborsX,neighborsY);
// compute happiness
happiness = 0;
int len = neighborsX.size();
// Now where to go? Pick a random spot from the empty spaces list.
int newLocIndex = state.random.nextInt(sch.emptySpaces.size());
happiness will hold the agent's happiness. It's an instance variable rather than a local variable because later
on -- not now -- we'd like to tap into it.
neighborsX and neighborsY are IntBags. An IntBag is like a Bag, but it stores ints, not Objects. We are
defining these two variables to avoid having to recreate them over and over again, creating needless garbage
collection.
If there are no empty spaces, our agent does nothing. Otherwise, the IntBags are passed into the
getNeighborsMaxDistance , along with the x and y location of the agent and his desired neighborhood.
("false" states that the environment is non-toroidal). This method will compute integer pairs for every
possible location in this neighborhood, and place them into the neighborsX and neighborsY IntBags
respectively. We can then scan through these bags and test each such location.
Obviously, it's not too complicated to just manually wander through the grid squares ourselves (the array is
the agents field); but this illustrates one way of getting neighborhood information. Realize that this is
trading convenience for speed: we could be significantly faster if we just dug through the grid manually.
The HeatBugs example in MASON shows how to do this.
The for-loop in our step method is extracting the agent (if any) located at position , where x and y are the
next coordinate values in the IntBag. If the agent is similar to us, then we increase val by the linear distance
from us to that coordinate. If we exceed the threshold, we're happy and don't do anything. Else it's time to
get up and move!
Moving is easy. First we set our location in the grid to null. Then we pick a random location from among
the spaces in the empty spaces list. Next we swap our current location with the chosen empty space in the
list. This removes the empty space from the list and marks our old location as empty (vacant) in the list.
Last, we place ourselves in the chosen space on the grid. And we've moved!
MASON Version 12. For further options, try adding ' -help' at end.
Job: 0 Seed: 1155675932470
Starting sim.app.wcss.Schelling
Steps: 250 Time: 249 Rate: 124.13108
Steps: 500 Time: 499 Rate: 126.83917
Steps: 750 Time: 749 Rate: 126.6464
Steps: 1000 Time: 999 Rate: 126.83917
Steps: 1250 Time: 1249 Rate: 126.19889
Steps: 1500 Time: 1499 Rate: 124.62612
Steps: 1750 Time: 1749 Rate: 120.36591
Steps: 2000 Time: 1999 Rate: 121.83236
Quit
When you double-click on a red square, the Console will switch to showing its inspected properties -- and
there's happiness! Next to the happiness property is a magnifying glass: this is the tracker menu, showing
what tracking options you have for this property. For a double (like happiness), you can plot the value as
well.
Note that the values shown in the inspector are for the location and not the object. That is, if a new object
moves into that location, the inspector will show its properties instead. Usually you'd prefer to track an
object as it moves about the world instead. It's expensive to track objects in a simple two-dimensional array
like this; you have to scan through the array to find the object. Instead, you should use a different kind of
grid: a sim.field.SparseGrid2D, which uses hash tables to store locations rather than a 2D array. Inspectors
for a SparseGrid2D will automatically track objects.
This tells MASON that it should add a global model inspector for the model. Now the Console will contain
an additional Model tab for inspecting the entire model.
We don't have anything interesting to inspect in the model yet. Let's add some Java Properties to the model.
Add to Schelling.java the following read/write properties, which allow us to inspect and modify the
neighborhood, threshold, redProbability, and blueProbability variables.
The properties we created were read/write (they had both "get" and "set" version). Now if you select the
Model tab, you can view and change those features of the model.
Add a Histogram
Let's add a read-only Java Property which returns an array or sim.util.DoubleBag of happiness values
gathered from the population of agents. A DoubleBag is like an IntBag but it stores Doubles. That's easy to
do: just scan through the grid and grab the happiness of each non-null agent and stuffs it in the bag. Add the
following to Schelling.java
Pause the simulation. Then go to the Models tab and click on the magnifying glass icon next to
"HappinessDistribution". Choose "Make Histogram", and a histogram pops up. You might want to set it to
redraw faster than every 0.5 seconds. Unpause the simulation and watch the histogram change. Note that
this histogram is for this model run only: if you stop and run again, the histogram is frozen and you'll need
to make a new histogram.
You can have multiple histograms overlaid on one another if you like.
You will probably want your model inspector to also be volatile. This means that MASON should update it
every time tick. Since updating the model inspector every time tick is potentially expensive, MASON
doesn't do it by default, and instead relies on you to press the "refresh" button when you want to see up-to-
date data. To change this behavior, you need to set the inspector that MASON will receive to be volatile, as
follows. In SchellingWithUI.java, add:
Follow the same procedure for getting a Histogram: except instead choose to make a chart from the
"MeanHappiness" property. Unpause the simulation and something like this will occur:
Speed Up Drawing
The reason that drawing is slow is because each object is independently being drawn with a rectangle or a
circle; and furthermore MASON must look up the simple portrayal for each object. This allows considerable
flexibility, but it's slow. There's a faster grid portrayal, called
sim.portrayal.grid.FastObjectGridPortrayal2D which draws all objects as colored squares. Indeed for
many "slow" MASON portrayals, they're often a "fast" one.
The fast portrayal works by querying each object to extract a "number" from it. It then uses a
sim.util.SimpleColorMap to map the number to a color, and sets that rectangle to that color. So we need to
do two things. First, we need to have each of our agents return numbers, and second, we need to add the
map and change the portrayal.
Next we need to add the doubleValue() method to our Red and Blue agents. Add the following method to
Red.java:
Switch to a FastObjectGridPortrayal2D
In SchellingWithUI.java, change the gridPortrayal variable and setupPortrayals() method like this:
Now instead of defining portrayals to draw the individuals, we're simply setting a map which maps numbers
to colors. Null values will be 0 (which maps to transparent). Red individuals will provide 1, which maps to
red. Blue individuals will provide 2, which maps to blue.
Checkpoint the
Simulation
MASON models can be checkpointed
("freeze dried") and restored, with or
without a GUI, and often across different
platforms and operating systems. This is
important for running on back-end
servers and visualizing the current results
on front-end workstations, or saving
recoverable snapshots in case a machine
goes down. Let's try it.
This says to run the simulation no more than 10000 timesteps, writing out a checkpoint every 1000 steps.
You'll get something like this:
MASON Version 12. For further options, try adding ' -help' at end.
Job: 0 Seed: 1155676001239
Starting sim.app.wcss.Schelling
Steps: 250 Time: 249 Rate: 117.53644
Steps: 500 Time: 499 Rate: 122.3092
Steps: 750 Time: 749 Rate: 120.71463
Steps: 1000 Time: 999 Rate: 121.24151
Checkpointing to file: 1000.0.Schelling.checkpoint
Steps: 1250 Time: 1249 Rate: 88.55827
Steps: 1500 Time: 1499 Rate: 124.06948
Steps: 1750 Time: 1749 Rate: 121.6545
Steps: 2000 Time: 1999 Rate: 123.57884
Checkpointing to file: 2000.0.Schelling.checkpoint
Steps: 2250 Time: 2249 Rate: 101.0101
Steps: 2500 Time: 2499 Rate: 123.88503
Steps: 2750 Time: 2749 Rate: 124.93753
Steps: 3000 Time: 2999 Rate: 124.62612
Checkpointing to file: 3000.0.Schelling.checkpoint
Steps: 3250 Time: 3249 Rate: 99.04913
Steps: 3500 Time: 3499 Rate: 122.54902
...
Choose "Open..." from the File menu. Select 1000.0.Schelling.checkpoint. And there's the model, at
timestep 1000. At this point it's probably converged, so not so interesting. But run it a bit and save it out
again as my.checkpoint ("Save As..." from the File menu).
Notice that MASON on the command line starts right up where you saved it under the GUI and goes from
there.
More Information
Go to the MASON Home Page. From there you can:
Further questions? See Sean Luke during the conference and he'll gladly work through them with you. Or
send the MASON development team questions directly at mason-help /-at-/ cs.gmu.edu (send general
development questions to the mailing list instead, please).