Sugarscape

Growing Artificial Societies

(Descriptions below are from this page)


"Growing Artificial Societies" (Epstein & Axtell 1996) is a reference book for scientists interested in agent-based modelling and computer simulation. It represents one of the most paradigmatic and fascinating examples of the so-called generative approach to social science (Epstein 1999). In their book, Epstein & Axtell (1996) present a computational model where a heterogeneous population of autonomous agents compete for renewable resources that are unequally distributed over a 2-dimensional environment. Agents in the model are autonomous in that they are not governed by any central authority and they are heterogeneous in that they differ in their genetic attributes and their initial environmental endowments (e.g. their initial location and wealth). The model grows in complexity through the different chapters of the book as the agents are given the ability to engage in new activities such as sex, cultural exchange, trade, combat, disease transmission, etc. The core of Sugarscape has provided the basis for various extensions to study e.g. norm formation through cultural diffusion (Flentge et al. 2001) and the emergence of communication and cooperation in artificial societies (Buzing et al. 2005). Here we analyse the model described in the second chapter of Epstein & Axtell's (1996) book within the Markov chain framework.

Rules of sugarscape

The first model that Epstein & Axtell (1996) present comprises a finite population of agents who live in an environment. The environment is represented by a two-dimensional grid which contains sugar in some of its cells, hence the name Sugarscape. Agents' role in this first model consists in wandering around the Sugarscape harvesting the greatest amount of sugar they can find.

Environment

The environment is a 50×50 grid that wraps around forming a torus. Grid positions have both a sugar level and a sugar capacity c. A cell's sugar level is the number of units of sugar in the cell (potentially none), and its sugar capacity c is the maximum value the sugar level can take on that cell. Sugar capacity is fixed for each individual cell and may be different for different cells. The spatial distribution of sugar capacities depicts a sugar topography consisting of two peaks (with sugar capacity c = 4) separated by a valley, and surrounded by a desert region of sugarless cells (see Figure 1). Note, however, that the grid wraps around in both directions.

The Sugarscape obbeys the following rule:

Sugarscape growback rule G$\alpha$: At each position, sugar grows back at a rate of $\alpha$ units per time-step up to the cell's capacity c.

Agents

Every agent is endowed with individual (life-long) characteristics that condition her skills and capacities to survive in the Sugarscape. These individual attributes are:

  • A vision v, which is the maximum number of positions the agent can see in each of

the four principal lattice directions: north, south, east and west.

  • A metabolic rate m, which represents the units of sugar the agent burns per time-step.
  • A maximum age max-age, which is the maximum number of time-steps the agent can live.

Agents also have the capacity to accumulate sugar wealth w. An agent's sugar wealth is incremented at the end of each time-step by the sugar collected and decremented by the agent's metabolic rate. Two agents are not allowed to occupy the same position in the grid.

The agents' behaviour is determined by the following two rules:

Agent movement rule M:

Consider the set of unoccupied positions within your vision (including the one you are standing on), identify the one(s) with the greatest amount of sugar, select the nearest one (Randomly() if there is more than one), move there and collect all the sugar in it. At this point, the agent's accumulated sugar wealth is incremented by the sugar collected and decremented by the agent's metabolic rate m. If at this moment the agent's sugar wealth is not greater than zero, then the agent dies.

Agent replacement rule R:

Whenever an agent dies it is replaced by a new agent of age 0 placed on a Randomly() chosen unoccupied position, having random attributes v, m and max-age, and random initial wealth w0. All random numbers are drawn from uniform distributions with ranges specified in Table 1 below.

Scheduling of events

Scheduling is determined by the order in which the different rules G, M and R are fired in the model. Environmental rule G comes first, followed by agent rule M (which is executed by all agents in random order) and finally agent rule R is executed (again, by all agents in random order).

Parameterisation

Our analysis corresponds to a model used by Epstein & Axtell (1996, pg. 33) to study the emergent wealth distribution in the agent population. This model is parameterised as indicated in Table 1 below (where U[a,b] denotes a uniform distribution with range [a,b]).

Initially, each position of the Sugarscape contains a sugar level equal to its sugar capacity c, and the 250 agents are created at a random unoccupied initial location and with random attributes (using the uniform distributions indicated in Table 1).

Table 1

ParameterValue
Lattice length L50
Number of sugar peaks2
Growth rate $\alpha$1
Number of agents N250
Agents' initial wealth w0 distributionU[5,25]
Agents' metabolic rate m distributionU[1,4]
Agents' vision v distributionU[1,6]
Agents' maximum age max-age distributionU[60,100]

