/** ** SleuthWorld.java ** ** Copyright 2011 by Sarah Wise, Mark Coletti, Andrew Crooks, and ** George Mason University. ** ** Licensed under the Academic Free License version 3.0 ** ** See the file "LICENSE" for more information ** ** $Id$ **/ package sim.app.geo.sleuth; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.GZIPInputStream; import sim.engine.SimState; import sim.field.geo.GeomGridField; import sim.field.geo.GeomGridField.GridDataType; import sim.field.grid.IntGrid2D; import sim.field.grid.ObjectGrid2D; import sim.io.geo.ArcInfoASCGridImporter; import sim.util.Bag; /** * The SleuthWorld simulation core. * * The simulation can require a LOT of memory, so make sure the virtual machine has enough. * Do this by adding the following to the command line, or by setting up your run * configuration in Eclipse to include the VM argument: * * -Xmx2048M * * With smaller simulations this chunk of memory is obviously not necessary. You can * take it down to -Xmx800M or some such. If you get an OutOfMemory error, push it up. */ public class SleuthWorld extends SimState { private static final String EXCLUDED_DATA_FILE_NAME = "data/excluded.txt.gz"; private static final String HILLSIDE_DATA_FILE_NAME = "data/hillshade.txt.gz"; private static final String LAND_USE_DATA_FILE_NAME = "data/landuse.txt.gz"; private static final String SLOPE_DATA_FILE_NAME = "data/reclass_slope.txt.gz"; private static final String TRANSPORT_DATA_FILE_NAME = "data/roads_0_1.txt.gz"; private static final String URBAN_AREA_DATA_FILE_NAME = "data/urban.txt.gz"; ObjectGrid2D landscape; ArrayList<Tile> spreadingCenters = new ArrayList<Tile>(); // model parameters double dispersionCoefficient = 5; // maximally 100? public double getDispersionCoefficient() { return dispersionCoefficient; } public void setDispersionCoefficient(double val) { dispersionCoefficient = val; } double breedCoefficient = 5; public double getBreedCoefficient() { return breedCoefficient; } public void setBreedCoefficient(double val) { breedCoefficient = val; } double spreadCoefficient = 5; public double getSpreadCoefficient() { return spreadCoefficient; } public void setSpreadCoefficient(double val) { spreadCoefficient = val; } double slopeCoefficient = 5; public double getSlopeCoefficient() { return slopeCoefficient; } public void setSlopeCoefficient(double val) { slopeCoefficient = val; } double roadGravityCoefficient = 1; public double getRoadGravityCoefficient() { return roadGravityCoefficient; } public void setRoadGravityCoefficient(double val) { roadGravityCoefficient = val; } int maxCoefficient = 100; double maxRoadValue = 4; // landscape parameters int grid_width = 100; int grid_height = 100; // cheap way to visualize int numUrban = 0; int numNonUrban = 0; private static final long serialVersionUID = 1L; /** * Constructor function. * @param seed */ public SleuthWorld(long seed) { super(seed); } /** * Starts a new run of the simulation. Reads in the data and schedules * the growth rules to fire every turn. */ @Override public void start() { try { super.start(); readSlopeData(); // Also create initial landscape from slope data readLandUseData(); readExcludedAreaData(); readUrbanAreaData(); readTransportData(); readHillShadeData(); System.out.println("Successfully read in all data!"); // Now count the initial total urban and non-urban areas. for (int i = 0; i < grid_width; i++) { for (int j = 0; j < grid_height; j++) { Tile tile = (Tile) landscape.get(i, j); if (tile != null && tile.urbanized) { numUrban++; int numUrbanizedNeighbors = getUrbanNeighbors(tile).size(); if (numUrbanizedNeighbors > 1 && numUrbanizedNeighbors < 6) { spreadingCenters.add(tile); } } else { numNonUrban++; } } } // schedule the growth cycle to happen every time step Grower grower = new Grower(); schedule.scheduleRepeating(grower); } catch (FileNotFoundException ex) { Logger.getLogger(SleuthWorld.class.getName()).log(Level.SEVERE, null, ex); } } private void readData(GeomGridField excludedGridField, final String fileName) throws FileNotFoundException { InputStream inputStream = SleuthWorld.class.getResourceAsStream(fileName); if (inputStream == null) { throw new FileNotFoundException(fileName); } try { GZIPInputStream compressedInputStream = new GZIPInputStream(inputStream); ArcInfoASCGridImporter.read(compressedInputStream, GridDataType.INTEGER, excludedGridField); } catch (IOException ex) { Logger.getLogger(SleuthWorld.class.getName()).log(Level.SEVERE, null, ex); System.exit(-1); } } /** * any non-urbanized cell on the lattice has a certain (small) probability * of becoming urbanized in any time step. * @param dispersion_value - the dispersion value * @return a list of Tiles that have been spontaneously urbanized */ ArrayList<Tile> spontaneousGrowth(double dispersion_value) { ArrayList<Tile> urbanized = new ArrayList<Tile>(); for (int i = 0; i < dispersion_value; i++) { // find a tile that is a part of the simulation int x = random.nextInt(grid_width), y = random.nextInt(grid_height); while (landscape.get(x, y) == null) { x = random.nextInt(grid_width); y = random.nextInt(grid_height); } Tile t = (Tile) landscape.get(x, y); // try to urbanize t! if (t.urbanized) { continue; // already urbanized } else if (t.excluded) { continue; // excluded from urbanization } else { // urbanize the tile! Tile newlyUrbanized = (Tile) landscape.get(x, y); boolean successful = urbanizeTile(newlyUrbanized); if (successful) { urbanized.add(newlyUrbanized); } } } return urbanized; } /** * Spontaneously urbanized cells can be new urban centers. With some probability, * these spontaneous cells will urbanize two of their unurbanized neighbors. * @param newlyUrbanized - the list of Tiles that were just spontaneously * urbanized * @return the list of all Tiles that grew up around these new centers */ ArrayList<Tile> newSpreadingCenters(ArrayList<Tile> newlyUrbanized) { ArrayList<Tile> spreadFromNewlyUrbanized = new ArrayList<Tile>(); // go through all of the newly urbanized tiles for (Tile t : newlyUrbanized) { // with some probability, try to spread out from them if (random.nextInt(maxCoefficient) < breedCoefficient) { // assemble a list of unurbanized neighbors ArrayList<Tile> potential = getNeighborsAvailableForUrbanization(t); // if there are at least 2 unurbanized neighbors, urbanize 2 of them! if (potential.size() > 1) { for (int i = 0; i < 2; i++) { Tile toUrbanize = potential.remove( random.nextInt(potential.size())); boolean successful = urbanizeTile(toUrbanize); if (successful) { spreadFromNewlyUrbanized.add(toUrbanize); } } // the central Tile is now an urban center spreadingCenters.add(t); } } } return spreadFromNewlyUrbanized; } /** * Urban centers tend to fill in. Consider the neighbors of urban centers, e.g. * cells that have at least 3 neighboring urbanized cells. With some probability, * urbanize these. * @return the list of newly urbanized Tiles */ ArrayList<Tile> edgeGrowth() { ArrayList<Tile> newlyUrbanized = new ArrayList<Tile>(); ArrayList<Tile> centers = new ArrayList<Tile>(spreadingCenters); // go through urban centers, potentially grow their neighboring unurbanized cells for (Tile t : centers) { // with some probability, spread out from this if (random.nextInt(maxCoefficient) < spreadCoefficient) { // get neighbors that can be urbanized ArrayList<Tile> suitableForUrbanization = getNeighborsAvailableForUrbanization(t); if (suitableForUrbanization.size() > 0) { Tile toUrbanize = suitableForUrbanization.get( random.nextInt(suitableForUrbanization.size())); // urbanize one such randomly selected Tile boolean successful = urbanizeTile(toUrbanize); if (successful) { newlyUrbanized.add(toUrbanize); } } } } return newlyUrbanized; } /** * Growth often spreads along the transportation network. For all of the cells * which have just been urbanized, check if there is a road within a given * distance of them. If so, walk along that road and potentially establish a * new urban center. If the urban center can spread, do so this turn. * @param max_search_index - the maximum distance from the Tile within which * roads are considered * @param recentlyUrbanized - the list of all Tiles urbanized this turn */ void roadInfluencedGrowth(double max_search_index, ArrayList<Tile> recentlyUrbanized) { // go through all Tiles that were urbanized this turn and possibly spread along // nearby roads for (Tile t : recentlyUrbanized) { // do so with some probability if (random.nextInt(maxCoefficient) < breedCoefficient) { // check if there is a road within a given distance of the newly // urbanized Tile ArrayList<Tile> neighboringRoads = getNeighborsTransport(t, (int) max_search_index); if (! neighboringRoads.isEmpty() ) { // if so, do a random walk along the road with number of steps // depending on the weight of that road Tile bordersRoad = neighboringRoads.get(0); // calculate the number of steps the test will random walk // along the road, based on the caliber of road on which // it initially finds itself double run_value = (dispersionCoefficient * (maxRoadValue - bordersRoad.transport + 1) / maxRoadValue); Tile finalPoint = walkAlongRoad(bordersRoad, (int) run_value); // at the place we finally end up, see if there are any neighbors // available for urbanization. If so, urbanize one. ArrayList<Tile> potential = getNeighborsAvailableForUrbanization(finalPoint); if (potential.isEmpty()) { continue; // no neighbors available } Tile newUrbanized = potential.get(random.nextInt(potential.size())); boolean successful = urbanizeTile(newUrbanized); if (!successful) { continue; // it didn't take, so we don't check } // the neighbors of this failed urbanization attempts // check and see if this newly urbanized Tile has neighbors to // urbanize. If it has at least two, urbanize two randomly selected // neighbors ArrayList<Tile> neighbors = getNeighborsAvailableForUrbanization(newUrbanized); if (neighbors.size() > 1) { for (int i = 0; i < 2; i++) { Tile neighbor = neighbors.remove( random.nextInt(neighbors.size())); urbanizeTile(neighbor); } } } } } } // // --- END GROWTH RULES --- // // // --- HELPFUL UTILITIES --- // /** * Takes a point of origin and randomly walks along the connected roads for the given * number of steps * @param origin - the Tile from which the random walk begins * @param numSteps - the number of random steps taken * @return the Tile where the random walk terminates */ Tile walkAlongRoad(Tile origin, int numSteps) { if (origin.transport < 1) { return null; // NOT A ROAD, SILLY } Tile result = origin; ArrayList<Tile> neighbors; // for the number of steps, move to random connected road segments for (int i = 0; i < numSteps; i++) { neighbors = getNeighborsTransport(result, 1); if (neighbors.isEmpty()) { return result; } // move to a random neighboring road segment result = neighbors.get(random.nextInt(neighbors.size())); } return result; } /** * Tidily urbanizes a tile, updating the list of edge Tiles given the Tile's new * status and its new, potentially now edge-y neighbors. * @param t - the Tile to urbanize */ boolean urbanizeTile(Tile t) { if (t.excluded || t.slope > 21) { // can't urbanize a Tile with slope > 21 System.out.println("Error: can't urbanize a Tile with slope > 21"); // unsuccessful urbanization return false; } t.urbanized = true; t.landuse = 1; // set the landuse to urban numUrban++; // if this Tile qualifies as a city center, make it so! ArrayList<Tile> urbanizedNeighbors = getUrbanNeighbors(t); if (urbanizedNeighbors.size() > 2) { spreadingCenters.add(t); } // check to see if the urbanization of this Tile has made any of its neighbors // into centers for (Tile n : urbanizedNeighbors) { // check to see if we've already found this Tile if (spreadingCenters.contains(n)) { continue; } // otherwise, if this Tile now has enough urbanized neighbors to be an urban // center, make it so! ArrayList<Tile> neighborsUrbanizedNeighbors = getUrbanNeighbors(n); // need to be at least 2 and if it's completely surrounded that's not very // useful if (neighborsUrbanizedNeighbors.size() > 1 && neighborsUrbanizedNeighbors.size() < 6) { spreadingCenters.add(n); } } // successfully urbanized cell return true; } /** * Sets up the landscape full of tiles * @param width - the width of the landscape * @param height - the height of the landscape */ void setupLandscape(final GeomGridField slope) { grid_width = slope.getGridWidth(); grid_height = slope.getGridHeight(); landscape = new ObjectGrid2D(grid_width, grid_height); // populate the new landscape with new Tiles System.out.print("\nInitializing landscape..."); for (int i = 0; i < grid_width; i++) { if (i % 500 == 0) { System.out.print("."); } for (int j = 0; j < grid_height; j++) { // -9999 means "no data" if ( ((IntGrid2D)slope.getGrid()).get(i, j) != -9999) { Tile tile = new Tile(i,j); tile.slope = ((IntGrid2D)slope.getGrid()).get(i, j); landscape.set(i, j, tile); } } } System.out.println("completed!"); } /** * Get the neighbors of the given Tile, specifically the urbanized or unurbanized * neighbors depending on the boolean value passed. * @param t - the Tile in question * @param urbanized - whether you want urbanized or unurbanized neighbors. E.g. * true means you want urbanized neighbors, false gives you unurbanized neighbors * @return returns the neighbors of the Tile t given the type passed to it */ ArrayList<Tile> getUrbanNeighbors(Tile t) { ArrayList<Tile> result = new ArrayList<Tile>(); Bag neighbors = new Bag(); landscape.getNeighborsMaxDistance(t.x, t.y, 1, false, neighbors, null, null); for (Object o : neighbors) { if (o == null) { continue; } else if (o == t) { continue; } Tile n = (Tile) o; if (n.urbanized) { result.add(n); } } return result; } /** * Get the urbanizable neighbors of the given Tile, that is unurbanized neighbors * with acceptable slopes * @param t - the Tile in question * @return returns the appropriate neighbors of the Tile t */ ArrayList<Tile> getNeighborsAvailableForUrbanization(Tile t) { ArrayList<Tile> result = new ArrayList<Tile>(); Bag neighbors = new Bag(); landscape.getNeighborsMaxDistance(t.x, t.y, 1, false, neighbors, null, null); for (Object o : neighbors) { if (o == null) { continue; } else if (o == t) { continue; } Tile n = (Tile) o; // if the tile hasn't been urbanized and its slope is within an acceptable // level, add it! if (n.urbanized == false && n.slope <= 21 && !n.excluded) { result.add(n); } } return result; } /** * Returns the neighbors of a given Tile where there are roads * @param t - the Tile around which we want to search * @param dist - the maximal distance from the Tile to search * @return the set of transport-enabled tiles within the search radius of the Tile t */ ArrayList<Tile> getNeighborsTransport(Tile t, int dist) { ArrayList<Tile> result = new ArrayList<Tile>(); Bag neighbors = new Bag(); landscape.getNeighborsMaxDistance(t.x, t.y, dist, false, neighbors, null, null); for (Object o : neighbors) { if (o == null) { continue; } else if (o == t) { continue; } else if (((Tile) o).transport > 0) { result.add((Tile) o); } } return result; } /** * Main function, runs the simulation without any visualization. * @param args */ public static void main(String[] args) { doLoop(SleuthWorld.class, args); System.exit(0); } private void readSlopeData() throws FileNotFoundException { System.out.println("Reading slope data ..."); // Let's read in all the slope data first. Not only will that give us // the dimensions, but we can use that to determine where to put new tiles. GeomGridField slopeField = new GeomGridField(); readData(slopeField, SLOPE_DATA_FILE_NAME); // Now setup this.landscape setupLandscape(slopeField); } /** * The land use flags each tile with one of four classifications. * * (And what are these?) */ private void readLandUseData() throws FileNotFoundException { System.out.println("Reading land use data ..."); GeomGridField landuseGridField = new GeomGridField(); readData(landuseGridField, LAND_USE_DATA_FILE_NAME); // Now set all the tiles' land use vales for (int y = 0; y < landscape.getHeight(); y++) { for (int x = 0; x < landscape.getWidth(); x++) { if (landscape.get(x, y) != null) { Tile tile = (Tile) landscape.get(x, y); tile.landuse = ((IntGrid2D) landuseGridField.getGrid()).get(x, y); } } } } /** * TODO: the excluded data area file appears to be either -9999 (no data) * or zeroes. Is this right? */ private void readExcludedAreaData() throws FileNotFoundException { System.out.println("Reading excluded area data ..."); GeomGridField excludedGridField = new GeomGridField(); readData(excludedGridField, EXCLUDED_DATA_FILE_NAME); // Now set all the tiles' land use vales for (int y = 0; y < landscape.getHeight(); y++) { for (int x = 0; x < landscape.getWidth(); x++) { if (landscape.get(x, y) != null) { Tile tile = (Tile) landscape.get(x, y); tile.excluded = ((IntGrid2D) excludedGridField.getGrid()).get(x, y) == 0; } } } } private void readUrbanAreaData() throws FileNotFoundException { System.out.println("Reading urban area data ..."); GeomGridField urbanAreaGridField = new GeomGridField(); readData(urbanAreaGridField,URBAN_AREA_DATA_FILE_NAME); // Now set all the tiles' land use vales for (int y = 0; y < landscape.getHeight(); y++) { for (int x = 0; x < landscape.getWidth(); x++) { if (landscape.get(x, y) != null) { Tile tile = (Tile) landscape.get(x, y); int classification = ((IntGrid2D) urbanAreaGridField.getGrid()).get(x, y); switch (classification) { case 1: tile.urbanOriginally = false; tile.urbanized = false; break; case 2: tile.urbanOriginally = true; tile.urbanized = false; break; default: throw new AssertionError(); } } } } } private void readTransportData() throws FileNotFoundException { System.out.println("Reading transport data ..."); GeomGridField transportGridField = new GeomGridField(); readData(transportGridField, TRANSPORT_DATA_FILE_NAME); // Now set all the tiles' land use vales for (int y = 0; y < landscape.getHeight(); y++) { for (int x = 0; x < landscape.getWidth(); x++) { if (landscape.get(x, y) != null) { Tile tile = (Tile) landscape.get(x, y); int classification = ((IntGrid2D) transportGridField.getGrid()).get(x, y); tile.transport = classification; } } } } private void readHillShadeData() throws FileNotFoundException { System.out.println("Reading hill shade data ..."); GeomGridField hillshadeGridField = new GeomGridField(); readData(hillshadeGridField, HILLSIDE_DATA_FILE_NAME); // Now set all the tiles' land use vales for (int y = 0; y < landscape.getHeight(); y++) { for (int x = 0; x < landscape.getWidth(); x++) { if (landscape.get(x, y) != null) { Tile tile = (Tile) landscape.get(x, y); int classification = ((IntGrid2D) hillshadeGridField.getGrid()).get(x, y); tile.hillshade = classification; } } } } }