Appendix B — Near Equality Tests

This document provides unit tests for the LogoClim NetLogo model. The tests perform near-equality comparisons to validate tolerance expectations during the processing of WorldClim data in NetLogo.

This pipeline requires access to WorldClim and GADM data. Tests may fail if their servers are unavailable.

B.1 Problem

LogoClim integrates WorldClim data into NetLogo models through a multi-step conversion process. WorldClim provides raster data in GeoTIFF format, a high-precision binary format for geospatial data, but NetLogo’s GIS extension requires data in Esri ASCII format, a text-based format that stores values with a limited decimal precision. This conversion can introduce numerical differences through two main sources:

  1. Decimal precision loss: Esri ASCII files store values as text with a fixed number of decimal places, rounding continuous values from the original GeoTIFF.
  2. Spatial interpolation: When NetLogo’s patch grid doesn’t perfectly align with the raster resolution, the GIS extension applies interpolation that may alter cell values.

These discrepancies propagate to summary statistics when comparing the original WorldClim GeoTIFF data with values extracted from LogoClim patches. The unit tests in this document verify that such differences remain within acceptable tolerance levels, ensuring the reliability of downstream simulations and analyses.

B.2 Methods

B.2.1 Source of Data

Data used in this report come from the following sources:

B.2.2 Data Munging

The data munging followed the data science workflow outlined by Wickham et al. (2023), as illustrated in Figure B.1. All processes were made using the Quarto publishing system (Allaire et al., n.d.), the NetLogo environment, the R programming language (R Core Team, n.d.), and several R packages.

For data manipulation and workflow, priority was given to packages from the Tidyverse, rOpenSci and rspatial ecosystems, as well as other packages adhering to the tidy tools manifesto (Wickham, 2023).

Source: Reproduced from Wickham et al. (2023).

Figure B.1: Data science workflow created by Wickham, Çetinkaya-Runde, and Grolemund.

B.2.3 Data Extraction and Transformation

Data extraction was performed using the worldclim_download() function from the orbis R package (Vartanian, 2026c). This function scrapes climate data from the WorldClim website and downloads the relevant GeoTIFF files for the specified variables and time periods.

Following the extraction, the transformation from GeoTIFF to Esri ASCII format is carried out using the worldclim_to_ascii() function from the orbis R package.

B.2.4 NetLogo Integration

Integration with NetLogo (Wilensky, 1999) is facilitated by the logolink R package (Vartanian, 2026b). This package enables the execution of BehaviorSpace experiments directly from R.

Output is extracted in Table and Lists format, containing values, latitude, and longitude for patches, along with global variables that describe the model’s settings.

No Java dependencies are required. NetLogo bundles its own Java Runtime Environment (JRE), ensuring independent operation regardless of the system’s Java installation.

B.2.5 Continuous Integration

The tests use the latest release of NetLogo and are automated using GitHub Actions provided by the LogoActions project (Vartanian, 2026a). Each commit to the code repository triggers test execution, ensuring that changes to the codebase are validated against defined tolerance levels.

B.2.6 Near Equality Tests

The data validation is performed using error tolerance tests with expectations functions from the testthat R package (Wickham, 2011). These tests compare the number of observations (n), minimum, mean, and maximum value of the WorldClim data loaded in LogoClim against the original WorldClim dataset.

Each test begins by selecting a random country to crop the WorldClim data. For each of the three WorldClim series, a random combination of variable, month, year, and other series-specific parameters is drawn using the worldclim_random() function from the orbis R package. No seed is set for the random number generator, so results vary between runs.

Elevation, bioclimatic variables, and the models FIO-ESM-2-0, GFDL-ESM4, and HadGEM3-GC31-LL are excluded due to their data limitations.

B.2.6.1 Minimum Number of Cells

To ensure valid statistical analysis, a minimum number of cells is required. Resolution for each country was determined based on its area, aiming to achieve approximately 1,000 cells. This calculation considers the available resolutions and their approximate cell areas at the Equator:

  • 10 minutes (~340 km² per cell)
  • 5 minutes (~85 km² per cell)
  • 2.5 minutes (~21 km² per cell)
  • 30 seconds (~1 km² per cell)

Micronations, like Dominica, were excluded from the analysis due to their small size and limited data availability.

B.2.6.2 Tolerance Level

The all.equal and testthat expect_equal functions were used to perform near-equality tests, both relying on relative tolerance. Comparisons are conducted between the original GeoTIFF file and patch values extracted directly from the LogoClim model.

Relative tolerance is proportional to the value of the quantity being measured. The principle is that larger values can tolerate larger errors.

Given \(x\) and \(y\), relative tolerance can be expressed as:

\[ |x - y| \leq \text{tolerance} \times \max(|x|, |y|) \]

or

\[ \frac{|x - y|}{\max(|x|, |y|)} \leq \text{tolerance} \]

where:

  • \(x\) and \(y\) are the values being compared
  • \(\text{tolerance}\) is the relative tolerance level
  • \(\max(|x|, |y|)\) is the maximum absolute value of \(x\) and \(y\)

For this analysis, the following tolerance level is used:

tolerance <- 0.05

