# Find project root and include ensure_packages.jl
project_root = let
current = pwd()
while !isfile(joinpath(current, "Project.toml")) && !isfile(joinpath(current, "_quarto.yml"))
parent = dirname(current)
parent == current && break
current = parent
end
current
end
include(joinpath(dirname(Base.active_project()), "scripts", "book_bootstrap.jl"))
# Agents.jl 7: agent types via @agent; models use StandardABM; RNG lives on the model
using Agents
using Agents: run!, dummystep, abmrng, abmspace
using Agents.Schedulers
@auto_using Graphs CairoMakie StableRNGs DataFrames
# Activate SVG output for responsive figures
# Set seed for reproducibility
rng = StableRNG(1234)
@agent struct Person(GraphAgent)
state::Int
behavior::Symbol
end
# Create network (using Graphs.jl)
g = erdos_renyi(50, 0.1, seed = 42) # Random network with 50 agents, 10% connection probability
# GraphSpace(graph) — one position per graph vertex (Agents.jl 7)
space = GraphSpace(g)
function agent_step!(agent, model)
rng = abmrng(model)
graph = abmspace(model).graph
node_id = agent.pos
neighbor_nodes = Graphs.neighbors(graph, node_id)
# One agent per node with agent id equal to node index (book convention)
neighbors_agents = [model[n] for n in neighbor_nodes if hasid(model, n)]
infected_neighbors = [n for n in neighbors_agents if n.state == 1]
if !isempty(infected_neighbors) && agent.behavior == :normal
if rand(rng) < 0.3
agent.behavior = :distancing
end
end
if agent.state == 0
for neighbor in neighbors_agents
if neighbor.state == 1 && agent.behavior == :normal
if rand(rng) < 0.1
agent.state = 1
break
end
end
end
elseif agent.state == 1
if rand(rng) < 0.05
agent.state = 2
end
end
end
function model_step!(model)
end
model = StandardABM(Person, space;
scheduler = Schedulers.Randomly(),
rng = rng,
agent_step! = agent_step!,
model_step! = model_step!)
for i in 1:50
add_agent!(i, model, 0, :normal)
end
for i in 1:5
model[i].state = 1
end
data, _ = run!(model, 100;
adata = [:state, :behavior],
showprogress = false)
# Aggregate by time step for visualization (`run!` uses column `:time`)
data_agg = combine(groupby(data, :time),
:state => (x -> sum(x .== 0)) => :susceptible,
:state => (x -> sum(x .== 1)) => :infected,
:state => (x -> sum(x .== 2)) => :recovered,
:behavior => (x -> sum(x .== :normal)) => :normal,
:behavior => (x -> sum(x .== :distancing)) => :distancing
)| Row | time | susceptible | infected | recovered | normal | distancing |
|---|---|---|---|---|---|---|
| Int64 | Int64 | Int64 | Int64 | Int64 | Int64 | |
| 1 | 0 | 45 | 5 | 0 | 50 | 0 |
| 2 | 1 | 44 | 6 | 0 | 45 | 5 |
| 3 | 2 | 43 | 7 | 0 | 42 | 8 |
| 4 | 3 | 42 | 8 | 0 | 36 | 14 |
| 5 | 4 | 40 | 9 | 1 | 32 | 18 |
| 6 | 5 | 39 | 8 | 3 | 27 | 23 |
| 7 | 6 | 39 | 8 | 3 | 24 | 26 |
| 8 | 7 | 39 | 7 | 4 | 23 | 27 |
| 9 | 8 | 39 | 7 | 4 | 22 | 28 |
| 10 | 9 | 38 | 6 | 6 | 22 | 28 |
| 11 | 10 | 38 | 6 | 6 | 19 | 31 |
| 12 | 11 | 38 | 6 | 6 | 19 | 31 |
| 13 | 12 | 38 | 5 | 7 | 17 | 33 |
| ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ |
| 90 | 89 | 38 | 0 | 12 | 17 | 33 |
| 91 | 90 | 38 | 0 | 12 | 17 | 33 |
| 92 | 91 | 38 | 0 | 12 | 17 | 33 |
| 93 | 92 | 38 | 0 | 12 | 17 | 33 |
| 94 | 93 | 38 | 0 | 12 | 17 | 33 |
| 95 | 94 | 38 | 0 | 12 | 17 | 33 |
| 96 | 95 | 38 | 0 | 12 | 17 | 33 |
| 97 | 96 | 38 | 0 | 12 | 17 | 33 |
| 98 | 97 | 38 | 0 | 12 | 17 | 33 |
| 99 | 98 | 38 | 0 | 12 | 17 | 33 |
| 100 | 99 | 38 | 0 | 12 | 17 | 33 |
| 101 | 100 | 38 | 0 | 12 | 17 | 33 |
Basic multiagent network: agents interacting on a network structure