LogoActions: GitHub Actions for the NetLogo Community 🚀

Author

Daniel Vartanian

Published

January 11, 2026

Overview

This document presents the output of a GitHub Action workflow that sets up NetLogo and runs experiments using Quarto and the logolink R package.

See the repository README for more details.

Set the Environment

Load Packages

library(logolink) # github.com/danielvartan/logolink

library(dplyr)
library(ggplot2)
library(magrittr)

Set ggplot2 Theme

theme_set(
  theme_bw() +
    theme(
      panel.grid.major = element_blank(),
      panel.grid.minor = element_blank(),
      legend.frame = element_blank(),
      legend.ticks = element_line(color = "white")
    )
)

Wolf Sheep Predation Model

Set Model Path

model_path <-
  find_netlogo_home() |>
  file.path(
    "models",
    "IABM Textbook",
    "chapter 4",
    "Wolf Sheep Simple 5.nlogox"
  )

Create Experiment

setup_file <- create_experiment(
  name = "Wolf Sheep Simple Model Analysis",
  repetitions = 10,
  sequential_run_order = TRUE,
  run_metrics_every_step = TRUE,
  time_limit = 1000,
  setup = 'setup',
  go = 'go',
  metrics = c(
    'count wolves',
    'count sheep'
  ),
  constants = list(
    "number-of-sheep" = 500,
    "number-of-wolves" = list(
      first = 5,
      step = 1,
      last = 15
    ),
    "movement-cost" = 0.5,
    "grass-regrowth-rate" = 0.3,
    "energy-gain-from-grass" = 2,
    "energy-gain-from-sheep" = 5
  )
)

Run Experiment

results <-
  model_path |>
  run_experiment(setup_file = setup_file)
results |> glimpse()
List of 2
 $ metadata:List of 6
  ..$ timestamp       : POSIXct[1:1], format: "2026-01-11 21:17:22"
  ..$ netlogo_version : chr "7.0.3"
  ..$ output_version  : chr "2.0"
  ..$ model_file      : chr "Wolf Sheep Simple 5.nlogox"
  ..$ experiment_name : chr "Wolf Sheep Simple Model Analysis"
  ..$ world_dimensions: Named int [1:4] -17 17 -17 17
  .. ..- attr(*, "names")= chr [1:4] "min-pxcor" "max-pxcor" "min-pycor" "max-pycor"
 $ table   : tibble [110,110 × 10] (S3: tbl_df/tbl/data.frame)
  ..$ run_number            : num [1:110110] 1 1 1 1 1 1 1 1 1 1 ...
  ..$ number_of_sheep       : num [1:110110] 500 500 500 500 500 500 500 500 500 500 ...
  ..$ number_of_wolves      : num [1:110110] 5 5 5 5 5 5 5 5 5 5 ...
  ..$ movement_cost         : num [1:110110] 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 ...
  ..$ grass_regrowth_rate   : num [1:110110] 0.3 0.3 0.3 0.3 0.3 0.3 0.3 0.3 0.3 0.3 ...
  ..$ energy_gain_from_grass: num [1:110110] 2 2 2 2 2 2 2 2 2 2 ...
  ..$ energy_gain_from_sheep: num [1:110110] 5 5 5 5 5 5 5 5 5 5 ...
  ..$ step                  : num [1:110110] 0 1 2 3 4 5 6 7 8 9 ...
  ..$ count_wolves          : num [1:110110] 5 5 5 5 5 5 5 5 5 5 ...
  ..$ count_sheep           : num [1:110110] 500 498 497 495 493 491 491 490 489 487 ...
results
$metadata
$metadata$timestamp
[1] "2026-01-11 21:17:22 GMT"

$metadata$netlogo_version
[1] "7.0.3"

$metadata$output_version
[1] "2.0"

$metadata$model_file
[1] "Wolf Sheep Simple 5.nlogox"

$metadata$experiment_name
[1] "Wolf Sheep Simple Model Analysis"

$metadata$world_dimensions
min-pxcor max-pxcor min-pycor max-pycor 
      -17        17       -17        17 


$table
# A tibble: 110,110 × 10
   run_number number_of_sheep number_of_wolves movement_cost grass_regrowth_rate
        <dbl>           <dbl>            <dbl>         <dbl>               <dbl>
 1          1             500                5           0.5                 0.3
 2          1             500                5           0.5                 0.3
 3          1             500                5           0.5                 0.3
 4          1             500                5           0.5                 0.3
 5          1             500                5           0.5                 0.3
 6          1             500                5           0.5                 0.3
 7          1             500                5           0.5                 0.3
 8          1             500                5           0.5                 0.3
 9          1             500                5           0.5                 0.3