This means the absolute difference between the numbers can be up to 5% of the larger number’s magnitude. In other words, \(x\) and \(y\) are considered nearly equal if:

\[ \frac{|x - y|}{\max(|x|, |y|)} \leq 0.05 \]

This tolerance may appear high, but it accounts for the maximum cumulative effects of multiple sources of numerical differences, including edge cases encountered during random testing. In practice, observed differences are expected to be substantially smaller than this threshold, reflecting a high degree of agreement between the datasets.

B.2.7 Code Style

The Tidyverse Tidy Tools Manifesto (Wickham, 2023), code style guide (Wickham, n.d.-a) and design principles (Wickham, n.d.-b) were followed to ensure consistency and enhance readability.

B.2.8 Reproducibility

The pipeline is fully reproducible and can be run again at any time. To ensure consistent results, the renv package (Ushey & Wickham, 2025) was used to manage and restore the R environment. See the README file in the code repository to learn how to run it.

B.3 Set Environment

B.3.1 Load Packages

B.3.2 Load Custom Functions

The source code for the functions below can be found in the R directory of the code repository.

here("R", "worldclim_raster.R") |> source()
here("R", "logoclim_raster.R") |> source()
here("R", "print_setup.R") |> source()
here("R", "compare_plots.R") |> source()
here("R", "plot_difference.R") |> source()
here("R", "compare_statistics.R") |> source()
here("R", "test_near_equality.R") |> source()

B.3.3 Set Data Directory

The here R package (Müller, 2025) is used to construct file paths relative to the project root directory, ensuring portability across different systems.

A local temporary directory is used to help with debugging and to avoid access privilege issues that can arise when writing outside the project directory. This is particularly relevant for GitHub Actions when using macOS runners.

data_dir <- here(".data-temp") |> path_norm()
if (!dir_exists(data_dir)) {
  dir_create(data_dir)
} else {
  dir_ls(data_dir) |> file_delete()
}

B.3.4 Set Initial Variables

Setting the JAVA_TOOL_OPTIONS environment variable is optional, but recommended to avoid unnecessary messages from the Java Media Framework.

Sys.setenv(JAVA_TOOL_OPTIONS = "-Dcom.sun.media.jai.disableMediaLib=true")

Setting ZIP_PROGRESS environment variable is required by the zip R package to display progress bars when unzipping data.

Sys.setenv(ZIP_PROGRESS = "true")
model_path <- here("nlogox", "logoclim.nlogox")

B.4 Select Random Country

B.4.1 Select Country

The list of countries is based on the ISO 3166-1 alpha-3 standard and draw using the ISOcodes R package (Hornik & Buchta, 2025).

country <-
  country_names("alpha 3") |>
  str_subset("ATA|ISL|MLI|TZA", negate = TRUE) |>
  sample(1)
country
#> Comoros 
#>   "COM"

B.4.2 Download Country Shape

The rspatial geodata R package (Hijmans et al., 2024) is used to download country shapes from the GADM database (Hijmans, n.d.).

country_shape <-
  country |>
  gadm(
    level = 0,
    path = path(data_dir),
    resolution = 1
  )
#> Cached as: /home/runner/work/logoclim/logoclim/.data-temp/gadm/gadm41_COM_0_pk.rds

B.4.3 Calculate Shape Area

shape_area <-
  country_shape |>
  expanse(unit = "km") |>
  magrittr::extract(1)

This while loop filters out micronations. The country’s area is divided by 21 km², the approximate area of a single cell at 2.5-minute resolution at the Equator. This resolution was chosen because it’s the highest available across all three WorldClim datasets (the Historical Monthly Weather Data series tops out at 2.5 minutes). The division ensures the selected country has enough area to yield at least 1,000 cells for analysis.

while (!(shape_area / 21 >= 1000)) {
  country <- country_names("alpha 3") |> sample(1)

  country_shape <-
    country |>
    gadm(
      level = 0,
      path = path(data_dir),
      resolution = 1
    )

  shape_area <-
    country_shape |>
    expanse(unit = "km") |>
    magrittr::extract(1)
}
#> Cached as: /home/runner/work/logoclim/logoclim/.data-temp/gadm/gadm41_ASM_0_pk.rds
#> Cached as: /home/runner/work/logoclim/logoclim/.data-temp/gadm/gadm41_NOR_0_pk.rds
shape_area
#> [1] 325121.2322
country
#> Norway 
#>  "NOR"

B.4.4 Visualize Country Shape

The rotate() function from the terra package (Hijmans, 2026) is used to adjust shapes that cross the International Date Line (IDL) (e.g., Russian territory).

This adjustment applies only for visualizing countries on leaflet and does not affect the near-equality tests. The Esri ASCII transformation function (worldclim_to_ascii()), used to prepare data for NetLogo, applies the necessary rotation to ensure proper alignment of the raster data with the model’s patch grid.

