Lunar Climate Simulations
Got the itch to do more speculative cartography, so for the last week or two I've been simulating the weather on the Moon, if it had an earth-like atmosphere.
Is this even possible?
Probably, sort of? Apparently, an advanced civilization could grace the lunar surface with air & liquid water by slamming ice comets into it. The low gravity & lack of magnetic field mean that the atmosphere wouldn't stick around forever. I've seen different estimates for its longevity, but the lowest I've seen is around tens of thousands of years. Since recorded history is only ~5,000 years old, that seems like plenty of time to work with.1 (And maybe the ancient terraformers orbitally engineered a queue of small comets to replenish the atmosphere every 200 years).
But at the end of the day, like with my Antarctica maps, this is Art & not Science. My goal is to make believable-looking maps for elfgames, not to definitively predict what Luna would look like after terraforming.
Climate Simulations
I used software called ExoPlaSim to simulate the Lunar atmosphere. I closely followed the workflow described by Worldbuilding Pasta (and you should definitely check that blog out if you're interested in this kind of thing).
ExoPlaSim is a fork of an earth-focused climate circulation model called PlaSim, extended to allow users to play with different star types, orbits, planet sizes, etc. It simulates the transfer of energy & matter within 3d grid cells representing the atmosphere & upper ocean and generates a wealth of information about temperature, precipitation, & wind. It even simulates feedbacks from vegetation coverage. Again, check out Worldbuilding Pasta to see deep-dives on its many applications.
Installing Exoplasim
Unfortunately, getting ExoPlaSim to work is a huge pain in the ass, as evidenced by the number of people trying to troubleshoot in Worldbuilding Pasta's comments. After a couple failed attempts to run it on an Ubuntu 24 computer, I made a Docker container to isolate its dependencies.
(My crude, layman's understanding of Docker is that it allows one to easily create 'containers' which are like virtual machines that contain an operating system and all the dependencies needed to run a specific program. It's a relatively 'safe' way to run software with weird dependencies without screwing up your OS, and because they're built from a script, it's easy to iteratively tweak & test.)
My dockerfile looks like this (note that it's mostly copied from WP's virtual machine instructions):
FROM ubuntu:22.04
RUN apt update -y
RUN apt install -y f2c
RUN apt install -y fftw3
RUN apt install -y openmpi-bin libopenmpi-dev
RUN apt install -y python3-pip
RUN apt install -y python-is-python3
RUN pip install netCDF4
RUN pip install exoplasim[netCDF4]
# Necessary for OpenMPI, which allows multi-core processing I think
ENV OMPI_ALLOW_RUN_AS_ROOT=1
ENV OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1
ADD ./To_add/exs_test.py .
RUN python exs_test.py
The file exs_test.py
was suggested in the ExoPlaSim docs as a lightweight test of the model. Running it in the docker setup script causes the exoplasim
library to run a one-time Fortran setup process that often causes issues. Once it's done, the software should be fully functional.
import exoplasim as exo
mymodel = exo.Model(workdir="mymodel_testrun",modelname="mymodel",resolution="T21",layers=10,ncpus=4)
mymodel.configure()
mymodel.exportcfg()
mymodel.run(years=10,crashifbroken=True)
mymodel.finalize("mymodel_output")
To build the container, I opened a terminal & navigated to the folder with the dockerfile (named dockerfile
with no extension) and including /To_add/exs_test.py
, and ran:
sudo docker build -t exoplasim_docker ./docker
And with the container built, I can run it by running:
sudo docker run -it --mount "type=bind,src=/home/idraluna-archives/D&D/Maps/Lune,dst=/Luna" exoplasim_docker
This lets me 'enter' the container and instructs docker to make the folder at the src
location available in it under the name /Luna
. I can then run cd Luna
and python simulation_script.py
to run ExoPlaSim.
Model Setup
Setting up ExoPlaSim for Luna was pretty easy, since the Moon shares many parameter values with the default (Earth) numbers.
Terrain
I snagged the lowest-resolution heightmap from NASA's CGI Moon Kit. This conveniently already has the same 2:1 aspect ratio needed for ExoPlaSim, and I used the Rescale Image tool in Krita to downscale it to 128x256 pixels. I then fed it into the convert_sra.py
script by OstimeusAlex found here, adding this line at the end:
convert_sra(filepath="./", infile="./ldem_4_T42.tif", grav=1.62,
debug_img=True, desert_planet=False, floor_value=0,
peak_value=10500, resotext="T21", sra_name="Luna_testT21")
Some notes:
- Lunar gravity is 1.62 m/s^2
- The file was a .tif, with floating point values representing km above or below the 'reference height', which I assume to be the mean or median elevation.
floor_value
sets the sea level, which I just set at the 'reference height', as mentioned in the intro. resotext
refers to a set of codes used to denote model resolution. For home computers, T21 (32x64) or T42 (64x128) are most viable. I made elevation outputs for both, but I ended up using T21. In addition to being faster to process, I'll be comparing outputs to Earth simulations run at T42, and don't want the size disparity to be too extreme.
The approximate coastlines look like this:
The Script
I copied WP's template here and changed the following parameters:
rotationperiod=29.5
obliquity=6.7
gravity=1.62,
radius=0.272
landmap="SRA/Luna_testT21_surf_0172.sra"
topomap="SRA/Luna_testT21_surf_0129.sra"
Unfortunately, ExoPlaSim can't do moons properly yet, so I was actually simulating a planet with the Moon's size, tilt, and rotation and Earth's orbit. I'm sure the slight variation in solar flux should have some effect but I doubt it's worth noting -- the radius of the moon's orbit around Earth is two thousandth's of its solar orbit radius and smaller than their shared eccentricity.
WP's script is cleverly written to calibrate atmospheric CO2 to get the climate to settle at a desired mean temperature.
Initial Observations & Scenarios
From some initial test runs, I learned that because the moon 1) is very small and 2) rotates slowly, the atmosphere doesn't form the atmospheric cells that keep Earth's climate zones so wonderfully variegated. It thus tends to produce Star-Wars-esque monoclimates that depend on the average temperature.
This also makes it prone to 'snowball' feedback loops where any level of glacier cover increases surface albedo, cooling the planet further until the whole thing is covered in ice. Starting the simulation with anything under ~1900 ppm CO2 seemed to guarantee settling at a frigid 220K (-53C, -63F).
Anyways, I tried out the following scenarios:
Scenario | Mean Temp | Rotation (Earth days) |
---|---|---|
A | 300K (27C, 80F) | 29.5 |
B | 290K (17C, 62F) | 29.5 |
C | 290K (17C, 62F) | 1 |
Outputs
ExoPlaSim produces .nc
output files that can be read in by QGIS. Each file represents a year & contains dozens of variables (explained here), and each variable will contain one raster layer per reporting timestep (12/year by default, I ended up doing 36 since the lunar day is a month long.)
To symbolize them, change from the default multiband (which maps values to color channels) to "Singleband Pseudocolor" and pick an appropriate gradient:
To mass-visualize runs from multiple scenarios, I wrote an R script, included at the end of this post. The data for earth was generated by running the model for 60 years with all default parameters and averaging the last decade.2 (Keep in mind that the Moon is much smaller than the Earth, so the map scales are misleading).
Scenario A: Jungle World
I set the minimum CO2 parameter rather high, so the model settled at a steamy 302K (29C, 83F). Day length was left at 29.5 Earth-days.
The entire planet resembles Earth's tropics, with a bit of fluctuation in the interior of the largest landmass.
It's extremely wet, albeit with large fluctuations in precipitation. I assume this is tied to the day-night cycle, though I don't have granular-enough reports to clearly asses whether day, night, or twilight is wetter.
There are some coastal regions with intense cloud formation, but otherwise skies are clear. (Not really sure how to interpret this one tbh.)
Vast, inland forests cover the middle latitudes.
Vegetation follows the same pattern.
And there's not a flake of snow to be found.
This version of the Moon, I think, has to be one with shirtless Frazetta-esque barbarians leaping through massive rainforest canopies to hunt pterodactlys or something.
Scenario B: Selenic Steppes
By dropping the minimum CO2 parameter I was able to get the simulation to settle at a mean temperature of 290K (17C, 62F).
It's still warmer than much of Earth, but things are more temperate, especially inland.
Precipitation is accordingly toned down. On land, rainfall resembles that in the southeastern US.
Cloud cover patterns remain similar, though it looks like this Moon is slightly cloudier overall.
ExoPlaSim predicted no forest and almost no plant cover for this scenario. I don't understand its internal vegetation simulation well enough to have a good idea of why this is.
Scenario C: 24-hour days
Accelerating the Moon's rotation should be possible for any civilization able to terraform it, by aligning the atmosphere-providing comets along its rotational direction or using railguns to fling matter off its surface.3 I wanted to see how faster rotation would affect the atmosphere, so I ran another simulation with a target temperature of 290K and a 24 hour rotation period.
This one settled at the same 290K surface temperature, but notice that the temperature variation is less affected by distance inland than by latitude -- so I'm guessing circulation is driven more by Coriolis force.
Precip patterns look similar to the other scenarios, though there's a faint equatorial band with more rain.
No forest, no veg :(. It seems like ExoPlaSim thinks plants need 27-7 greenhouse conditions(?)
This one has a tiny smattering of snow around the south pole!
Final notes
I'm not going to place too much stock in the vegetation simulation. In a future post, I hope to feed some of the temperature & precip outputs into a random forest classifier (similar to the one trained for Antarctica) to predict biomes. Edit: Worldbuilding Pasta kindly clarified this one for me -- plants breathe CO2, so when script shifts atmospheric CO2 down to converge on a desired temperature, it kills off most of the plant life. So for fantasy mapping, I think I'm going to exclude vegetation data and just rely on temperature & precipitation.
With that said, it's tough to get a variegated climate on the Moon, so I'm not sure how likely it is to yield an interesting fantasy map. I'm most optimistic about scenarios A, with equatorial rain forests & something else (?) at the poles, and C, with rudimentary convection cells & a tiny bit of snow.
Appendix: Scenario Output Code
Probably not that useful, but here's the R code I used to generate the plots above:
library(terra)
library(tidyverse)
library(tidyterra)
mapdir <- '../Maps/Lune'
vars <- data.frame(esvar = c('tas', 'pr', 'clt', 'vegf', 'vegplantc', 'glac', 'snd'),
name = c('Surface Temp', 'Precipitation', 'Cloud cover', 'Forest cover', 'Vegetation cover', 'Glacier cover', 'Snow depth'),
unit = c('K', 'm/s', 'fraction', 'fraction', 'fraction', 'fraction', 'm'),
stdv = c(T, T, T, F, F, F, T),
lowcol = c('skyblue', 'white', 'white', 'beige', 'beige', 'white', 'white'),
highcol = c('red', 'blue', 'gray30', 'forestgreen', 'yellowgreen', 'skyblue', 'skyblue')
)
# batch <- '2'
# batchrange <- 37:47
batch <- '3'
batchrange <- 25:35
# batch <- '4'
# batchrange <- 68:78
earth_terrain <- rast(file.path("H:\\My Drive\\RPGs\\Idraluna_Archives", paste0('MOST.000', 10,'.nc'))) %>% select('grnz_1')
earth_outline.shp <- ifel(earth_terrain > 100, 1, 0) %>% as.polygons()
luna_terrain.shp <- rast(file.path(mapdir, 'ldem_16.tif'))
crs(luna_terrain.shp) <- crs("ESRI:103881"); ext(luna_terrain.shp) <- c(-180, 180, -90, 90)
luna_terrain.shp <- as.polygons(ifel(luna_terrain.shp>0, 1, 0))
for (evar in vars$esvar){
vardata <- vars %>% filter(esvar == evar)
if(file.exists(file.path(mapdir, 'Summary_rasters', paste0('earth_', evar, '_mean.tif')))){
print("Loading Earth data from file.")
earth_mean <- rast(file.path(mapdir, 'Summary_rasters', paste0('earth_', evar, '_mean.tif')))
earth_sd <- rast(file.path(mapdir, 'Summary_rasters', paste0('earth_', evar, '_sd.tif')))
} else{
#load earth
print(paste('Loading earth:', evar))
earth_stack <- rast(file.path("H:\\My Drive\\RPGs\\Idraluna_Archives", paste0('MOST.000', 11,'.nc'))) %>% select(starts_with(evar))
for (year in 12:20){
cat(paste0('..', year))
thisyear <- rast(file.path("H:\\My Drive\\RPGs\\Idraluna_Archives", paste0('MOST.000', year,'.nc'))) %>% select(starts_with(evar))
earth_stack <- c(earth_stack, thisyear)
}
cat('...done')
earth_mean <- earth_stack %>% mean()
earth_sd <- earth_stack %>% app(fun = 'sd')
writeRaster(earth_mean, file.path(mapdir, 'Summary_rasters', paste0('earth_', evar, '_mean.tif')))
writeRaster(earth_sd, file.path(mapdir, 'Summary_rasters', paste0('earth_', evar, '_sd.tif')))
}
#load luna
luna_stack <- rast(file.path(mapdir, paste0('model_run',batch,'_', batchrange[1]), paste0('model', batch, '.nc'))) %>% select(starts_with(evar))
for (year in batchrange[2:length(batchrange)]){
folder <- file.path(mapdir, paste0('model_run', batch,'_', year))
print(folder)
thisyear <- rast(file.path(folder, paste0('model', batch,'.nc'))) %>% select(starts_with(evar))
}
print(paste("Loaded luna:", evar))
crs(luna_stack) <- crs("ESRI:103881"); ext(luna_stack) <- c(-180, 180, -90, 90)
luna_mean <- luna_stack %>% mean()
luna_sd <- luna_stack %>% app(fun = 'sd')
#export visuals
earth_quantiles_mean <- global(earth_mean, fun=quantile)
moon_quantiles_mean <- global(luna_mean, fun=quantile)
meanrange <- c(min(earth_quantiles_mean$X0, moon_quantiles_mean$X0), max(earth_quantiles_mean$X100, moon_quantiles_mean$X100))
earth_quantiles_sd <- global(earth_sd, fun=quantile)
moon_quantiles_sd <- global(luna_sd, fun=quantile)
sdrange <- c(min(earth_quantiles_sd$X0, moon_quantiles_sd$X0), max(earth_quantiles_sd$X100, moon_quantiles_sd$X100))
png(file.path(mapdir, 'Plots_out', paste0(evar, batch, '.png')), width=1000, height=500)
par(mfrow = c(2,2))
plot(luna_mean, range=meanrange, fill_range = T, col=colorRampPalette(c(vardata$lowcol, vardata$highcol))(50), main=paste0(vardata$name, '--mean (', vardata$unit, ')')); polys(luna_terrain.shp, lwd=0.25)
plot(luna_sd, range=sdrange, fill_range = T, main=paste0(vardata$name, '--SD (', vardata$unit, ')')); polys(luna_terrain.shp, lwd=0.25)
plot(earth_mean, range=meanrange, fill_range = T, col=colorRampPalette(c(vardata$lowcol, vardata$highcol))(50)); polys(earth_outline.shp)
plot(earth_sd, range=sdrange, fill_range = T)
dev.off()
#export rasters
writeRaster(luna_mean, file.path(mapdir, 'Summary_rasters', paste0(evar, '_mean_', batch, '.tif')), overwrite=T)
writeRaster(luna_sd, file.path(mapdir, 'Summary_rasters', paste0(evar, '_sd_', batch, '.tif')), overwrite=T)
}
Sci-fi story idea: aliens plunk 500 naked humans down on a terraformed moon and explain to them that they have 3,000 years to before the atmosphere becomes too thin to breathe, unless they can geoengineer a solution. What do the erstwhile colonists do to speedrun millennia of civilization? How can they convey a sense of urgency to their descendants?↩
It doesn't perfectly align with Earth's actual atmosphere; I think it's more informative to compare the Lunar outputs to modeled Earth outputs, implicitly illustrating its margin of error (I guess I should provide the actual Earth data too, but I'm lazy).↩
According to this video by Isaac Arthur, a mere 3x10^23 Joules are needed to achieve a 24 hour day.↩