static final int VIEW_SIZE = 340; static final int LEFT_MARG = 20; static final int TOP_MARG = 20; static final int BOT_MARG = 20; static final int RIGHT_MARG = 140; static final int BUTTON_LEFT = LEFT_MARG + VIEW_SIZE + LEFT_MARG; static final int BUTTON_WIDTH = RIGHT_MARG - LEFT_MARG * 2; static final int BUTTON_HEIGHT = 24; static final int BUTTON_LEAD = 36; static final int PARAM_TOP = TOP_MARG + BUTTON_LEAD * 3 + 24; static final int SIM_FRAMES = 200; static final int TOT_FRAMES = 300; Map map = null; int frame = 0; boolean[][] simulation = null; int mapSize = 60; PFont font = loadFont("OfficinaSans-Book-16.vlw"); Button replay = new Button ( BUTTON_LEFT, TOP_MARG, BUTTON_WIDTH, BUTTON_HEIGHT, "Replay" ); Button resim = new Button ( BUTTON_LEFT, TOP_MARG + BUTTON_LEAD, BUTTON_WIDTH, BUTTON_HEIGHT, "Resimulate" ); Button newMap = new Button ( BUTTON_LEFT, TOP_MARG + 2 * BUTTON_LEAD, BUTTON_WIDTH, BUTTON_HEIGHT, "New Map" ); Button restore = new Button ( BUTTON_LEFT, TOP_MARG + VIEW_SIZE - BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT, "Defaults" ); Slider patchSlider = new Slider ( BUTTON_LEFT, PARAM_TOP, BUTTON_WIDTH, 3, 20, "Patches", 1, 10); Slider scaleSlider = new Slider ( BUTTON_LEFT, PARAM_TOP + BUTTON_LEAD, BUTTON_WIDTH, 10, 500, "Scale", 10, 60 ); Slider alphaSlider = new Slider ( BUTTON_LEFT, PARAM_TOP + BUTTON_LEAD * 2, BUTTON_WIDTH, 0, 0.2, "Alpha", -2, 0.06); Slider xSlider = new Slider ( BUTTON_LEFT, PARAM_TOP + BUTTON_LEAD * 3, BUTTON_WIDTH, 0, 1, "x", -2, 0.5); Slider ySlider = new Slider ( BUTTON_LEFT, PARAM_TOP + BUTTON_LEAD * 4, BUTTON_WIDTH, 0, 300, "y", 10, 50); void setup () { size( VIEW_SIZE + LEFT_MARG + RIGHT_MARG, VIEW_SIZE + TOP_MARG + BOT_MARG ); ellipseMode(CENTER); reinit(); } void reinit () { mapSize = (int) scaleSlider.value; do { map = new Map ( round(patchSlider.value), mapSize ); } while ( ! map.converge ( alphaSlider.value, xSlider.value, ySlider.value ) ); simulation = map.simulate( SIM_FRAMES, 0.6 ); frame = 0; } void draw () { background(255); replay.draw(); resim.draw(); newMap.draw(); restore.draw(); patchSlider.draw(); scaleSlider.draw(); alphaSlider.draw(); xSlider.draw(); ySlider.draw(); drawMap(); } void mousePressed () { if ( replay.hit() ) { frame = 0; } else if ( resim.hit() ) { simulation = map.simulate( SIM_FRAMES, 0.6 ); frame = 0; } else if ( newMap.hit() ) { reinit(); } else if ( restore.hit() ) { patchSlider.setValue(10); scaleSlider.setValue(60); alphaSlider.setValue(0.06); xSlider.setValue(0.5); ySlider.setValue(50); } else { scaleSlider.mousePressed(); patchSlider.mousePressed(); alphaSlider.mousePressed(); xSlider.mousePressed(); ySlider.mousePressed(); } } void mouseReleased () { scaleSlider.mouseReleased(); patchSlider.mouseReleased(); alphaSlider.mouseReleased(); xSlider.mouseReleased(); ySlider.mouseReleased(); } void drawMap () { double scale = VIEW_SIZE / (double) mapSize; if ( frame < simulation.length ) { boolean[] sf = simulation[frame]; stroke(0,64,196); fill(255); rect(LEFT_MARG, TOP_MARG, VIEW_SIZE, VIEW_SIZE); stroke(0); for (int i = 0; i < sf.length; ++i ) { if ( sf[i] ) { fill ( 255, (int) (192 * map.p[i]), (int) (192 * map.p[i]) ); } else { fill ( (int) (192 * map.c[i]), (int) (192 * map.c[i]), (int) (192 * map.c[i]) ); } ellipse ( (float) (map.patches[i].x * scale) + LEFT_MARG, (float) (map.patches[i].y * scale) + TOP_MARG, (float) (map.patches[i].radius * scale * 2), (float) (map.patches[i].radius * scale * 2) ); } fill (0,64,255); textAlign(LEFT); text( ""+frame, LEFT_MARG + 2, VIEW_SIZE + TOP_MARG + BOT_MARG - 22 ); frame = frame + 1; } else if ( frame >= simulation.length ) { boolean[] sf = simulation[simulation.length - 1]; stroke(0); fill(250); rect(LEFT_MARG, TOP_MARG, VIEW_SIZE, VIEW_SIZE); for (int i = 0; i < sf.length; ++i ) { if ( sf[i] ) { fill ( 255, (int) (192 * map.p[i]), (int) (192 * map.p[i]) ); } else { fill ( (int) (192 * map.c[i]), (int) (192 * map.c[i]), (int) (192 * map.c[i]) ); } ellipse ( (float) (map.patches[i].x * scale) + LEFT_MARG, (float) (map.patches[i].y * scale) + TOP_MARG, (float) (map.patches[i].radius * scale * 2), (float) (map.patches[i].radius * scale * 2) ); } } } boolean mouseIn ( int left, int top, int width, int height ) { return ( (mouseX >= left) && (mouseY >= top) && (mouseX < left + width) && (mouseY < top + height) ); } // class for buttons class Button { int left, top, width, height; String label; int baseline; Button ( int left, int top, int width, int height, String label ) { this.left = left; this.top = top; this.width = width; this.height = height; this.label = label; baseline = -6 + height/2; } void draw () { stroke(0); fill(mouseIn(left, top, width, height) ? 220 : 255); rect(left, top, width, height); textFont(font,16); textAlign(CENTER); fill(0); text(label, left, top + baseline, width, height ); } boolean hit () { return mouseIn(left, top, width, height); } } // class for sliders class Slider { int x, y, width; static final int HEIGHT = 25; float hi, lo; String label; int roundToNearest; static final int THUMB_SIZE = 10; static final int BAR_OFFSET = 20; int thumbX, thumbY, barY; boolean held = false; float value; String valString; Slider ( int x, int y, int width, float lo, float hi, String label, int roundToNearest, float value ) { this.x = x; this.y = y; this.width = width; this.lo = lo; this.hi = hi; this.label = label; this.roundToNearest = roundToNearest; barY = y + BAR_OFFSET; thumbY = barY - THUMB_SIZE / 2; setValue ( value ); } void setValue ( float value ) { this.value = value; setValueString(); thumbX = (int) (x + (width - THUMB_SIZE) * ((value - lo) / (hi - lo)) ); } void draw () { if ( held ) { thumbX = min(x + width - 1 - THUMB_SIZE, max(x, mouseX - THUMB_SIZE/2)); value = lo + (hi - lo) * ((float) thumbX - x)/(width - THUMB_SIZE); setValueString(); } stroke(0); fill((held || mouseIn(thumbX, thumbY, THUMB_SIZE, THUMB_SIZE)) ? 220 : 255); line(x, barY, x + width - 1, barY); line(x, barY - 2, x, barY + 2); line(x + width - 1, barY - 2, x + width - 1, barY + 2); rect(thumbX, thumbY, THUMB_SIZE, THUMB_SIZE); textFont(font,16); textAlign(LEFT); fill(0); text(label, x, y - 1, width, HEIGHT ); textAlign(RIGHT); text(valString, x, y - 1, width, HEIGHT); } void setValueString() { if ( roundToNearest == 1 ) { valString = Integer.toString(round(value)); } else if ( roundToNearest > 0 ) { int i = roundToNearest * (round(value + roundToNearest/2) / roundToNearest); valString = Integer.toString(i); } else if ( roundToNearest < 0 ) { valString = Float.toString(value); int offset = valString.indexOf('.'); if ( valString.length() > offset - roundToNearest + 1 ) { valString = valString.substring(0, offset - roundToNearest + 1); } } else { valString = Float.toString(value); } } void mousePressed () { if ( mouseIn( thumbX, thumbY, THUMB_SIZE, THUMB_SIZE ) ) { held = true; } } void mouseReleased () { held = false; } } // simple struct to hold patch information class Patch { public double radius; public double x; public double y; public double area; public double[] distances; } // class to hold the geography and run the model class Map { public static final int MAX_ITERATIONS = 1000; public static final double EPSILON = 0.00000001; public int n; public double size; public Patch[] patches; public double[] p; public double[] s; public double[] c; // private default constructor for possible internal use later! private Map () {} public Map ( int n, double size ) { this.n = n; this.size = size; p = new double[n]; s = new double[n]; c = new double[n]; patches = new Patch[n]; // initialise model values and start initing patches for ( int i = 0; i < n; ++i ) { p[i] = 1; c[i] = 0; s[i] = 0; patches[i] = new Patch(); patches[i].x = Math.random() * size; patches[i].y = Math.random() * size; patches[i].distances = new double[n]; } // assign radii randomly to the patches, ensuring the resulting // circles do not overlap each other or the field edges for ( int i = 0; i < n; ++i ) { Patch pi = patches[i]; double max = Math.min(Math.min(pi.x,size-pi.x),Math.min(pi.y,size-pi.y)); for ( int j = 0; j < n; ++j ) { Patch pj = patches[j]; if ( i == j ) { pi.distances[j] = 0; } else if ( i < j ) { double dx = pi.x - pj.x; double dy = pi.y - pj.y; pi.distances[j] = Math.sqrt( dx*dx + dy*dy ); max = Math.min(max, pi.distances[j]); } else // i > j, distance already calculated and j's radius assigned { pi.distances[j] = pj.distances[i]; max = Math.min(max, pi.distances[j] - pj.radius); } } pi.radius = Math.random() * max; pi.area = Math.PI * pi.radius * pi.radius; } } public boolean converge ( double alpha, double x, double y ) { double[] s1 = new double[n]; double[] p1 = new double[n]; double[] c1 = new double[n]; double y2 = y * y; for ( int iter = 0; iter < MAX_ITERATIONS; ++iter ) { for ( int i = 0; i < n; ++i ) { // note: moved the -piAi term out of the summation s1[i] = -1 * p[i] * patches[i].area; for ( int j = 0; j < n; ++j ) { s1[i] += p[j] * patches[j].area * Math.exp( -alpha * patches[i].distances[j] ); } double s2 = s1[i] * s1[i]; c1[i] = s2 / ( y2 + s2 ); p1[i] = s2 / ( s2 + (y2 / Math.pow( patches[i].area, x )) ); } if ( matches(p, p1, EPSILON) && matches(c, c1, EPSILON) && matches(s, s1, EPSILON) ) { return true; } System.arraycopy ( s1, 0, s, 0, n ); System.arraycopy ( p1, 0, p, 0, n ); System.arraycopy ( c1, 0, c, 0, n ); } return false; } public boolean matches ( double[] a, double[] b, double epsilon ) { for ( int i = 0; i < a.length; ++i ) { if ( Math.abs( a[i] - b[i] ) > epsilon ) { return false; } } return true; } boolean[][] simulate ( int steps, double initProb ) { boolean[][] result = new boolean[steps + 1][n]; for ( int i = 0; i < n; ++i ) { result[0][i] = Math.random() < initProb; } for ( int gen = 1; gen <= steps; ++gen ) { for ( int i = 0; i < n; ++i ) { if ( result[gen-1][i] ) { result[gen][i] = Math.random() > p[i]; } else { result[gen][i] = Math.random() <= c[i]; } } } return result; } public String toString () { String result = "Map of " + n + " patches:\n"; for ( int i = 0; i < n; ++i ) { Patch p = patches[i]; result += " " + i + ": (" + p.x + "," + p.y + "," + p.radius + ")]\n"; } return result; } }