idl_countries <- list(
  "FJI" = list(left = FALSE),
  "KIR" = list(left = TRUE),
  "NZL" = list(left = FALSE),
  "RUS" = list(left = FALSE),
  "USA" = list(left = TRUE)
)
if (country %in% names(idl_countries)) {
  country_shape_leaflet <-
    country_shape |>
    rotate(left = idl_countries[[country]]$left)
} else {
  country_shape_leaflet <- country_shape
}
leaflet() |>
  addProviderTiles(providers$Esri.WorldStreetMap) |>
  fitBounds(
    lng1 = country_shape_leaflet |>
      st_bbox() |>
      magrittr::extract("xmin") |>
      unname(),
    lat1 = country_shape_leaflet |>
      st_bbox() |>
      magrittr::extract("ymin") |>
      unname(),
    lng2 = country_shape_leaflet |>
      st_bbox() |>
      magrittr::extract("xmax") |>
      unname(),
    lat2 = country_shape_leaflet |>
      st_bbox() |>
      magrittr::extract("ymax") |>
      unname()
  ) |>
  addPolygons(
    data = country_shape_leaflet,
    fillColor = "transparent",
    color = "blue",
    weight = 2,
    opacity = 1
  )

B.5 Test Historical Climate Data

This section performs near-equality tests using WorldClim’s Historical Climate Data series.

B.5.1 Select Random WorldClim Dataset

setup <- worldclim_random("hcd")
while (setup$variable %in% c("bioc", "elev")) {
  setup <- worldclim_random("hcd")
}
setup <-
  setup |>
  inset2(
    "resolution",
    case_when(
      shape_area / 340 >= 1000 ~
        c("10 Minutes (~340 km2 at the Equator)" = "10m"),
      shape_area / 85 >= 1000 ~
        c("5 Minutes (~85 km2 at the Equator)" = "5m"),
      shape_area / 21 >= 1000 ~
        c("2.5 Minutes (~21 km2 at the Equator)" = "2.5m"),
      TRUE ~
        c("30 Seconds (~1 km2  at the Equator)" = "30s")
    )
  )
setup
#> $series
#> Historical Climate Data 
#>                   "hcd" 
#> 
#> $resolution
#> 5 Minutes (~85 km2 at the Equator) 
#>                               "5m" 
#> 
#> $variable
#> Average Temperature (°C) 
#>                   "tavg" 
#> 
#> $year
#> 1970-2000 
#>      1986 
#> 
#> $month
#> April 
#>     4

B.5.2 Download Dataset

tif_files <- worldclim_download(
  series = setup$series,
  resolution = setup$resolution,
  variable = setup$variable,
  model = setup$model,
  ssp = setup$ssp,
  year = names(setup$year),
  dir = data_dir,
  connection_timeout = 60,
  max_tries = 3,
  retry_on_failure = TRUE,
  backoff = \(attempt) 5^attempt
)
#> ℹ Scraping WorldClim website
#> ✔ Scraping WorldClim website [202ms]
#> 
#> ℹ Calculating file sizes
#> ℹ Total download size (compressed): 121M.
#> ℹ Calculating file sizes
✔ Calculating file sizes [558ms]
#> 
#> ℹ Creating LICENSE and README files
#> ✔ Creating LICENSE and README files [136ms]
#> 
#> ℹ Downloading files
#> ℹ Downloading 1 file to '/home/runner/work/logoclim/logoclim/.data-temp/historical-climate-data'
#> ℹ Downloading files
✔ Downloading files [4.3s]
#> 
#> ℹ Unzipping files
#> ✔ Unzipping files [13ms]

B.5.3 Transform Data to Esri ASCII Format

tif_file <-
  tif_files |>
  str_subset(
    paste0(
      "(?<=_)",
      setup$variable |> unname(),
      "_",
      str_pad(
        setup$month,
        width = 2,
        pad = "0"
      )
    )
  )

The dx parameter specifies the degree and direction of data rotation. Negative values rotate the data to the left, while positive values rotate it to the right. This adjustment is applied only for countries crossing the International Date Line (IDL).

asc_file <-
  tif_file |>
  worldclim_to_ascii(
    shape = country_shape,
    dx = if_else(country == "USA", 30, -45)
  )

B.5.4 Run Data in LogoClim

setup_file <- create_experiment(
  name = paste0("WorldClim", ": ", names(setup$series)),
  setup = 'setup false',
  go = NULL,
  metrics = c(
    'index',
    'month',
    'year',
    'files',
    'world-width',
    'world-height',
    'cell-size',
    '[first latitude] of patches',
    '[first longitude] of patches',
    '[value] of patches'
  ),
  constants = list(
    "data-series" = names(setup$series),
    "data-resolution" = names(setup$resolution),
    "climate-variable" = names(setup$variable),
    "start-month" = names(setup$month),
    "start-year" = setup$year,
    "data-path" = data_dir
  )
)
results <-
  model_path |>
  run_experiment(
    setup_file = setup_file,
    output = c("table", "lists")
  )
