Dynamic drivers and agents, part 2 of 4
Agents: decision makers inside the loop
Give a land change model its decision makers, a farmer per parcel whose wealth follows the soil and the market, and watch the land they manage change with their decisions.
- Level
- Intermediate
- Time
- 40 min
Land does not change by itself: farmers clear it, abandon it, sell it; councils zone it; companies plant it. Most land change models describe the outcome of those decisions statistically, as a suitability and a demand. Agent-based models describe the decision makers themselves: each has a state (its wealth, its land, its age) and rules for changing it, and the land changes because they act. In this part you will build, node by node, the template Farmers on their parcels (agents): sixty farmers, one per parcel, whose wealth follows the fertility of their soil and a price that falls year after year, and who stop farming when they are broke. You will learn the five operators for agents in Terraflux, how a population is carried through a loop like a map, how to read what the agents did, and how sensitive the outcome is to one number in their rules. At the end you will be able to write agents of your own and connect them to the land change models of this category.
The idea#
Agents, populations and rules#
An agent is a decision maker represented individually: a household, a farm, a firm, a municipality. An agent-based model of land use couples a population of agents with a map: the agents decide, their decisions change the land, and the land feeds back on their next decisions (Parker et al., 2003). Since the 1990s such models have been used to study deforestation by colonists, the abandonment of farmland, the spread of suburbs and the effect of subsidies on what farmers grow (Matthews et al., 2007). Their appeal for an ecologist is that they can ask who changes the land and why: what happens to a forest when the price of beef falls, when only large farms get credit, when young people leave. Their difficulty is that human decisions are hard to observe and to calibrate (An, 2012; Rounsevell et al., 2012).
In Terraflux a population is a table: a row per agent, a column per attribute (a number: id,
wealth, farming...). Five operators make and change populations:
| Operator | What it does |
|---|---|
agents.table |
agents written in the model, such as a few countries or markets |
agents.parcels |
an agent per parcel of a map of parcel ids, with its size and the mean of other maps over its parcel |
agents.update |
the agents' behaviour: rules attribute = expression, applied in order to every agent |
agents.map |
an attribute painted on the agents' parcels, as a map |
agents.summary |
the number of agents and the sum and mean of every attribute, as a table of one row |
A population is a value like a map, so a control.repeat loop can carry it: at every step the body reads
the population of the step before (@farmers) and gives the population of this step, which the next step will
read.
Key idea: the rules of the farmers
Every year \(t\), each farmer \(a\) with a parcel of mean fertility \(f_a\) (0 to 1) applies, in order,
\(p_t\) is the price of the crop, falling by a quarter every year; \(y_{a,t}\) the income; \(w_{a,t}\) the wealth, starting at 20; and 7 a yearly cost of living. A farmer breaks even when \(2 f_a p_t = 7\), that is when \(f_a = 3.5 / p_t\): a fertility of 0.36 in the first year (price 9.75), of 0.70 in the twentieth (price 5). As the price falls, the break-even fertility rises past more and more farms.
Agents and the map#
The farmers here act on the map through one attribute, farming, which agents.map paints on their parcels
every year: a map of the parcels still farmed. In a full land change model the same map would become a
restriction, a multiplier of a transition probability or the land of a CRAFTY simulation (see
CRAFTY and a coupled model).
When to use it#
Use agents when the question is about the decision makers: how many farmers would give up under a price, a tax, a subsidy; which parcels a council would protect; how the size of farms changes what is grown. Valbuena et al. (2010), for example, simulated the farmers of a Dutch region as agents of a few types, each with its own rules for expanding, abandoning or diversifying, to see how the landscape would change under policies.
Agents are not needed when only the amount and location of change matter and their causes can stay statistical: a Markov chain and weights of evidence (Dinamica EGO), or a demand and logistic regressions (CLUE-S, the first series), are easier to fit and validate. They are also not a way to add precision: a model of agents whose rules are guessed is less certain, not more, than a statistical one fitted on maps. Choose them when the decisions are the subject.
Terraflux's agents are deliberately simple: rules are expressions over numbers, applied to every agent in the same order; agents do not see each other or trade with each other within a step. For agents that negotiate, learn or move, a dedicated platform (NetLogo, Mesa, GAMA) is the better tool; Terraflux's agents fit models where a population of decision makers modifies maps that a land change model then uses.
What you need#
- A map of parcels: a raster whose cells hold the id of the parcel (the farm, the property, the
municipality) they belong to, missing where none. A cadastre rasterized with
vector.rasterizeis typical. The template draws one: a Voronoi mosaic of 60 parcels. - Maps of what matters to the decisions: here the soil's fertility, 0 to 1.
agents.parcelsgives each agent the mean of each map over its parcel. - Rules: what each agent knows and does each step. They are the hypothesis of the model, and should come from interviews, surveys, economic data or the literature.
The template Farmers on their parcels (agents) (in the application, Templates) has everything drawn by chance, so it runs as it is.
Build the model#
Step 1: the landscape and its parcels#
Model file (TOML)
format = "terraflux/1"
[[nodes]]
id = "mosaic"
op = "nlm.mosaic_tess"
params = { rows = 120, columns = 120, germs = 60, seed = 51 }
[[nodes]]
id = "parcels"
op = "raster.calc"
inputs = { m = "mosaic.map" }
params = { expression = "1 + floor(m * 100000)" }
[[nodes]]
id = "fertility"
op = "nlm.gaussian_field"
params = { rows = 120, columns = 120, range = 25, seed = 52 }
mosaic(nlm.mosaic_tess) draws a Voronoi mosaic of 60 cells on a grid of 120 x 120: every cell takes the random value of its nearest of 60 random points, so the map is made of 60 polygons.parcels(raster.calc) turns each polygon's value (0 to 1) into a whole number, its parcel id. With a real cadastre this node and the one before are araster.readof the parcel ids.fertility(nlm.gaussian_field) draws a smooth random surface from 0 to 1, the soil's fertility, with patches about 25 cells across.