10          1             500                5           0.5                 0.3
# ℹ 110,100 more rows
# ℹ 5 more variables: energy_gain_from_grass <dbl>,
#   energy_gain_from_sheep <dbl>, step <dbl>, count_wolves <dbl>,
#   count_sheep <dbl>

Plot Results

plot_data <-
  results |>
  extract2("table") |>
  summarize(
    across(everything(), ~ mean(.x, na.rm = TRUE)),
    .by = c(step, number_of_wolves)
  ) |>
  arrange(number_of_wolves, step)
plot_data |>
  mutate(
    number_of_wolves = as.factor(number_of_wolves)
  ) |>
  ggplot(
    aes(
      x = step,
      y = count_sheep,
      group = number_of_wolves,
      color = number_of_wolves
    )
  ) +
  geom_line() +
  labs(
    x = "Time Step",
    y = "Average Number of Sheep",
    color = "Wolves"
  )

Spread of Disease Model

Set Model Path

model_path <-
  find_netlogo_home() |>
  file.path(
    "models",
    "IABM Textbook",
    "chapter 6",
    "Spread of Disease.nlogox"
  )

Create Experiment

setup_file <- create_experiment(
  name = "Population Density (Runtime)",
  repetitions = 10,
  sequential_run_order = TRUE,
  run_metrics_every_step = TRUE,
  time_limit = 1000,
  setup = 'setup',
  go = 'go',
  metrics = c(
    'count turtles with [infected?]'
  ),
  constants = list(
    "variant" = "mobile",
    "num-people" = list(
      first = 50,
      step = 50,
      last = 200
    ),
    "connections-per-node" = 4.1,
    "num-infected" = 1,
    "disease-decay" = 0
  )
)

Run Experiment

results <-
  model_path |>
  run_experiment(setup_file = setup_file)
results |> glimpse()
List of 2
 $ metadata:List of 6
  ..$ timestamp       : POSIXct[1:1], format: "2026-01-11 21:17:46"
  ..$ netlogo_version : chr "7.0.3"
  ..$ output_version  : chr "2.0"
  ..$ model_file      : chr "Spread of Disease.nlogox"
  ..$ experiment_name : chr "Population Density (Runtime)"
  ..$ world_dimensions: Named int [1:4] -20 20 -20 20
  .. ..- attr(*, "names")= chr [1:4] "min-pxcor" "max-pxcor" "min-pycor" "max-pycor"
 $ table   : tibble [8,465 × 8] (S3: tbl_df/tbl/data.frame)
  ..$ run_number                 : num [1:8465] 1 1 1 1 1 1 1 1 1 1 ...
  ..$ variant                    : chr [1:8465] "mobile" "mobile" "mobile" "mobile" ...
  ..$ num_people                 : num [1:8465] 50 50 50 50 50 50 50 50 50 50 ...
  ..$ connections_per_node       : num [1:8465] 4.1 4.1 4.1 4.1 4.1 4.1 4.1 4.1 4.1 4.1 ...
  ..$ num_infected               : num [1:8465] 1 1 1 1 1 1 1 1 1 1 ...
  ..$ disease_decay              : num [1:8465] 0 0 0 0 0 0 0 0 0 0 ...
  ..$ step                       : num [1:8465] 0 1 2 3 4 5 6 7 8 9 ...
  ..$ count_turtles_with_infected: num [1:8465] 1 1 1 1 1 1 1 1 1 1 ...
results
$metadata
$metadata$timestamp
[1] "2026-01-11 21:17:46 GMT"

$metadata$netlogo_version
[1] "7.0.3"

$metadata$output_version
[1] "2.0"

$metadata$model_file
[1] "Spread of Disease.nlogox"

$metadata$experiment_name
[1] "Population Density (Runtime)"

$metadata$world_dimensions
min-pxcor max-pxcor min-pycor max-pycor 
      -20        20       -20        20 


$table
# A tibble: 8,465 × 8
   run_number variant num_people connections_per_node num_infected disease_decay
        <dbl> <chr>        <dbl>                <dbl>        <dbl>         <dbl>
 1          1 mobile          50                  4.1            1             0
 2          1 mobile          50                  4.1            1             0
 3          1 mobile          50                  4.1            1             0
 4          1 mobile          50                  4.1            1             0
 5          1 mobile          50                  4.1            1             0
 6          1 mobile          50                  4.1            1             0
 7          1 mobile          50                  4.1            1             0
 8          1 mobile          50                  4.1            1             0
 9          1 mobile          50                  4.1            1             0