results |> glimpse()
#> List of 3
#>  $ metadata:List of 6
#>   ..$ timestamp       : POSIXct[1:1], format: "2026-07-18 18:36:58"
#>   ..$ netlogo_version : chr "7.0.4"
#>   ..$ output_version  : chr "2.0"
#>   ..$ model_file      : chr "logoclim.nlogox"
#>   ..$ experiment_name : chr "WorldClim: Historical Climate Data"
#>   ..$ world_dimensions: Named int [1:4] -135 135 -116 117
#>   .. ..- attr(*, "names")= chr [1:4] "min-pxcor" "max-pxcor" "min-pycor" "max-pycor"
#>  $ table   : tibble [1 × 18] (S3: tbl_df/tbl/data.frame)
#>   ..$ run_number                : num 1
#>   ..$ data_series               : chr "Historical Climate Data"
#>   ..$ data_resolution           : chr "5 Minutes (~85 km2 at the Equator)"
#>   ..$ climate_variable          : chr "Average Temperature (°C)"
#>   ..$ start_month               : chr "April"
#>   ..$ start_year                : num 1986
#>   ..$ data_path                 : chr "/home/runner/work/logoclim/logoclim/.data-temp"
#>   ..$ step                      : num 1
#>   ..$ index                     : num 0
#>   ..$ month                     : chr "April"
#>   ..$ year                      : chr "1970-2000"
#>   ..$ files                     : chr "[wc2.1_5m_tavg_1970-2000-04.asc]"
#>   ..$ world_width               : num 328
#>   ..$ world_height              : num 160
#>   ..$ cell_size                 : num 0.0833
#>   ..$ first_latitude_of_patches : chr "[65.166666666638 70.666666666616 70.749999999949 70.249999999951 67.08333333329699 69.249999999955 59.666666666"| __truncated__
#>   ..$ first_longitude_of_patches: chr "[24.083333333251996 26.999999999906997 10.249999999973998 31.083333333223997 15.583333333285998 6.7499999999879"| __truncated__
#>   ..$ value_of_patches          : chr "[false -1.76911759376526 false false -2.96799993515015 false 4.66428565979004 false false false false 3.0712499"| __truncated__
#>  $ lists   : tibble [52,480 × 13] (S3: tbl_df/tbl/data.frame)
#>   ..$ run_number                : num [1:52480] 1 1 1 1 1 1 1 1 1 1 ...
#>   ..$ data_series               : chr [1:52480] "Historical Climate Data" "Historical Climate Data" "Historical Climate Data" "Historical Climate Data" ...
#>   ..$ data_resolution           : chr [1:52480] "5 Minutes (~85 km2 at the Equator)" "5 Minutes (~85 km2 at the Equator)" "5 Minutes (~85 km2 at the Equator)" "5 Minutes (~85 km2 at the Equator)" ...
#>   ..$ climate_variable          : chr [1:52480] "Average Temperature (°C)" "Average Temperature (°C)" "Average Temperature (°C)" "Average Temperature (°C)" ...
#>   ..$ start_month               : chr [1:52480] "April" "April" "April" "April" ...
#>   ..$ start_year                : num [1:52480] 1986 1986 1986 1986 1986 ...
#>   ..$ data_path                 : chr [1:52480] "/home/runner/work/logoclim/logoclim/.data-temp" "/home/runner/work/logoclim/logoclim/.data-temp" "/home/runner/work/logoclim/logoclim/.data-temp" "/home/runner/work/logoclim/logoclim/.data-temp" ...
#>   ..$ step                      : num [1:52480] 1 1 1 1 1 1 1 1 1 1 ...
#>   ..$ index                     : num [1:52480] 0 1 2 3 4 5 6 7 8 9 ...
#>   ..$ files                     : chr [1:52480] "wc2.1_5m_tavg_1970-2000-04.asc" NA NA NA ...
#>   ..$ first_latitude_of_patches : num [1:52480] 65.2 70.7 70.7 70.2 67.1 ...
#>   ..$ first_longitude_of_patches: num [1:52480] 24.1 27 10.2 31.1 15.6 ...
#>   ..$ value_of_patches          : chr [1:52480] "false" "-1.76911759376526" "false" "false" ...

B.5.5 Compare Plots

To enable side-by-side comparison in the plots, the LogoClim patch data is resampled to match the WorldClim GeoTIFF grid extent and resolution. This resampling is performed solely for visualization purposes and may introduce minor visual differences between the maps. The statistical comparisons presented in the tables are based on the original data values.

compare_plots(
  tif_file = tif_file,
  country_shape = country_shape,
  results = results,
  setup = setup,
  dx = if_else(country == "USA", 30, -45),
  viridis = FALSE
)

plot_difference(
  tif_file = tif_file,
  country_shape = country_shape,
  results = results,
  setup = setup,
  dx = if_else(country == "USA", 30, -45),
  viridis = FALSE
)

B.5.6 Compare Statistics

statistics <- compare_statistics(
  tif_file = tif_file,
  country_shape = country_shape,
  results = results,
  dx = if_else(country == "USA", 30, -45),
  tolerance = tolerance
)
statistics

B.5.7 Test Near-Equality

statistics |>
  test_near_equality(
    check = c("n", "min", "mean", "max"),
    tolerance = tolerance
  )
#> ℹ n
#> ✔ n [248ms]
#> 
#> ℹ min
#> ✔ min [41ms]
#> 
#> ℹ mean
#> ✔ mean [34ms]
#> 
#> ℹ max
#> ✔ max [33ms]
#> 

B.6 Test Historical Monthly Weather Data

This section performs near-equality tests using WorldClim’s Historical Monthly Weather Data series.