Step 2: the agents#
Model file (TOML)
format = "terraflux/1"
[[nodes]]
id = "mosaic"
op = "nlm.mosaic_tess"
params = { rows = 120, columns = 120, germs = 60, seed = 51 }
[[nodes]]
id = "parcels"
op = "raster.calc"
inputs = { m = "mosaic.map" }
params = { expression = "1 + floor(m * 100000)" }
[[nodes]]
id = "fertility"
op = "nlm.gaussian_field"
params = { rows = 120, columns = 120, range = 25, seed = 52 }
[[nodes]]
id = "farms"
op = "agents.parcels"
inputs = { parcels = "parcels.map", fertility = "fertility.map" }
[[nodes]]
id = "start"
op = "agents.update"
inputs = { agents = "farms.agents" }
params = { rules = "wealth = 20\nfarming = 1" }
farms(agents.parcels) makes an agent per parcel id found inparcels.map, in increasing order of id, with the columnsidandcells(the parcel's size in cells). Every other map connected to it becomes a column of the same name holding the mean of the map over the parcel: the input is namedfertility, so each farmer getsfertility. In the application, after adding the node, type a name in the Node pane to add an input, then dragfertility.maponto it. Connect as many maps as the rules need (slope, distance to the market, a soil class...).start(agents.update) gives every farmer its starting attributes. A rule's left side is an attribute, new or existing; its right side an expression of map algebra (see map algebra) over the agent's attributes and the node's number inputs. Here both rules are constants: every farmer starts with a wealth of 20 and farming.
Rules are lines of attribute = expression, applied in order, each line seeing the result of the lines
before it, to every agent separately. The expressions are those of raster.calc without the neighbourhood
functions: arithmetic, comparisons (1 true, 0 false), and, or, not, ... if ... else ..., min,
max, clamp, exp, log, round... For instance:
price = 10 - step * 0.25
income = fertility * price * 2 if farming == 1 else 1
wealth = wealth + income - 7
farming = farming if wealth > 0 else 0
price is a new attribute, the same for every farmer since it depends only on step; income reads the
price just computed; wealth the income; and farming the new wealth. Swap the last two lines and a
farmer would decide on last year's wealth.
Step 3: the years#
Model file (TOML)
format = "terraflux/1"
[[nodes]]
id = "mosaic"
op = "nlm.mosaic_tess"
params = { rows = 120, columns = 120, germs = 60, seed = 51 }
[[nodes]]
id = "parcels"
op = "raster.calc"
inputs = { m = "mosaic.map" }
params = { expression = "1 + floor(m * 100000)" }
[[nodes]]
id = "fertility"
op = "nlm.gaussian_field"
params = { rows = 120, columns = 120, range = 25, seed = 52 }
[[nodes]]
id = "farms"
op = "agents.parcels"
inputs = { parcels = "parcels.map", fertility = "fertility.map" }
[[nodes]]
id = "start"
op = "agents.update"
inputs = { agents = "farms.agents" }
params = { rules = "wealth = 20\nfarming = 1" }
[[nodes]]
id = "years"
op = "control.repeat"
inputs = { times = 20, farmers = "start.agents" }
[nodes.body]
carry = { farmers = "year.agents" }
collect = { summary = "totals.table", maps = "farmed.map" }
[[nodes.body.nodes]]
id = "year"
op = "agents.update"
inputs = { agents = "@farmers", step = "@step" }
params = { rules = "price = 10 - step * 0.25\nincome = fertility * price * 2 if farming == 1 else 1\nwealth = wealth + income - 7\nfarming = farming if wealth > 0 else 0" }
[[nodes.body.nodes]]
id = "totals"
op = "agents.summary"
inputs = { agents = "year.agents" }
[[nodes.body.nodes]]
id = "farmed"
op = "agents.map"
inputs = { agents = "year.agents", parcels = "parcels.map" }
params = { column = "farming" }
The loop years (control.repeat) has two inputs: times = 20 and farmers = start.agents. Any input
other than times is a carried value: inside the body it is @farmers, the starting farmers at step 1 and,
at every later step, what carry = { farmers = "year.agents" } says: the farmers after the year's rules. The
body:
year(agents.update) applies the four rules above to@farmers. Its second input,step, connected to@step, is a number input: the rules can use it by that name. Any number can be connected this way (a price from a table, a policy switch), named as the rules call it.totals(agents.summary) sums the farmers up: a table of one row withn(the number of agents), then<attribute>_sumand<attribute>_meanfor every attribute (farming_sumis the number still farming,wealth_meantheir mean wealth).farmed(agents.map) paints the attributefarmingof each farmer on the cells of its parcel: 1 farmed, 0 given up, missing outside any parcel.
collect = { summary = "totals.table", maps = "farmed.map" } keeps both every year: years.summary is a
series of 20 tables (written as one CSV, a row per year) and years.maps a series of 20 maps (written as a
raster of 20 bands). After the loop, years.farmers is the population of the last year.
Read @farmers, not start.agents, inside the body
If year read start.agents instead of @farmers, every year would start again from the first year's
farmers: the loop would run, but nothing would accumulate. The carried value is the only way a step sees
what the step before did.
In the application
Open the template from Templates, double-click years to see its body, and click year to read and
edit its rules in the Node pane. To carry a new value, add an input to the loop (its name becomes @name
inside) and set what it takes at the end of each step in the loop's carry. Press Ctrl+Enter on the
loop to run up to it. The template's Plots, Parcels still farmed, year by year, Wealth after 20 years and
Fertility, are in the Viewer's Plots menu: a click opens one.
The whole template
Model file (TOML)
format = "terraflux/1"
[[nodes]]
id = "mosaic"
op = "nlm.mosaic_tess"
params = { rows = 120, columns = 120, germs = 60, seed = 51 }
[[nodes]]
id = "parcels"
op = "raster.calc"
inputs = { m = "mosaic.map" }
params = { expression = "1 + floor(m * 100000)" }
[[nodes]]
id = "fertility"
op = "nlm.gaussian_field"
params = { rows = 120, columns = 120, range = 25, seed = 52 }
[[nodes]]
id = "farms"
op = "agents.parcels"
inputs = { parcels = "parcels.map", fertility = "fertility.map" }
[[nodes]]
id = "start"
op = "agents.update"
inputs = { agents = "farms.agents" }
params = { rules = "wealth = 20\nfarming = 1" }
[[nodes]]
id = "years"
op = "control.repeat"
inputs = { times = 20, farmers = "start.agents" }
[nodes.body]
carry = { farmers = "year.agents" }
collect = { summary = "totals.table", maps = "farmed.map" }
[[nodes.body.nodes]]
id = "year"
op = "agents.update"
inputs = { agents = "@farmers", step = "@step" }
params = { rules = "price = 10 - step * 0.25\nincome = fertility * price * 2 if farming == 1 else 1\nwealth = wealth + income - 7\nfarming = farming if wealth > 0 else 0" }
[[nodes.body.nodes]]
id = "totals"
op = "agents.summary"
inputs = { agents = "year.agents" }
[[nodes.body.nodes]]
id = "farmed"
op = "agents.map"
inputs = { agents = "year.agents", parcels = "parcels.map" }
params = { column = "farming" }
[[nodes]]
id = "wealth"
op = "agents.map"
inputs = { agents = "years.farmers", parcels = "parcels.map" }
params = { column = "wealth" }
[[plots]]
id = "plot_wealth"
title = "Wealth after 20 years"
layers = [{ from = "wealth.map" }]
[[plots]]
id = "plot_fertility"
title = "Fertility"
layers = [{ from = "fertility.map" }]
[[plots]]
id = "plot_farming"
title = "Parcels still farmed, year by year"
layers = [{ from = "years.maps" }]
From the command line, the series can be written as files by adding outputs to the model file:
[outputs.summary]
from = "years.summary" # summary.csv, a row per year
[outputs.farmers]
from = "years.farmers" # farmers.csv, a row per farmer after the last year
terraflux run farmers.toml --out farmers
The run takes a few hundredths of a second.
Read the results#
Year by year#




















For five years every farmer survives: even those on poor soil lose money, but they started with a wealth of 20. The first gives up in year 6, on the poorest parcel (mean fertility 0.20). From then on farmers leave steadily, and faster at the end, when the price has fallen so far that farms of middling fertility lose money every year: by year 20, 43 of 60 still farm. The mean wealth rises for ten years and then falls, because those who quit keep paying their cost of living while earning 1 elsewhere, and because the price falls for those who stay.
Who gave up#

The 17 farmers who gave up are exactly the 17 with the poorest soil (mean fertility 0.20 to 0.40). The rules contain nothing else that differs between farmers, so the pattern of abandonment is the pattern of soil: the map of parcels farmed in year 20 is a map of fertility cut at a threshold. That is a check that the model does what its rules say, and a warning: an agent-based model whose agents differ in one attribute only produces a threshold of that attribute. Real farmers differ in their age, debts, skills, family and alternatives, and models give agents several attributes, often drawn from surveys, so that farms of the same soil can make different choices (Valbuena et al., 2010).

How to report it#
An agent-based model must be described fully enough to be rebuilt: the agents, their attributes, the rules in order, the time step, what is drawn at random, and what is observed. The ODD protocol (Overview, Design concepts, Details) is the standard way to do it (Grimm et al., 2006, updated by Grimm et al., 2020). In Terraflux the model file itself holds the rules, so publish it with the paper. Report the outcomes as distributions over runs with different parameters, as below, not as one run.
Settings that change the answer#
The yearly cost of 7 is a guess, as most numbers in agent rules are. Move the slider to see the parcels still
farmed after 20 years with a cost of 5 to 9 (each a real run of the template with that number changed in the
rule wealth = wealth + income - 7):





Every unit of cost moves the break-even fertility up by \(1 / (2 p_t)\), about 0.1 at the end of the run, and the soils of this landscape are spread around the middle (half the parcels have a mean fertility between 0.39 and 0.63), so a small change in cost moves many farms across the line. The outcome is sensitive to a number nobody measured: such a model is a tool for exploring "what if" (what if costs rise by a third?), not for predicting how many farmers there will be. Run a range of values, as here, and report how the conclusions change across it.
Other settings worth exploring in the template: the starting wealth (a buffer that delays abandonment), the slope of the price, and the income of those who quit.
Common pitfalls#
Rules in the wrong order. Each line sees the lines above it. farming = ... before wealth = ... decides
on last year's wealth. Read the rules top to bottom as a sequence of steps, not as equations.
Reading the start instead of the carried value. Inside the body, read @farmers; reading
start.agents restarts from the first year at every step (see the warning above).
Parcels without an agent, agents without a parcel. agents.map paints only the parcels whose id an
agent has; other cells are missing. If parcel ids are not whole numbers (a mosaic's raw values), or if the map
of parcels is not the one the agents came from, the painted map is empty or patchy. Make ids whole numbers, as
the template's parcels node does, and paint on the same map.
Rules that keep running after an agent leaves. In the template, a farmer who quits still pays 7 and earns 1
every year, so its wealth falls without end (the red parcels above). That may be meant (debts) or not (a farmer
who sells the farm leaves the model). Make the intention explicit: wealth = if farming == 1 then wealth +
income - 7 else wealth keeps a quitter's wealth where it was.
Mean values of a mixed population. wealth_mean averages over all agents, farming or not. To follow the
farmers only, add an attribute (farm_wealth = wealth if farming == 1 else 0) and divide its sum by
farming_sum.
One run is one story. Rules with thresholds turn small differences into all-or-nothing outcomes. Explore
the parameters, and when rules draw random numbers, run replicates (terraflux submit --replicates).
Going further#
- CRAFTY makes agents compete for land: agent functional types that produce services from capitals and take over the cells where they are most competitive.
- A coupled model puts macro agents, parcel managers and CRAFTY in one loop, and shows how agents can freeze land in a CLUE-S model.
- Dynamic drivers, the first part, explains the loop's carry and collect in a land change model.
- The operators:
agents.parcels,agents.update,agents.summary,agents.map,agents.table.
References#
- An, L. (2012). Modeling human decisions in coupled human and natural systems: review of agent-based models. Ecological Modelling, 229, 25–36. https://doi.org/10.1016/j.ecolmodel.2011.07.010
- Grimm, V., Berger, U., Bastiansen, F., Eliassen, S., Ginot, V., Giske, J., et al. (2006). A standard protocol for describing individual-based and agent-based models. Ecological Modelling, 198(1–2), 115–126. https://doi.org/10.1016/j.ecolmodel.2006.04.023
- Grimm, V., Railsback, S. F., Vincenot, C. E., Berger, U., Gallagher, C., DeAngelis, D. L., et al. (2020). The ODD protocol for describing agent-based and other simulation models: a second update to improve clarity, replication, and structural realism. Journal of Artificial Societies and Social Simulation, 23(2), 7. https://doi.org/10.18564/jasss.4259
- Matthews, R. B., Gilbert, N. G., Roach, A., Polhill, J. G., & Gotts, N. M. (2007). Agent-based land-use models: a review of applications. Landscape Ecology, 22(10), 1447–1459. https://doi.org/10.1007/s10980-007-9135-1
- Parker, D. C., Manson, S. M., Janssen, M. A., Hoffmann, M. J., & Deadman, P. (2003). Multi-agent systems for the simulation of land-use and land-cover change: a review. Annals of the Association of American Geographers, 93(2), 314–337. https://doi.org/10.1111/1467-8306.9302004
- Rounsevell, M. D. A., Robinson, D. T., & Murray-Rust, D. (2012). From actors to agents in socio-ecological systems models. Philosophical Transactions of the Royal Society B, 367(1586), 259–269. https://doi.org/10.1098/rstb.2011.0187
- Valbuena, D., Verburg, P. H., Bregt, A. K., & Ligtenberg, A. (2010). An agent-based approach to model land-use change at a regional scale. Landscape Ecology, 25(2), 185–199. https://doi.org/10.1007/s10980-009-9380-6