10          1 mobile          50                  4.1            1             0
# ℹ 8,455 more rows
# ℹ 2 more variables: step <dbl>, count_turtles_with_infected <dbl>

Tidy Data

data <-
  results |>
  extract2("table") |>
  rename(infected = count_turtles_with_infected) |>
  mutate(
    variant = as.factor(variant),
    frac_infected = infected / num_people
  ) |>
  arrange(run_number, num_infected, step)
data |> glimpse()
Rows: 8,465
Columns: 9
$ run_number           <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
$ variant              <fct> mobile, mobile, mobile, mobile, mobile, mobile, m…
$ num_people           <dbl> 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 5…
$ connections_per_node <dbl> 4.1, 4.1, 4.1, 4.1, 4.1, 4.1, 4.1, 4.1, 4.1, 4.1,…
$ num_infected         <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
$ disease_decay        <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ step                 <dbl> 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,…
$ infected             <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
$ frac_infected        <dbl> 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0…
data

Plot Data

data |>
  mutate(
    num_people = as.factor(num_people)
  ) |>
  ggplot(
    aes(
      x = step,
      y = frac_infected,
      color = num_people
    )
  ) +
  geom_point(
    aes(shape = num_people),
    size = 1,
    alpha = 0.75
  ) +
  scale_x_continuous(breaks = seq(0, max(data$step), 100)) +
  labs(
    x = "Steps",
    y = "Fraction of Infected",
    title = "Infected Over Time",
    color = "People",
    shape = "People"
  )

plot <-
  data |>
  ggplot(
    aes(
      x = step,
      y = frac_infected, # infected
      color = as.factor(num_people)
    )
  ) +
  scale_x_continuous(
    breaks = seq(0, max(data$step), 100)
  ) +
  labs(
    x = "Steps",
    y = "Fraction of Infected",
    title = "Infected Over Time",
    color = "People"
  )

data_i <-
  data |>
  group_by(num_people, run_number) |>
  group_split()

for (i in data_i) {
  plot <-
    plot +
    geom_line(
      data = i,
      aes(x = step, y = frac_infected),
      linewidth = 1,
      alpha = 0.75
    )
}

plot

Summarize Data

summarized_data <-
  data |>
  select(num_people, step, infected, frac_infected) |>
  summarize(
    mean = mean(frac_infected, na.rm = TRUE),
    sd = sd(frac_infected, na.rm = TRUE),
    .by = c(num_people, step)
  ) |>
  arrange(num_people, step)
summarized_data |> glimpse()
Rows: 1,080
Columns: 4
$ num_people <dbl> 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50,…
$ step       <dbl> 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 1…
$ mean       <dbl> 0.020, 0.022, 0.022, 0.026, 0.026, 0.026, 0.026, 0.026, 0.0…
$ sd         <dbl> 0.000000000, 0.006324555, 0.006324555, 0.009660918, 0.00966…
summarized_data

Plot Summarized Data with SD Error Bars

summarized_data |>
  filter(step %% 10 == 0) |>
  ggplot(
    aes(
      x = step,
      y = mean,
      color = as.factor(num_people)
    )
  ) +
  geom_point(
    aes(shape = as.factor(num_people)),
    size = 1,
    alpha = 0.75
  ) +
  geom_errorbar(
    aes(
      ymin = mean + sd,
      ymax = mean - sd
    ),
    width = 5
  ) +
  geom_line() +
  scale_x_continuous(
    breaks = seq(0, max(summarized_data$step), 100)
  ) +
  labs(
    title = "Infected Over Time",
    x = "Steps",
    y = "Fraction of Infected",
    color = "People",
    shape = "People"
  )

Plot Summarized Data with SE Error Bars

summarized_data |>
  filter(step %% 10 == 0) |>
  mutate(se = sd / sqrt(10)) |>
  ggplot(aes(
    x = step,
    y = mean,
    color = as.factor(num_people)
  )) +
  geom_point(
    aes(shape = as.factor(num_people)),
    size = 1,
    alpha = 0.75
  ) +
  geom_errorbar(
    aes(
      ymin = mean + se,
      ymax = mean - se
    ),
    width = 5
  ) +
  geom_line() +
  scale_x_continuous(
    breaks = seq(0, max(summarized_data$step), 100)
  ) +
  labs(
    title = "Infected Over Time",
    x = "Steps",
    y = "Fraction of Infected",
    color = "People",
    shape = "People"
  )