B.6.1 Select Random WorldClim Dataset

setup <- worldclim_random("hmwd")

WorldClim’s Historical Monthly Weather Data series does not include the 30 seconds (~1 km² at the Equator) resolution.

setup <-
  setup |>
  inset2(
    "resolution",
    case_when(
      shape_area / 340 >= 1000 ~
        c("10 Minutes (~340 km2 at the Equator)" = "10m"),
      shape_area / 85 >= 1000 ~
        c("5 Minutes (~85 km2 at the Equator)" = "5m"),
      TRUE ~
        c("2.5 Minutes (~21 km2 at the Equator)" = "2.5m")
    )
  )
setup
#> $series
#> Historical Monthly Weather Data 
#>                          "hmwd" 
#> 
#> $resolution
#> 5 Minutes (~85 km2 at the Equator) 
#>                               "5m" 
#> 
#> $variable
#> Average Maximum Temperature (°C) 
#>                           "tmax" 
#> 
#> $year
#> 1970-1979 
#>      1971 
#> 
#> $month
#> October 
#>      10

B.6.2 Download Dataset

tif_files <- worldclim_download(
  series = setup$series,
  resolution = setup$resolution,
  variable = setup$variable,
  model = setup$model,
  ssp = setup$ssp,
  year = names(setup$year),
  dir = data_dir,
  connection_timeout = 60,
  max_tries = 3,
  retry_on_failure = TRUE,
  backoff = \(attempt) 5^attempt
)
#> ℹ Scraping WorldClim website
#> ✔ Scraping WorldClim website [180ms]
#> 
#> ℹ Calculating file sizes
#> ℹ Total download size (compressed): 319M.
#> ℹ Calculating file sizes
✔ Calculating file sizes [338ms]
#> 
#> ℹ Creating LICENSE and README files
#> ✔ Creating LICENSE and README files [18ms]
#> 
#> ℹ Downloading files
#> ℹ Downloading 1 file to '/home/runner/work/logoclim/logoclim/.data-temp/historical-monthly-weather-data'
#> ℹ Downloading files
✔ Downloading files [10s]
#> 
#> ℹ Unzipping files
#> ✔ Unzipping files [26ms]
#> 
#> Unzipping ■■■■■■■■■■■■■■■■■■■■■■■■          78% |  ETA:  1s
#> Unzipping ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■  100% |  ETA:  0s

B.6.3 Transform Data to Esri ASCII Format

tif_file <-
  tif_files |>
  str_subset(
    paste0(
      setup$year,
      "-",
      str_pad(setup$month, width = 2, pad = "0")
    )
  )

The dx parameter specifies the degree and direction of data rotation. Negative values rotate the data to the left, while positive values rotate it to the right. This adjustment is applied only for countries crossing the International Date Line (IDL).

asc_file <-
  tif_file |>
  worldclim_to_ascii(
    shape = country_shape,
    dx = if_else(country == "USA", 30, -45)
  )

B.6.4 Run Data in LogoClim

setup_file <- create_experiment(
  name = paste0("WorldClim", ": ", names(setup$series)),
  setup = 'setup false',
  go = NULL,
  metrics = c(
    'index',
    'month',
    'year',
    'files',
    'world-width',
    'world-height',
    'cell-size',
    '[first latitude] of patches',
    '[first longitude] of patches',
    '[value] of patches'
  ),
  constants = list(
    "data-series" = names(setup$series),
    "data-resolution" = names(setup$resolution),
    "climate-variable" = names(setup$variable),
    "start-month" = names(setup$month),
    "start-year" = setup$year,
    "data-path" = data_dir
  )
)
results <-
  model_path |>
  run_experiment(
    setup_file = setup_file,
    output = c("table", "lists")
  )
