Level: ⚫⚫⚫⚫⚪ Advanced

Requirements

  • A GitHub Codespace for MAgPIE — GAMS, R (with the gms and magpie4 packages) and a MAgPIE checkout are pre-installed
  • The MAgPIE default input data — download it once via Rscript start.R -> “download data” (skip this if it is still present from a previous session)

Content

  • adding a new module realization
  • changing the MAgPIE GAMS code
  • integrating and testing the new realization

Overview

Introduction

MAgPIE has a modular concept. Each module can have several realizations: one holds the current default behavior, while you develop a new one alongside it. A typical workflow is to copy the current default realization, rename it, and apply your changes — which is exactly what we do here.

We add a new realization pop_growth to the urban land module (modules/34_urban). The module currently has two realizations: static (urban land constant over time) and exo_nov21 (urban land prescribed from an input dataset — the default). Our pop_growth realization instead lets urban land grow with population. This is for illustrative purposes only.

exo_nov21 already pulls urban land towards a cellular target using a soft penalty (urban land may deviate from the target at a high cost, which keeps the model feasible). We reuse that machinery and only change where the target comes from: instead of reading it from a file, we compute it from population growth. The reasoning is explained in the Details section at the end.

Adding a new realization

Copy the default realization

From the MAgPIE main folder, copy exo_nov21 to a new pop_growth folder. Use the terminal — in Codespaces you cannot duplicate a folder by clicking on it:

cp -r modules/34_urban/exo_nov21 modules/34_urban/pop_growth

Remove the input data

pop_growth computes its target instead of reading it from a file, so delete the input-data parts of the copy:

rm -rf modules/34_urban/pop_growth/input.gms \
       modules/34_urban/pop_growth/input \
       modules/34_urban/pop_growth/sets.gms

Edit the files

For each file below, set its content to what is shown. Keep the license header (the *** | block) at the top. In declarations.gms, also keep the auto-generated R SECTION block at the bottom — it is updated for you by the helper commands in the next step. The headers and R SECTION blocks are omitted from the code below for brevity.

declarations.gms

Declares the population-growth parameter and the target, the deviation-cost scalar, the cost and deviation variables, and the equations:

parameters
 p34_pop_growth(t_all,i)   annual population growth rate (1)
 p34_urban_target(j)       urban land target from population growth (mio. ha)
;

scalars
 s34_urban_deviation_cost  artificial cost for deviating from the urban target (USD17MER per ha)  / 1e+06 /
;

positive variables
 vm_cost_urban(j)          Technical adjustment costs
 v34_cost1(j)              Technical adjustment costs
 v34_cost2(j)              Technical adjustment costs
;

equations
 q34_urban_cell(j)         Constraint for urban land
 q34_urban_cost1(j)        Technical punishment equation
 q34_urban_cost2(j)        Technical punishment equation
 q34_bv_urban(j,potnatveg) Biodiversity value for urban land (Mha)
;

preloop.gms

Computes the annual population growth rate from the population driver im_pop, and initialises the biodiversity value:

$ontext
Calculate the annual population growth rate. Since t_all has 5-year time steps, we
divide the change between time steps by the number of years between them
(m_yeardiff) to get annual values.
$offtext
loop(t_all$(ord(t_all) > 1),
 p34_pop_growth(t_all,i) = (im_pop(t_all,i)/im_pop(t_all-1,i) - 1) / m_yeardiff(t_all);
);

* Initialize biodiversity value
vm_bv.l(j2,"urban", potnatveg) =
  pcm_land(j2,"urban") * fm_bii_coeff("urban",potnatveg) * fm_luh2_side_layers(j2,potnatveg);

presolve.gms

Computes the target each time step — the previous urban land scaled by population growth — and fixes urban land in the first time step (see Details):

vm_carbon_stock.fx(j,"urban",ag_pools,stockType) = 0;

if(ord(t) = 1,
  p34_urban_target(j)   = pcm_land(j,"urban");
  vm_land.fx(j,"urban") = pcm_land(j,"urban");
else
  p34_urban_target(j)   = pcm_land(j,"urban") * (1 + sum(cell(i,j), p34_pop_growth(t,i)) * m_timestep_length);
  vm_land.lo(j,"urban") = 0;
  vm_land.l(j,"urban")  = p34_urban_target(j);
  vm_land.up(j,"urban") = Inf;
);

equations.gms

Holds the soft penalty: q34_urban_cost1 and q34_urban_cost2 measure the deviation of urban land below and above the target, q34_urban_cell charges it to the costs, and q34_bv_urban sets the biodiversity value:

q34_urban_cost1(j2) ..
            v34_cost1(j2) =g= p34_urban_target(j2) - vm_land(j2,"urban");

q34_urban_cost2(j2) ..
            v34_cost2(j2) =g= vm_land(j2,"urban") - p34_urban_target(j2);

q34_urban_cell(j2) ..
            vm_cost_urban(j2) =e= (v34_cost1(j2) + v34_cost2(j2)) * s34_urban_deviation_cost;

q34_bv_urban(j2,potnatveg) ..
 vm_bv(j2,"urban", potnatveg) =e= vm_land(j2,"urban") * fm_bii_coeff("urban",potnatveg) * fm_luh2_side_layers(j2,potnatveg);

scaling.gms and realization.gms

Leave scaling.gms unchanged (it scales vm_cost_urban for the solver). In realization.gms, update the @description to document the new behavior:

