Idraluna Archives

Mapping Fantasy-Antarctica

My campaigns are all located on a fictional far-future version of Antarctica (called Antibor), made temperate by alien geo-engineering. To lend the world map as much verisimilitude as possible (& because it made for a diverting project) I used real-world data, QGIS, and R to make as realistic a map as I could. This had three stages:

  1. Calculating new coastlines and accounting for 'isostatic rebound' in which tectonic plates rise slightly when no longer burdened by ice sheets.

  2. Using hydrology-simulating tools to place rivers.

  3. Using (crude) machine-learning to assign biomes to each pixel on the map.

At the end of all this I have an expansive geographic setting that looks and feels pretty realistic even if it is totally fanciful. I can then apply my GIS Hexcrawl process to any desired subset of it to initiate prep for a new campaign.


Step 1: Topography

The process for this step pulls directly from this assignment, so credit is due to Mark Helper.

The Plan

To get a post-ice continent, I needed to account for both the rising sea level and the 'rebound effect' that occurs when the Antarctic bedrock is no longer burdened with massive ice sheets. These can be done in the raster calculator in QGIS.

Data

In Raster calculator, I entered the following:

bedmap2_bed + bedmap2_thickness*0.2825 - 80.5

The .02825 models the 'isostatic rebound' when the ice melts, and the 80.5 adjusts the sea level to account for the expected rise once all the ice has melted.

I then used the Reclassify raster tool to make a land/water raster with all negative pixels to 0 and positive pixels to 1, and the Polygonize tool to turn this into a vector map of the coastline. The result looked like this:

Step 2: Climate and Hydrology

Next, I wanted to generate rivers and watersheds using the built-in simulation tools in QGIS.

The Plan

Data

Converting RACMO projections to the right coordinates

The data come as .nc files which are commonly used for storing time series rasters for meteorology and climate modeling. They show up as expandable databases in the QGIS browser.

I needed only four sub-files:

The t2m and precip layers were actually stacks of 85 rasters, one for each year of the climate sim. I used Rearrange bands in the QGIS toolbox to grab out year 85 from each and save them as separate .tif files.

Unfortunately, RACMO data were in a weird lat/long projection that doesn’t line up with what I'd been using in the previous step. Moreover, QGIS doesn’t seem to have a good transformation between them.

My dumb but robust solution was as follows: Make a raster stack of lat, lon, temp, and precipitation → convert the raster to a .csv file where each pixel is a row → convert the .csv file to a point layer using the lat-lon coordinate info for each pixel → reproject points into desired coordinates → interpolate the points back into a raster.

Silly? Yes, but it got the job done. I used an R script to do the first half, reproduced below. Note that I also made some adjustments to the temperature - converting to Celsius and adjusting it so that the climate ranges from around -10 C to 25 C.

library(raster)
library(dplyr)

RACMO_temp <- raster("t2m_yearly.tif")
RACMO_precip <- raster("precip_yearly.tif")
RACMO_lon <- raster("RACMO_lon.tif")
RACMO_lat <- raster("RACMO_lat.tif")

RACMO_stack <- stack(RACMO_lon, RACMO_lat, RACMO_temp, RACMO_precip)
RACMO_df <- as.data.frame(RACMO_stack) %>%
  mutate(T_C = t2m_yearly - 273) %>%
  mutate(future_climate = T_C/1.25 + 35)

plot(density(RACMO_df$T_C), col = 'blue', xlim = c(-50, 50))
lines(density(RACMO_df$future_climate), col = 'red')

write.csv(RACMO_df, "RACMO_df.csv")

Loading the points into Qgis, I got this:

I then reprojected the layer into the Antarctic Polar CRS and used the TIN Interpolation tool to create temperature and precipitation rasters of the same extent and cellsize as the elevation raster.

Modeling Hydrology

With a precipitation map and a terrain map, I was ready to calculate where rivers should go.