results |> glimpse()
#> List of 3
#>  $ metadata:List of 6
#>   ..$ timestamp       : POSIXct[1:1], format: "2026-07-18 18:38:48"
#>   ..$ netlogo_version : chr "7.0.4"
#>   ..$ output_version  : chr "2.0"
#>   ..$ model_file      : chr "logoclim.nlogox"
#>   ..$ experiment_name : chr "WorldClim: Historical Monthly Weather Data"
#>   ..$ world_dimensions: Named int [1:4] -135 135 -116 117
#>   .. ..- attr(*, "names")= chr [1:4] "min-pxcor" "max-pxcor" "min-pycor" "max-pycor"
#>  $ table   : tibble [1 × 18] (S3: tbl_df/tbl/data.frame)
#>   ..$ run_number                : num 1
#>   ..$ data_series               : chr "Historical Monthly Weather Data"
#>   ..$ data_resolution           : chr "5 Minutes (~85 km2 at the Equator)"
#>   ..$ climate_variable          : chr "Average Maximum Temperature (°C)"
#>   ..$ start_month               : chr "October"
#>   ..$ start_year                : num 1971
#>   ..$ data_path                 : chr "/home/runner/work/logoclim/logoclim/.data-temp"
#>   ..$ step                      : num 1
#>   ..$ index                     : num 0
#>   ..$ month                     : chr "October"
#>   ..$ year                      : num 1971
#>   ..$ files                     : chr "[wc2.1_cruts4.09_5m_tmax_1971-10.asc]"
#>   ..$ world_width               : num 328
#>   ..$ world_height              : num 160
#>   ..$ cell_size                 : num 0.0833
#>   ..$ first_latitude_of_patches : chr "[65.416666666637 63.999999999976 61.833333333317995 63.41666666664499 59.583333333327 59.749999999992994 66.333"| __truncated__
#>   ..$ first_longitude_of_patches: chr "[14.833333333288998 13.416666666627997 29.416666666563998 7.666666666650997 3.833333333332998 12.08333333329999"| __truncated__
#>   ..$ value_of_patches          : chr "[false 2 false false false false false false 10 4.66666650772095 false false false false false false false 5.75"| __truncated__
#>  $ lists   : tibble [52,480 × 13] (S3: tbl_df/tbl/data.frame)
#>   ..$ run_number                : num [1:52480] 1 1 1 1 1 1 1 1 1 1 ...
#>   ..$ data_series               : chr [1:52480] "Historical Monthly Weather Data" "Historical Monthly Weather Data" "Historical Monthly Weather Data" "Historical Monthly Weather Data" ...
#>   ..$ data_resolution           : chr [1:52480] "5 Minutes (~85 km2 at the Equator)" "5 Minutes (~85 km2 at the Equator)" "5 Minutes (~85 km2 at the Equator)" "5 Minutes (~85 km2 at the Equator)" ...
#>   ..$ climate_variable          : chr [1:52480] "Average Maximum Temperature (°C)" "Average Maximum Temperature (°C)" "Average Maximum Temperature (°C)" "Average Maximum Temperature (°C)" ...
#>   ..$ start_month               : chr [1:52480] "October" "October" "October" "October" ...
#>   ..$ start_year                : num [1:52480] 1971 1971 1971 1971 1971 ...
#>   ..$ data_path                 : chr [1:52480] "/home/runner/work/logoclim/logoclim/.data-temp" "/home/runner/work/logoclim/logoclim/.data-temp" "/home/runner/work/logoclim/logoclim/.data-temp" "/home/runner/work/logoclim/logoclim/.data-temp" ...
#>   ..$ step                      : num [1:52480] 1 1 1 1 1 1 1 1 1 1 ...
#>   ..$ index                     : num [1:52480] 0 1 2 3 4 5 6 7 8 9 ...
#>   ..$ files                     : chr [1:52480] "wc2.1_cruts4.09_5m_tmax_1971-10.asc" NA NA NA ...
#>   ..$ first_latitude_of_patches : num [1:52480] 65.4 64 61.8 63.4 59.6 ...
#>   ..$ first_longitude_of_patches: num [1:52480] 14.83 13.42 29.42 7.67 3.83 ...
#>   ..$ value_of_patches          : chr [1:52480] "false" "2" "false" "false" ...

B.6.5 Compare Plots

To enable side-by-side comparison in the plots, the LogoClim patch data is resampled to match the WorldClim GeoTIFF grid extent and resolution. This resampling is performed solely for visualization purposes and may introduce minor visual differences between the maps. The statistical comparisons presented in the tables are based on the original data values.

compare_plots(
  tif_file = tif_file,
  country_shape = country_shape,
  results = results,
  setup = setup,
  dx = if_else(country == "USA", 30, -45),
  viridis = FALSE
)

plot_difference(
  tif_file = tif_file,
  country_shape = country_shape,
  results = results,
  setup = setup,
  dx = if_else(country == "USA", 30, -45),
  viridis = FALSE
)

B.6.6 Compare Statistics

statistics <- compare_statistics(
  tif_file = tif_file,
  country_shape = country_shape,
  results = results,
  dx = if_else(country == "USA", 30, -45),
  tolerance = tolerance
)
statistics

B.6.7 Test Near-Equality

statistics |>
  test_near_equality(
    check = c("n", "min", "mean", "max"),
    tolerance = tolerance
  )
#> ℹ n
#> ✔ n [26ms]
#> 
#> ℹ min
#> ✔ min [33ms]
#> 
#> ℹ mean
#> ✔ mean [33ms]
#> 
#> ℹ max
#> ✔ max [32ms]
#> 

B.7 Test Future Climate Data

This section performs near-equality tests using WorldClim’s Future Climate Data series.

B.7.1 Select Random WorldClim Dataset

setup <- worldclim_random("fcd")
while (
  setup$model %in%
    c("FIO-ESM-2-0", "GFDL-ESM4", "HadGEM3-GC31-LL") ||
    setup$variable %in% c("bioc")
) {
  setup <- worldclim_random("fcd")
}
setup <-
  setup |>
  inset2(
    "resolution",
    case_when(
      shape_area / 340 >= 1000 ~
        c("10 Minutes (~340 km2 at the Equator)" = "10m"),
      shape_area / 85 >= 1000 ~
        c("5 Minutes (~85 km2 at the Equator)" = "5m"),
      shape_area / 21 >= 1000 ~
        c("2.5 Minutes (~21 km2 at the Equator)" = "2.5m"),
      TRUE ~
        c("30 Seconds (~1 km2  at the Equator)" = "30s")
    )
  )