*' @description In this realization urban land changes over time with population
*' growth. Each time step urban land is pulled towards a target equal to the previous
*' urban land scaled by population growth, via a high punishment term for deviating
*' from the target. Carbon stocks are assumed zero.

*' @limitations Only for illustrative purpose.

Update and check the code

In the main folder, open an R session (type R) and run:

gms::update_fulldataOutput()
gms::update_modules_embedding()
gms::codeCheck(interactive = TRUE)

The first two commands regenerate the auto-managed parts (postsolve.gms, the output declarations in declarations.gms, and the phase list in realization.gms). codeCheck then verifies the module interfaces. When it stops at an interface that is not handled in every realization, type a short reason such as not needed (or press Enter) and it writes the not_used.txt entry for you — type n only if the interface should actually be handled in code (see Details). Then quit R with q() to return to the terminal — the commands in the next section run in the shell, not in R.

Testing the new realization

Compile and run

Set the realization to pop_growth in main.gms: open main.gms and change the line $setglobal urban exo_nov21 to $setglobal urban pop_growth. Equivalently, run the sed command below in the terminal (not in R). Then check that the model compiles:

sed -i 's/^$setglobal urban .*/$setglobal urban  pop_growth/' main.gms
gams main.gms action=C

If there are errors, look into main.lst. For a fast test, reduce the number of time steps to three: change the line $setglobal c_timesteps coup2100 to $setglobal c_timesteps quicktest, or run the command below in the terminal. Then run the model (this can take 10–15 minutes, depending on the Codespace):

sed -i 's/^$setglobal c_timesteps .*/$setglobal c_timesteps  quicktest/' main.gms
gams main.gms

This creates a fulldata.gdx in the main folder. The commands above are a quick GAMS test; for a full productive run, use a dedicated start script instead (see Details).

Check the results

Start an R session in the main folder (type R) and run:

options(digits=2)
library(magpie4)
gdx <- "fulldata.gdx"
land(gdx,level="glo",types="urban")
land(gdx,level="reg",types="urban")

Global urban land now grows with population (your exact numbers may differ slightly with the input data version):

y1995 y2010 y2025
   48    55    60

Regionally it grows fastest where population grows fastest (e.g. SSA, MEA) and stays roughly constant where population stagnates (e.g. JPN, EUR). The base year (y1995) reproduces the initial urban land (taken from the LUH input data), because urban land is fixed in the first time step.

Clean up

When you are done, reset the model to its default state so the following tutorials are not affected. Run these commands in the terminal — if R is still open from the previous step, quit it first with q(). This restores main.gms (in particular the urban realization and the time steps), removes the new realization, and checks the result:

git checkout main.gms modules/
rm -rf modules/34_urban/pop_growth modules/34_urban/exo_nov21/not_used.txt
git status

git status should report no modified tracked files and no pop_growth folder under modules/34_urban/ — the model code is back to default. Any untracked run artifacts that remain (e.g. restart_*.g00) are harmless; your run outputs (the output/ folder, fulldata.gdx) and the downloaded input data are kept.

Details and background

Why a soft penalty instead of a hard equation? Forcing urban land to exactly match the target with a hard =e= equation can make the model infeasible: in a tight cell there may be no room left to grow urban land without violating another land constraint. The soft penalty lets urban land deviate from the target at a high cost (s34_urban_deviation_cost = 1e6 USD per ha): q34_urban_cost1 and q34_urban_cost2 measure the deviation below and above the target, and q34_urban_cell charges it to vm_cost_urban. Urban land therefore follows the target closely while the model stays feasible. This is the same pattern exo_nov21 uses for its input-data target.

First time step. Urban land is fixed to its initial value in the first time step, so the base year keeps the input urban land (taken from the LUH land-use dataset) rather than the growth target, which would otherwise already move urban land in the base year. From the second time step on it is freed and follows the target.

Productive run. The quick test above runs GAMS directly. For a full run (complete time horizon and output processing), do not edit config/default.cfg. Instead add a start script scripts/start/pop_growth.R that overrides only the urban realization:

library(gms)
source("scripts/start_functions.R")
source("config/default.cfg")

cfg$title     <- "pop_growth"
cfg$gms$urban <- "pop_growth"

start_run(cfg, codeCheck = FALSE)

and launch it with Rscript start.R runscripts=pop_growth submit=direct (or run Rscript start.R without arguments for an interactive menu to pick the start script and submission type). start_run writes the config’s settings into main.gms, so running this script also resets the $setglobal lines edited during the quick test back to their configured values.

License header. Every .gms file must start with the MAgPIE license header (the *** | block). codeCheck checks for it, so copy it to the top of any new file you create.

Interfaces and not_used.txt. pop_growth uses the population interface im_pop, which the sibling realizations static and exo_nov21 do not. Every interface must be addressed in all realizations of a module, so codeCheck adds im_pop, input, not needed to a not_used.txt in static and exo_nov21. Conversely, pop_growth does not use sm_fix_SSP2 (which exo_nov21 uses), so it gets a not_used.txt listing sm_fix_SSP2. In interactive mode codeCheck stops at each such interface and asks for a reason; type a short reason (e.g. not needed, or press Enter for a default) and it appends the entry to the relevant not_used.txt. Type n only if the interface should actually be handled in the code.

What we changed relative to exo_nov21. We removed the input dataset (input.gms, the input/ folder, sets.gms) and the regional-total constraint q34_urban_land, and replaced the input-data target i34_urban_area with the population-growth target p34_urban_target computed in preloop.gms and presolve.gms. The soft-penalty equations, the biodiversity equation and the scaling are reused unchanged.


Overview