Creating the ABM

using Agents, Random

@agent struct SugarSeeker(GridAgent{2})
    vision::Int
    metabolic_rate::Int
    age::Int
    max_age::Int
    wealth::Int
end

Functions distances and sugar_caps produce a matrix for the distribution of sugar capacities.

function distances(pos, sugar_peaks)
    all_dists = zeros(Int, length(sugar_peaks))
    for (ind, peak) in enumerate(sugar_peaks)
        d = round(Int, sqrt(sum((pos .- peak) .^ 2)))
        all_dists[ind] = d
    end
    return minimum(all_dists)
end

function sugar_caps(dims, sugar_peaks, max_sugar, dia = 4)
    sugar_capacities = zeros(Int, dims)
    for i in 1:dims[1], j in 1:dims[2]
        sugar_capacities[i, j] = distances((i, j), sugar_peaks)
    end
    for i in 1:dims[1]
        for j in 1:dims[2]
            sugar_capacities[i, j] = max(0, max_sugar - (sugar_capacities[i, j] ÷ dia))
        end
    end
    return sugar_capacities
end

"Create a sugarscape ABM"
function sugarscape(;
    dims = (50, 50),
    sugar_peaks = ((10, 40), (40, 10)),
    growth_rate = 1,
    N = 250,
    w0_dist = (5, 25),
    metabolic_rate_dist = (1, 4),
    vision_dist = (1, 6),
    max_age_dist = (60, 100),
    max_sugar = 4,
    seed = 42
)
    sugar_capacities = sugar_caps(dims, sugar_peaks, max_sugar, 6)
    sugar_values = deepcopy(sugar_capacities)
    space = GridSpaceSingle(dims)
    properties = Dict(
        :growth_rate => growth_rate,
        :N => N,
        :w0_dist => w0_dist,
        :metabolic_rate_dist => metabolic_rate_dist,
        :vision_dist => vision_dist,
        :max_age_dist => max_age_dist,
        :sugar_values => sugar_values,
        :sugar_capacities => sugar_capacities,
    )
    model = StandardABM(
        SugarSeeker,
        space;
        agent_step!,
        model_step!,
        scheduler = Schedulers.Randomly(),
        properties = properties,
        rng = MersenneTwister(seed)
    )
    for _ in 1:N
        add_agent_single!(
            model,
            rand(abmrng(model), vision_dist[1]:vision_dist[2]),
            rand(abmrng(model), metabolic_rate_dist[1]:metabolic_rate_dist[2]),
            0,
            rand(abmrng(model), max_age_dist[1]:max_age_dist[2]),
            rand(abmrng(model), w0_dist[1]:w0_dist[2]),
        )
    end
    return model
end
Main.sugarscape

Defining stepping functions

Now we define the stepping functions that handle the time evolution of the model. The model stepping function controls the sugar growth:

function model_step!(model)
    # At each position, sugar grows back at a rate of α units
    # per time-step up to the cell's capacity c.
    @inbounds for pos in positions(model)
        if model.sugar_values[pos...] < model.sugar_capacities[pos...]
            model.sugar_values[pos...] += model.growth_rate
        end
    end
    return
end
model_step! (generic function with 1 method)

The agent stepping function contains the dynamics of the model:

function agent_step!(agent, model)
    move_and_collect!(agent, model)
    replacement!(agent, model)
end

function move_and_collect!(agent, model)
    # Go through all unoccupied positions within vision, and consider the empty ones.
    # From those, identify the one with greatest amount of sugar, and go there!
    max_sugar_pos = agent.pos
    max_sugar = model.sugar_values[max_sugar_pos...]
    for pos in nearby_positions(agent, model, agent.vision)
        isempty(pos, model) || continue
        sugar = model.sugar_values[pos...]
        if sugar > max_sugar
            max_sugar = sugar
            max_sugar_pos = pos
        end
    end
    # Move to the max sugar position (which could be where we are already)
    move_agent!(agent, max_sugar_pos, model)
    # Collect the sugar there and update wealth (collected - consumed)
    agent.wealth += (model.sugar_values[max_sugar_pos...] - agent.metabolic_rate)
    model.sugar_values[max_sugar_pos...] = 0
    # age
    agent.age += 1
    return
end