setup
#> $series
#> Future Climate Data 
#>               "fcd" 
#> 
#> $resolution
#> 5 Minutes (~85 km2 at the Equator) 
#>                               "5m" 
#> 
#> $variable
#> Average Maximum Temperature (°C) 
#>                           "tmax" 
#> 
#> $model
#> Beijing Climate Center Climate System Model, China 
#>                                      "BCC-CSM2-MR" 
#> 
#> $ssp
#>  SSP-370 
#> "ssp370" 
#> 
#> $year
#> 2041-2060 
#>      2055 
#> 
#> $month
#> February 
#>        2

B.7.2 Download Dataset

tif_file <- worldclim_download(
  series = setup$series,
  resolution = setup$resolution,
  variable = setup$variable,
  model = setup$model,
  ssp = setup$ssp,
  year = names(setup$year),
  dir = data_dir,
  connection_timeout = 60,
  max_tries = 3,
  retry_on_failure = TRUE,
  backoff = \(attempt) 5^attempt
)
#> ℹ Scraping WorldClim website
#> ✔ Scraping WorldClim website [221ms]
#> 
#> ℹ Calculating file sizes
#> ℹ Total download size (compressed): 64.8M.
#> ℹ Calculating file sizes
✔ Calculating file sizes [292ms]
#> 
#> ℹ Creating LICENSE and README files
#> ✔ Creating LICENSE and README files [17ms]
#> 
#> ℹ Downloading files
#> ℹ Downloading 1 file to '/home/runner/work/logoclim/logoclim/.data-temp/future-climate-data'
#> ℹ Downloading files
✔ Downloading files [2.9s]
#> 
#> ℹ Unzipping files
#> ✔ Unzipping files [14ms]

B.7.3 Transform Data to Esri ASCII Format

The dx parameter specifies the degree and direction of data rotation. Negative values rotate the data to the left, while positive values rotate it to the right. This adjustment is applied only for countries crossing the International Date Line (IDL).

asc_file <-
  tif_file |>
  worldclim_to_ascii(
    shape = country_shape,
    dx = if_else(country == "USA", 30, -45)
  )
asc_file <-
  asc_file |>
  str_subset(
    paste0(
      names(setup$year),
      "[_-]",
      ifelse(
        setup$variable == "bioc",
        str_pad(setup$bioclimatic_variable, width = 2, pad = "0"),
        str_pad(setup$month, width = 2, pad = "0")
      )
    ),
  )

B.7.4 Run Data in LogoClim

setup_file <- create_experiment(
  name = paste0("WorldClim", ": ", names(setup$series)),
  setup = 'setup false',
  go = NULL,
  metrics = c(
    'index',
    'month',
    'year',
    'files',
    'world-width',
    'world-height',
    'cell-size',
    '[first latitude] of patches',
    '[first longitude] of patches',
    '[value] of patches'
  ),
  constants = list(
    "data-series" = names(setup$series),
    "data-resolution" = names(setup$resolution),
    "climate-variable" = names(setup$variable),
    "global-climate-model" = setup$model,
    "shared-socioeconomic-pathway" = names(setup$ssp),
    "start-month" = names(setup$month),
    "start-year" = setup$year,
    "data-path" = data_dir
  )
)
results <-
  model_path |>
  run_experiment(
    setup_file = setup_file,
    output = c("table", "lists")
  )
