FUTURES tutorial
r.futures.* is an implementation of FUTure Urban-Regional Environment Simulation (FUTURES) which is a model for multilevel simulations of emerging urban-rural landscape structure. FUTURES produces regional projections of landscape patterns using coupled submodels that integrate nonstationary drivers of land change: per capita demand (DEMAND submodel), site suitability (POTENTIAL submodel), and the spatial structure of conversion events (PGA submodel).
This tutorial shows how to prepare data and run the model. You can download sample dataset and simulate urban growth in The Triangle, region in the Piedmont of North Carolina in the United States with rapidly growing cities Raleigh, Durham and Chapel Hill.
Software
Required software includes:
- GRASS GIS 7
- r.futures addon
- R (≥ 3.0.2)
- SciPy (optional)
Input data used in this tutorial
Download sample dataset containing:
- digital elevation model (NED)
- NLCD 2001, 2011
- NLCD 1992/2001 Retrofit Land Cover Change Product
- transportation network (TIGER)
- county boundaries (TIGER)
- protected areas (Secured Lands)
- cities above 20000 (USGS)
- county population past estimates and future projections (NC OSBM) per county (nonspatial)
Workflow
Initial steps and data preparation
First we will set computational region of our analyses to an extent covering our study area and so that the cells are aligned with one of our landuse rasters:
g.region align=landuse_2011 -p
We will derive urbanized areas from NLCD dataset for year 1992, 2001 and 2011 by extracting categories category 21 - 24 into a new binary map where developed is 1, undeveloped 0 and NULL (no data) is area unsuitable for development (water, wetlands, protected areas). First we will convert protected areas from vector to raster and set NULLs to zeros (for simpler raster algebra expression in the next step):
v.to.rast input=protected_areas output=protected_areas use=val
r.null map=protected_areas null=0
And then create rasters of developed/undeveloped areas using raster algebra:
r.mapcalc "urban_1992 = if(landuse_1992 >= 21 && landuse_1992 <= 24, 1, if(landuse_1992 == 11 || landuse_1992 >= 90 || protected_areas, null(), 0))"
r.mapcalc "urban_2001 = if(landuse_2001 >= 21 && landuse_2001 <= 24, 1, if(landuse_2001 == 11 || landuse_2001 >= 90 || protected_areas, null(), 0))"
r.mapcalc "urban_2011 = if(landuse_2011 >= 21 && landuse_2011 <= 24, 1, if(landuse_2011 == 11 || landuse_2011 >= 90 || protected_areas, null(), 0))"
We will convert vector counties to raster with the values of the FIPS attribute which links to population file:
v.to.rast input=counties type=area use=attr attribute_column=FIPS output=counties_FIPS
If we need to run FUTURES only for smaller number of counties, we can extract them based on the identifier:
v.to.rast input=counties type=area where="FIPS in (37183, 37101)" use=attr attribute_column=FIPS output=counties_FIPS
Before further steps, we will set our working directory so that the input population files and text files
we are going to create are saved in one directory and easily accessible.
You can do that from menu Settings → GRASS working environment → Change working directory.
Select (or create) a directory and move there the downloaded files population_projection.csv and population_trend.csv.
Potential submodel
Predictors
Slope
We will derive slope in degrees from digital elevation model using r.slope.aspect module:
r.slope.aspect elevation=elevation slope=slope
Distance from lakes/rivers
First we will extract category Open Water from 2011 NLCD dataset:
r.mapcalc "water = if(landuse_2011 == 11, 1, null())"
Then we compute the distance to water with module r.grow.distance and set color table to shades of blue:
r.grow.distance input=water distance=dist_to_water
r.colors -n map=dist_to_water color=blues
Distance from protected areas
We will use raster protected of protected areas we already created. We compute the distance to protected areas with module r.grow.distance and set color table from green to red:
r.grow.distance input=protected_areas distance=dist_to_protected
r.colors map=dist_to_protected color=gyr
Forests
We will smooth the transition between forest and other land use, see NLCD category Open Water:
r.mapcalc "forest = if(landuse_2011 >= 41 && landuse_2011 <= 43, 1, 0)"
r.neighbors -c input=forest output=forest_smooth size=15 method=average
r.colors map=forest_smooth color=ndvi
Travel time to cities
Here we will compute travel time to cities (> population 20000) as cumulative cost distance where cost is defined as travel time on roads. First we specify the speed on different types of roads. We copy the roads raster into our mapset so that we can change it by adding a new attribute field speed. Then we assign speed values based on the type of road:
g.copy vector=roads,myroads
v.db.addcolumn map=myroads columns="speed double precision"
v.db.update map=myroads column=speed value=50 where="MTFCC = 'S1400'"
v.db.update map=myroads column=speed value=100 where="MTFCC IN ('S1100', 'S1200')"
Now we rasterize the selected road types using the speed values from the attribute table as raster values.
v.to.rast input=myroads type=line where="MTFCC IN ('S1100', 'S1200', 'S1400')" output=roads_speed use=attr attribute_column=speed
We set the rest of the area to low speed and recompute the speed as time to travel through a 30m cell in minutes:
r.null map=roads_speed null=5
r.mapcalc "roads_travel_time = 1.8 / roads_speed"
Finally we compute the travel time to larger cities using r.cost:
r.cost input=roads_travel_time output=travel_time_cities start_points=cities_20000
r.colors map=travel_time_cities color=byr
Road density
We will rasterize roads and use moving window analysis (r.neighbors) to compute road density:
v.to.rast input=roads output=roads use=val
r.null map=roads null=0
r.neighbors -c input=roads output=road_dens size=25 method=average
Distance to interchanges
We will consider TIGER roads of type Ramp as interchanges, rasterize them and compute euclidean distance to them:
v.to.rast input=roads where="MTFCC = 'S1630'" output=interchanges use=val
r.grow.distance -m input=interchanges distance=dist_interchanges
Development pressure
Computing development pressure with r.futures.devpressure:
r.futures.devpressure -n input=urban_2011 output=devpressure_0_5 method=gravity size=20 gamma=0.5 scaling_factor=0.001
When gamma increases, development influence decreases more rapidly with distance. Size is half the size of the moving window. To explore the effects of these parameters, you can view the development pressure as a surface with draped urban_2011 raster over it.
TODO We should derive more layers with different gamma parameters for the potential statistical model.
Rescaling variables
First we will look at the ranges of our predictor variables by running a short Python code snippet in Python shell:
for name in ('slope', 'dist_to_water', 'dist_to_protected', 'forest_smooth', 'travel_time_cities', 'road_dens', 'dist_interchanges', 'devpressure_0_5'):
minmax = grass.raster_info(name)
print name, minmax['min'], minmax['max']
We will rescale some of our input variables:
r.mapcalc "dist_to_water_km = dist_to_water / 1000"
r.mapcalc "dist_to_protected_km = dist_to_protected / 1000"
r.mapcalc "dist_interchanges_km = dist_interchanges / 1000"
r.mapcalc "road_dens_perc = road_dens * 100"
r.mapcalc "forest_smooth_perc = forest_smooth * 100"
Sampling
To sample only in the analyzed counties, we will clip development layer:
r.mapcalc "urban_2011_clip = if(counties_FIPS, urban_2011)"
To estimate the number of sampling points, we can use r.report to report number of developed/undeveloped cells and their ratio.
r.report map=urban_2011 units=h,c,p
We will sample the predictors and the response variable with 10000 random points in undeveloped areas and 2000 points in developed area:
r.sample.category input=urban_2011_clip output=sampling sampled=counties_FIPS,devpressure_0_5,slope,road_dens_perc,forest_smooth_perc,dist_to_water_km,dist_to_protected_km,dist_interchanges_km,travel_time_cities npoints=10000,2000
The attribute table can be exported as CSV file (not necessary step):
v.db.select map=sampling columns=urban_2011_clip,counties_FIPS,devpressure_0_5,slope,road_dens_perc,forest_smooth_perc,dist_to_water_km,dist_to_protected_km,dist_interchanges_km,travel_time_cities separator=comma file=samples.csv
Development potential
Now we find best model for predicting urbanization using r.futures.potential which wraps an R script.
r.futures.potential input=sampling output=potential.csv columns=devpressure_0_5,slope,road_dens_perc,forest_smooth_perc,dist_to_water_km,dist_to_protected_km,dist_interchanges_km,travel_time_cities developed_column=urban_2011_clip subregions_column=counties_FIPS min_variables=4
Demand
TODO: check if we need to mask roads
v.to.rast input=roads type=line output=roads_mask use=val
r.mask roads_mask -i
We will use r.futures.demand which derives the population vs. development relation. The relation can be linear/logarithmic/exponential/exponential approach.
The format of the input population CSV files is described in the manual. It is important to have synchronized categories of subregions and the column headers of the CSV files (in our case FIPS number). How to simply generate the list of years (for which demand is computed) is described in r.futures.demand manual.
r.futures.demand development=urban_1992,urban_2001,urban_2011 subregions=counties_FIPS observed_population=population_trend.csv projected_population=population_projection.csv simulation_times=2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030,2031,2032,2033,2034,2035 plot=plot_demand.pdf demand=demand.csv separator=comma
In your current working directory, you should find files plot_demand.png and demand.csv.
Creating incentive table
We need to prepare incentive table, which allows us to run urban sprawl/infill scenarios by changing probabilities on the landscape with a power function.
It's easy to generate one using python commands as shown on r.futures.pga module manual page:
import numpy as np
power = 2
length = 1001
x = np.linspace(0, 1, length)
np.savetxt(fname="incentive_table.txt", fmt='%1.3f', delimiter=',', X=np.column_stack((x, np.power(x, power))), header=",{}".format(length), comments='')
Paste those commands for example to the Python Shell in GRASS GIS(line by line). You can change the power to a number between 0.25 and 4 to test urban sprawl/infill scenarios.
FUTURES simulation
TODO
r.futures.pga developed=urban_2011 predictors=slope,road_dens_perc,forest_smooth_perc,dist_to_water_km,dist_to_protected_km,dist_interchanges_km,travel_time_cities demand=demand.csv devpot_params=potential.csv discount_factor=0.3 compactness_mean=0.4 compactness_range=0.08 num_neighbors=4 seed_search=2 patch_sizes=patches.txt development_pressure=devpressure_0_5 n_dev_neighbourhood=20 development_pressure_approach=gravity gamma=0.5 scaling_factor=0.001 subregions=counties_FIPS incentive_table=incentive_table.txt random_seed=1 output=final output_series=final