QGIS has several watershed algorithm calculations available through the GRASS and SAGA plugins. I tried several and found that the r.watershed tool served my purposes best.

The elevation parameter is self-explanatory. For the overland flow parameter (which assigns how much water starts flowing from each grid cell), I used a rescaled version of the precipitation map that divided each pixel by the modal value of 1500. The default flow amount is 1, so this way most pixels have a flow value of 1, with a few being higher or lower. Bumping the convergence factor from 5 to 7 resulted in fewer, larger rivers rather than a maze of small streams. (Moving forward, I'll probably do a few passes with different convergence factors to get a hierarchy of streams, or do local simulations when making a smaller hexcrawl map.)

The result was a raster that looked like this:

To turn the rivers into polyline objects, I used the r.thin tool followed by r.to.vect. I also used the Smooth tool with two iterations to make the rivers less jagged looking. (Hard to see but the pink lines are before and the purple after smoothing)

Step 3: Climate Zones and Biomes

For this step, I wanted to divide the map into rough zones that could guide finer-scale biome allocation. The following is NOT meant to be scientific, only a loose approximation.

The Plan

Biomes can be roughly plotted on a wet/dry and a warm/cool axis. Thus, the measurements of temperature and precipitation derived above are key for assigning them to the map.

Source

I tried several methods of manually slicing up the climate space as in the graph above but eventually settled on using machine learning to assign each pixel a biome. This was done as follows:

Data

Preparation

First, I was curious to see if my pixels matched the triangular pattern on the above chart (especially after my meddling), so used R to plot them.

library(raster)
reference_map <- raster("post_rebound_binary.tif")
temp <- raster("future_temp.tif")
precip <- raster("precip_yearly_mm.tif")

biome_stack <- stack(reference_map, temp, precip)
biome_df <- as.data.frame(biome_stack, xy = TRUE)

biome_df <- biome_df %>%
  filter(post_rebound_binary == 1) %>%
  slice_sample(n = 10000)

plot(biome_df$future_temp, biome_df$precip_yearly_mm, cex = 0.5, pch = 16, col = rgb(red = .5, green = .25, blue = .5, alpha = 0.1), ylim = c(0,6000), xlim = c(0,32))

The results are pretty close; note that my rainfall variable is measured in mm.

Assigning Biomes

Here is the R code I used to assign biomes to the Antarctica map:

### Modeling biomes based on real-world data
library(terra)
library(tidyterra)
library(dplyr)
library(class)
library(psych)
library(e1071)
set.seed(997859)

# Load in present-day Earth data
world_elev <- rast("ETOPO_DEM60s.tif") %>%
  aggregate(fact = 3, fun = 'mean')  #downscale as super-high res not needed
world_temp <- rast("tempC.tif") %>%
  aggregate(fact = 3, fun = 'mean')
world_precip <- rast("rainmm.tif") %>%
  aggregate(fact = 3, fun = 'mean')
world_biome <- rast("Earth_biomes.tif") %>%
  aggregate(fact = 3, fun = 'modal')

# calculate distance to coastlines
world_water <- ifel(is.na(world_biome), 1, 0)
plot(world_water)
world_wdist <- gridDist(world_water, target = 1, scale = 1000, overwrite=TRUE)
plot(world_wdist)

world_biodata <- c(world_elev, world_temp, world_precip, world_wdist, world_biome)
names(world_biodata) <- c('elev', 'temp', 'precip', 'ocean_dist', 'biome')

# Sample pixels for training/testing a classifier
sample_pts <- spatSample(world_biodata, size = 20000, method = 'random', replace = F, na.rm = TRUE)
summary(sample_pts)

par(mfrow = c(1,4))
plot(density(sample_pts$elev))
plot(density(sample_pts$temp))
plot(density(sample_pts$precip))
plot(density(sample_pts$ocean_dist))

sample_pts <- sample_pts %>%
  mutate(biome = as.factor(biome),
         id = row_number())

dev.off()
plot(sample_pts$temp, sample_pts$precip, col = sample_pts$biome, pch = 16, cex = 0.5)
cor(sample_pts[,1:4])

# check counts to make sure each biome is represented
biome_count <- sample_pts %>%
  group_by(biome) %>%
  summarise(count = n())

# Train/test a svm classifier
train_df <- sample_pts %>%
  group_by(biome) %>%
  slice_sample(prop = 0.5)

test_df <- sample_pts %>%
  filter(!(id %in% train_df$id))

svm_biome <- svm(biome ~ temp + elev + precip + ocean_dist, data = train_df)
svm_biome_pred <- predict(svm_biome, test_df)
table(svm_biome_pred, test_df$biome)
print(sum(diag(table(svm_biome_pred, test_df$biome)))/sum(table(svm_biome_pred, test_df$biome)))
cohen.kappa(cbind(svm_biome_pred, test_df$biome))

# Apply to Antarctica
reference_map <- rast("post_rebound_binary.tif")
temp <- rast("future_temp.tif")
precip <- rast("precip_yearly_mm.tif")
elev <- rast("Antarctica_rebound_sealevelrise.tif")

biome_stack <- c(temp, precip, reference_map, elev)
biome_stack$water_dist <- gridDist(biome_stack$post_rebound_binary, target = 0, scale=1000)
biome_stack[is.na(biome_stack$post_rebound_binary)] <- NA
biome_stack[biome_stack$post_rebound_binary != 1] <- NA
biome_stack$precip_yearly_mm2 <- biome_stack$precip_yearly_mm + 250 + (250*(((700.0874 - biome_stack$water_dist)/700087.4)))  # 250 extra mm by water to zero farthest inland

plot(biome_stack)
names(biome_stack) <- c('temp', 'precip_base', 'land', 'elev', 'ocean_dist', 'precip')

# Compare AA to World
AA_samp <- spatSample(biome_stack, size = 2000, method = 'random', replace = F, na.rm = TRUE)
plot(sample_pts$temp, sample_pts$precip, col = rgb(0,0,0, alpha = 0.1), pch = 16, cex = 0.5)
points(AA_samp$temp, AA_samp$precip, col = rgb(0,0,0, alpha = 0.1), pch = 16, cex = 0.5)

# Predict for AA
AA_biome <- predict(biome_stack, svm_biome, na.rm = TRUE)
plot(AA_biome, colNA = 'gray', type = 'classes')

writeRaster(AA_biome, "biomes2.tif", overwrite = TRUE)

A few notes:

Results

The SVM classifier was able to get 73% accuracy on the present-day biome data.

Classes 2 (Tropical and subtropical dry broadleaf forests), 3 (Tropical and subtropical coniferous forests), 9 (Flooded Grasslands and Savannas), and 14 (Mangroves) all had zero accurate predictions, but otherwise it did pretty well. I suspect that their zones of climate space were completely absorbed by larger categories, as they all had good negative prediction rates (i.e. no points were falsely assigned to them).

The Final Biome Map

Categories are:

  1. Tropical and subtropical moist broadleaf forests
  2. Temperate broadleaf and mixed forests
  3. Temperate conifer forests
  4. Boreal forests or taiga
  5. Tropical and subtropical grasslands, savannas, and shrublands
  6. Temperate grasslands, savannas, and shrublands
  7. Montane grasslands and shrublands
  8. Tundra
  9. Mediterranean forests, woodlands, and scrub
  10. Deserts and xeric shrublands.

Thus Antibor is a continent dominated by a vast expanse of cold steppe with a smattering of coniferous forests. Beginning 200-500 kilometers from the sea, the steppe gives way to Mediterranean chaparral, deciduous forests, savannahs, and occasionally deserts (especially in the mountains and around bays). On the windward side of the long, snaky mountainous islands grow steaming jungles.


Discuss this post on Reddit

#DIY #GIS #antibor