results |> glimpse()
#> List of 3
#>  $ metadata:List of 6
#>   ..$ timestamp       : POSIXct[1:1], format: "2026-07-18 18:40:27"
#>   ..$ netlogo_version : chr "7.0.4"
#>   ..$ output_version  : chr "2.0"
#>   ..$ model_file      : chr "logoclim.nlogox"
#>   ..$ experiment_name : chr "WorldClim: Future Climate Data"
#>   ..$ world_dimensions: Named int [1:4] -135 135 -116 117
#>   .. ..- attr(*, "names")= chr [1:4] "min-pxcor" "max-pxcor" "min-pycor" "max-pycor"
#>  $ table   : tibble [1 × 20] (S3: tbl_df/tbl/data.frame)
#>   ..$ run_number                  : num 1
#>   ..$ data_series                 : chr "Future Climate Data"
#>   ..$ data_resolution             : chr "5 Minutes (~85 km2 at the Equator)"
#>   ..$ climate_variable            : chr "Average Maximum Temperature (°C)"
#>   ..$ global_climate_model        : chr "BCC-CSM2-MR"
#>   ..$ shared_socioeconomic_pathway: chr "SSP-370"
#>   ..$ start_month                 : chr "February"
#>   ..$ start_year                  : num 2055
#>   ..$ data_path                   : chr "/home/runner/work/logoclim/logoclim/.data-temp"
#>   ..$ step                        : num 1
#>   ..$ index                       : num 0
#>   ..$ month                       : chr "February"
#>   ..$ year                        : chr "2041-2060"
#>   ..$ files                       : chr "[wc2.1_5m_tmax_BCC-CSM2-MR_ssp370_2041-2060-02.asc wc2.1_5m_tmax_BCC-CSM2-MR_ssp370_2041-2060-03.asc wc2.1_5m_t"| __truncated__
#>   ..$ world_width                 : num 328
#>   ..$ world_height                : num 160
#>   ..$ cell_size                   : num 0.0833
#>   ..$ first_latitude_of_patches   : chr "[61.666666666652 69.999999999952 61.74999999998499 65.416666666637 62.583333333315 63.999999999976 61.666666666"| __truncated__
#>   ..$ first_longitude_of_patches  : chr "[9.666666666642996 20.583333333265998 21.333333333262996 11.249999999969997 22.249999999925997 19.4999999999369"| __truncated__
#>   ..$ value_of_patches            : chr "[-2.5 false false false false false -3.40000009536743 false false false false false 3.29999995231628 false fals"| __truncated__
#>  $ lists   : tibble [52,480 × 15] (S3: tbl_df/tbl/data.frame)
#>   ..$ run_number                  : num [1:52480] 1 1 1 1 1 1 1 1 1 1 ...
#>   ..$ data_series                 : chr [1:52480] "Future Climate Data" "Future Climate Data" "Future Climate Data" "Future Climate Data" ...
#>   ..$ data_resolution             : chr [1:52480] "5 Minutes (~85 km2 at the Equator)" "5 Minutes (~85 km2 at the Equator)" "5 Minutes (~85 km2 at the Equator)" "5 Minutes (~85 km2 at the Equator)" ...
#>   ..$ climate_variable            : chr [1:52480] "Average Maximum Temperature (°C)" "Average Maximum Temperature (°C)" "Average Maximum Temperature (°C)" "Average Maximum Temperature (°C)" ...
#>   ..$ global_climate_model        : chr [1:52480] "BCC-CSM2-MR" "BCC-CSM2-MR" "BCC-CSM2-MR" "BCC-CSM2-MR" ...
#>   ..$ shared_socioeconomic_pathway: chr [1:52480] "SSP-370" "SSP-370" "SSP-370" "SSP-370" ...
#>   ..$ start_month                 : chr [1:52480] "February" "February" "February" "February" ...
#>   ..$ start_year                  : num [1:52480] 2055 2055 2055 2055 2055 ...
#>   ..$ data_path                   : chr [1:52480] "/home/runner/work/logoclim/logoclim/.data-temp" "/home/runner/work/logoclim/logoclim/.data-temp" "/home/runner/work/logoclim/logoclim/.data-temp" "/home/runner/work/logoclim/logoclim/.data-temp" ...
#>   ..$ step                        : num [1:52480] 1 1 1 1 1 1 1 1 1 1 ...
#>   ..$ index                       : num [1:52480] 0 1 2 3 4 5 6 7 8 9 ...
#>   ..$ files                       : chr [1:52480] "wc2.1_5m_tmax_BCC-CSM2-MR_ssp370_2041-2060-02.asc" "wc2.1_5m_tmax_BCC-CSM2-MR_ssp370_2041-2060-03.asc" "wc2.1_5m_tmax_BCC-CSM2-MR_ssp370_2041-2060-04.asc" "wc2.1_5m_tmax_BCC-CSM2-MR_ssp370_2041-2060-05.asc" ...
#>   ..$ first_latitude_of_patches   : num [1:52480] 61.7 70 61.7 65.4 62.6 ...
#>   ..$ first_longitude_of_patches  : num [1:52480] 9.67 20.58 21.33 11.25 22.25 ...
#>   ..$ value_of_patches            : chr [1:52480] "-2.5" "false" "false" "false" ...

B.7.5 Compare Plots

To enable side-by-side comparison in the plots, the LogoClim patch data is resampled to match the WorldClim GeoTIFF grid extent and resolution. This resampling is performed solely for visualization purposes and may introduce minor visual differences between the maps. The statistical comparisons presented in the tables are based on the original data values.

compare_plots(
  tif_file = tif_file,
  country_shape = country_shape,
  results = results,
  setup = setup,
  dx = if_else(country == "USA", 30, -45),
  layer_pattern = setup$month |>
    str_pad(, width = 2, pad = "0") |>
    paste0("$"),
  viridis = FALSE
)

plot_difference(
  tif_file = tif_file,
  country_shape = country_shape,
  results = results,
  setup = setup,
  dx = if_else(country == "USA", 30, -45),
  layer_pattern = setup$month |>
    str_pad(, width = 2, pad = "0") |>
    paste0("$"),
  viridis = FALSE
)

B.7.6 Compare Statistics

statistics <- compare_statistics(
  tif_file = tif_file,
  country_shape = country_shape,
  results = results,
  layer_pattern = setup$month |>
    str_pad(, width = 2, pad = "0") |>
    paste0("$"),
  dx = if_else(country == "USA", 30, -45),
  tolerance = tolerance
)
statistics

B.7.7 Test Near-Equality

statistics |>
  test_near_equality(
    check = c("n", "min", "mean", "max"),
    tolerance = tolerance
  )
#> ℹ n
#> ✔ n [27ms]
#> 
#> ℹ min
#> ✔ min [32ms]
#> 
#> ℹ mean
#> ✔ mean [33ms]
#> 
#> ℹ max
#> ✔ max [33ms]
#>