function replacement!(agent, model)
    # If the agent's sugar wealth become zero or less, it dies
    if agent.wealth ≤ 0 || agent.age ≥ agent.max_age
        remove_agent!(agent, model)
        # Whenever an agent dies, a young one is added to a random empty position
        add_agent_single!(
            model,
            rand(abmrng(model), model.vision_dist[1]:model.vision_dist[2]),
            rand(abmrng(model), model.metabolic_rate_dist[1]:model.metabolic_rate_dist[2]),
            0,
            rand(abmrng(model), model.max_age_dist[1]:model.max_age_dist[2]),
            rand(abmrng(model), model.w0_dist[1]:model.w0_dist[2]),
        )
    end
end

model = sugarscape()
StandardABM with 250 agents of type SugarSeeker
 agents container: Dict
 space: GridSpaceSingle with size (50, 50), metric=chebyshev, periodic=true
 scheduler: Agents.Schedulers.Randomly
 properties: growth_rate, N, max_age_dist, metabolic_rate_dist, vision_dist, sugar_values, sugar_capacities, w0_dist

Let's plot the spatial distribution of sugar capacities in the Sugarscape.

using CairoMakie

fig = Figure(size = (600, 600))
ax, hm = heatmap(fig[1,1], model.sugar_capacities; colormap=:thermal)
Colorbar(fig[1, 2], hm, width = 20)
fig
Example block output

Plotting & Animating

We can plot the ABM and the sugar distribution side by side using abmplot and standard Makie.jl commands like lifting the model observable. (we could plot the sugar distribution as a heatmap, but we choose this composite plot for more variaty in the example pool)

model = sugarscape()
fig, ax, abmp = abmplot(model; add_controls = false, figkwargs = (size = (800, 600)))
# Lift model observable for heatmap
sugar = @lift($(abmp.model).sugar_values)
axhm, hm = heatmap(fig[1,2], sugar; colormap=:thermal, colorrange=(0,4))
axhm.aspect = AxisAspect(1) # equal aspect ratio for heatmap
Colorbar(fig[1, 3], hm, width = 15, tellheight=false)
rowsize!(fig.layout, 1, axhm.scene.px_area[].widths[2]) # Colorbar height = axis height
fig
Example block output

Animating this is now trivial. We simply step the model observable in abmp. We'll also add a title that counts the step number

s = Observable(0) # counter of current step, also observable
t = @lift("Sugarscape, step = $($(s))")
connect!(ax.title, t)
ax.titlealign = :left
fig
Example block output

We animate the evolution of both the ABM and the sugar distribution using the following simple loop involving the abmstepper

record(fig, "sugarvis.mp4", 0:100; framerate = 3) do j
    # This updates the abm plot and lifted heatmap
    Agents.step!(abmp, 1)
    # This updates the title counter
    s[] = s[] + 1
end
"sugarvis.mp4"

Distribution of wealth across individuals

First we produce some data that include the wealth

model2 = sugarscape()
adata, _ = run!(model2, 100, adata = [:wealth])
adata[1:10,:]
10×3 DataFrame
Rowtimeidwealth
Int64Int64Int64
1019
20225
30314
40423
50513
60615
70715
80819
9099
1001017

And now we animate the evolution of the distribution of wealth

figure = Figure(size = (600, 600))
step_number = Observable(0)
title_text = @lift("Wealth distribution of individuals, step = $($step_number)")
Label(figure[1, 1], title_text; fontsize=20, tellwidth=false)
ax = Axis(figure[2, 1]; xlabel="Wealth", ylabel="Number of agents")
histdata = Observable(adata[adata.time .== 20, :wealth])
hist!(ax, histdata; bar_position=:time)
ylims!(ax, (0, 50))
record(figure, "sugarhist.mp4", 0:50; framerate=3) do i
    histdata[] = adata[adata.time .== i, :wealth]
    step_number[] = i
    xlims!(ax, (0, max(histdata[]...)))
end
"sugarhist.mp4"

We see that the distribution of wealth shifts from a more or less uniform distribution to a skewed distribution resembling a power-law.

References

BUZING P, Eiben A & Schut M (2005) Emerging communication and cooperation in evolving agent societies. Journal of Artificial Societies and Social Simulation 8(1)2. http://jasss.soc.surrey.ac.uk/8/1/2.html.

EPSTEIN J M (1999) Agent-Based Computational Models And Generative Social Science. Complexity 4(5), pp. 41-60.

EPSTEIN J M & Axtell R L (1996) Growing Artificial Societies: Social Science from the Bottom Up. The MIT Press.

FLENTGE F, Polani D & Uthmann T (2001) Modelling the emergence of possession norms using memes. Journal of Artificial Societies and Social Simulation 4(4)3. http://jasss.soc.surrey.ac.uk/4/4/3.html.