This tutorial walks through the process of adding a new data source and variable to PRIOGRID. Contributions are welcome — report issues or suggest new sources via the Issue Tracker.
Package Layout for Contributors
priogrid/
├── R/
│ ├── data_{source}.R # read_*() and gen_*() functions for each source
│ ├── config.R # pg_config(), pg_set_config(), pg_set_rawfolder(), .pg_release_specs
│ ├── build_priogrid.R # calc_pg(), load_pgvariable(), read_pg_static(), etc.
│ ├── utility.R # robust_transformation(), rast_to_df(), prio_blank_grid()
│ ├── references.R # pgcitations(), get_bibliography()
│ ├── source_class.R # Source R6 class (dev-only: used in devtools::load_all())
│ └── download_data.R # pgsearch(), pg_rawfiles(), check_pgsourcefiles()
├── data_raw/
│ ├── sources.csv # Add new source here (tab-separated)
│ ├── variables.csv # Add new variable here (tab-separated)
│ ├── pgsources.R # Regenerates data/pgsources.rda from sources.csv
│ └── pgvariables.R # Regenerates data/pgvariables.rda from variables.csv
├── data/
│ ├── pgsources.rda # Compiled metadata (auto-generated, do not edit)
│ ├── pgvariables.rda # Compiled metadata (auto-generated, do not edit)
│ └── pgchecksum.rda # MD5 reference checksums (auto-generated)
├── inst/
│ ├── REFERENCES.bib # Full bibliography — add new BibTeX entries here
│ └── extdata/urls/ # Multi-file URL lists (one .txt per source)
└── tests/testthat/
└── test-build_priogrid.R # Integration tests for gen_*() functions
Step 1: Register the Data Source
Add to data_raw/sources.csv
sources.csv is tab-separated with 18
columns. The intended path is not hand-editing but the
Source class + add_source() dev workflow (see
below), which fills id, created_at, and the
three *_exists booleans automatically.
Required fields:
| Column | Description | Example |
|---|---|---|
source_name |
Full name of the dataset | "My New Dataset" |
source_version |
Version string | "1.0" |
license |
SPDX or common license name | "CC BY 4.0" |
website_url |
Landing page | "https://example.com/data" |
spatial_extent |
One of: "World", "Multiple continents",
"Single continent", "Several countries"
|
"World" |
temporal_resolution |
One of: "Static", "Higher than monthly",
"Monthly", "Quarterly", "Yearly",
"Less than yearly"
|
"Yearly" |
citation_keys |
Semicolon-separated BibTeX keys | "doeNewDataset2025" |
download_url |
Direct download URL, or "urls/{uuid}.txt" for
multi-file sources |
"https://example.com/data.zip" |
Optional fields: aws_bucket, aws_region,
prio_mirror, tags,
reference_keys.
The following columns are auto-generated by
Source$to_tibble() and should not be
hand-written: id, download_url_exists,
website_url_exists, prio_mirror_exists,
created_at. If you are writing a row by hand (not via
add_source()), generate a UUID first:
uuid::UUIDgenerate()
# "a1b2c3d4-e5f6-7890-abcd-ef1234567890"Use the Source Class (dev mode)
In development mode (devtools::load_all()), use the
Source R6 class to validate a new source before adding it
to the CSV:
new_source <- Source$new(
source_name = "My New Dataset",
source_version = "1.0",
license = "CC BY 4.0",
website_url = "https://example.com/data",
spatial_extent = "World",
temporal_resolution = "Yearly",
citation_keys = "doeNewDataset2025",
download_url = "https://example.com/data/dataset_v1.zip",
tags = "climate, land cover"
)
new_source # prints validation report; warns if citation key not in REFERENCES.bibThe Source class validates required fields, checks that
spatial_extent and temporal_resolution use the
allowed vocabulary, and verifies that citation keys exist in
inst/REFERENCES.bib. Use
new_source$get_existing_tags() and
new_source$get_existing_licenses() to inspect vocabulary
already in use and keep tags and licenses consistent across sources.
Once validated, write the row to sources.csv with
add_source():
add_source(new_source)
# Appends a tab-separated row to data_raw/sources.csv and writes
# inst/extdata/urls/{id}.txt if download_url or prio_mirror points at a URL list.add_source(source, csv_file = "data_raw/sources.csv") is
dev-only (requires devtools::load_all()). It appends the
row and calls new_source$save_url_files() automatically.
After running it, regenerate pgsources.rda as below.
Add the Citation to inst/REFERENCES.bib
Add a BibTeX entry to inst/REFERENCES.bib for each
citation_keys value:
Regenerate pgsources.rda
After adding a source, regenerate the .rda file:
source("data_raw/pgsources.R")Verify the result:
pgsources[pgsources$source_name == "My New Dataset", ]Download the Raw Files
Before writing read_*()/gen_*(), fetch the
source’s files. Set the raw-data folder first if you haven’t
already:
pg_set_rawfolder("/path/to/rawdata")Then download and verify:
files <- pg_rawfiles() |> dplyr::filter(id == "a1b2c3d4-...")
download_pg_rawdata(file_info = files)
pg_data_availability() # confirm n_present == n_filesdownload_pg_rawdata() is resumable (.part
files), batched (batch_size = 20,
max_concurrent = 4), retries with backoff
(max_retry = 10), and verifies MD5 against
pgchecksum advisorily. See the metadata vignette for the discovery helpers
(pgsearch, pg_rawfiles,
pg_data_availability).
Step 2: Write read_*() and gen_*()
Functions
Create a new file R/data_mynewsource.R.
read_*() — Load Raw Data
The read function downloads (via get_pgfile()) and
returns the raw data as an sf or SpatRaster
object:
#' Read My New Dataset
#'
#' Downloads and imports My New Dataset.
#'
#' @return An \code{sf} object
#' @export
#' @references
#' \insertRef{doeNewDataset2025}{priogrid}
read_mynewsource <- function() {
f <- get_pgfile(
source_name = "My New Dataset",
source_version = "1.0",
id = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
)
sf::st_read(f)
}get_pgfile() automatically downloads the file if it is
not present locally (when automatic_download = TRUE in the
config).
gen_*() — Generate the PRIOGRID Variable
The generate function transforms raw data to the PRIOGRID grid and
returns a SpatRaster. The signature must accept
config:
#' My New Variable
#'
#' Computes my_new_variable for each PRIOGRID cell.
#'
#' @param config A \code{pg_config} object. Defaults to \code{\link{pg_current_config}()}.
#' @return A \code{SpatRaster} object
#' @export
#' @references
#' \insertRef{doeNewDataset2025}{priogrid}
gen_my_new_variable <- function(config = pg_current_config()) {
raw <- read_mynewsource()
# Reproject if needed
pg_crs <- sf::st_crs(config$crs)
if (sf::st_crs(raw) != pg_crs) {
raw <- sf::st_transform(raw, pg_crs)
}
# Convert to raster and re-grid to PRIOGRID resolution
raw_rast <- terra::rasterize(terra::vect(raw), prio_blank_grid(config),
field = "my_value_column", fun = "mean")
r <- robust_transformation(raw_rast, agg_fun = "mean", config = config)
names(r) <- "my_new_variable"
return(r)
}Using robust_transformation()
robust_transformation() handles reprojection, cropping,
aggregation, disaggregation, and final resampling in a single call. It
is the standard way to re-grid any raster:
robust_transformation(
r = raw_rast, # Any SpatRaster (any resolution / CRS / extent)
agg_fun = "mean", # Aggregation function for higher-res inputs
disagg_method = "near", # Disaggregation for lower-res inputs
config = config # Target config
)Common agg_fun values: "mean",
"sum", "max", "min",
"modal" (for categorical).
Temporal Variables
For time-varying variables, iterate over
pg_dates(config) and combine layers:
gen_my_yearly_variable <- function(config = pg_current_config()) {
dates <- pg_dates(config)
layers <- lapply(dates, function(d) {
raw <- read_mynewsource_for_year(lubridate::year(d)) # your reader
r <- robust_transformation(raw, agg_fun = "mean", config = config)
names(r) <- as.character(d)
r
})
do.call(c, layers) # stack into a multi-layer SpatRaster
}Step 3: Register the Variable
Add to data_raw/variables.csv
variables.csv is tab-separated with 7
columns. Add one row per variable:
| Column | Description / Allowed values |
|---|---|
name |
Must match the string set by names(r) in
gen_*() and equal gen_{name} without the
prefix |
static |
TRUE for variables without a time dimension,
FALSE for time-varying |
source_ids |
Comma-separated source UUIDs from sources.csv
|
label |
Human-readable display title for plots/legends |
unit |
Unit string (e.g. °C, mm); leave empty if
dimensionless |
transform |
One of: identity, log1p,
log10, sqrt
|
plot_type |
One of: continuous, positive_real,
count, share, discrete
|
A hand-written row must include all 7 tab-separated fields; empty
fields still need their tab delimiter. Example (tabs shown as
→):
name→static→source_ids→label→unit→transform→plot_type
my_new_variable→FALSE→a1b2c3d4-e5f6-7890-abcd-ef1234567890→My new variable→→identity→continuous
Regenerate pgvariables.rda:
source("data_raw/pgvariables.R")Verify:
pgvariables[pgvariables$name == "my_new_variable", ]Display Metadata and COG Metatags
The four authored display columns (label,
unit, transform, plot_type) are
stamped into each built COG as pg_* GDAL metatags by
save_pgvariable() via .pg_build_metatags(),
making the .tif self-describing. Built COGs additionally
carry the derived pg_colormap and computed
pg_value_min/pg_value_max/pg_value_mean/pg_value_std/pg_nunique/pg_class_values
metatags — these are not authored columns.
After editing display columns for an already-built variable, re-stamp without recomputing:
priogrid:::.pg_restamp_metatags("my_new_variable")Step 4: Document with roxygen2
PRIOGRID uses roxygen2 with Markdown support and Rdpack for citation references.
Key tags: - @param config — always document the config
parameter for gen_*() functions - @return —
describe what the function returns - @export — all
user-facing functions must be exported -
@references \insertRef{key}{priogrid} — links to
inst/REFERENCES.bib
Regenerate documentation:
devtools::document()Step 5: Add Tests
Add tests to tests/testthat/. For a gen_*
function, use the shared test helpers from
tests/testthat/helper-priogrid.R:
test_config() (nrow=5, ncol=10, 2010–2012 yearly) and
skip_if_no_rawdata(). A gen_* test needs
downloaded raw data, so it must call
skip_if_no_rawdata():
# tests/testthat/test-data_mynewsource.R
test_that("gen_my_new_variable returns a SpatRaster", {
skip_if_not_installed("terra")
skip_if_no_rawdata()
r <- gen_my_new_variable(config = test_config())
expect_s4_class(r, "SpatRaster")
expect_equal(names(r), "my_new_variable")
})Internal accessors (hashes, resolve_pg_mode) are tested
via priogrid::: — see
tests/testthat/test-build_priogrid.R for examples.
Run tests:
devtools::test()Step 6: Verify End-to-End
Test the full pipeline:
cfg <- pg_config()
# 1. Calculate
calc_pg("my_new_variable", config = cfg)
# 2. Load as raster
r <- load_pgvariable("my_new_variable", config = cfg)
terra::plot(r)
# 3. Load as table
pg_tv <- read_pg_timevarying(config = cfg)
"my_new_variable" %in% names(pg_tv)
# 4. Check citations resolve
pgcitations("my_new_variable")Multi-file Sources and URL Lists
When a source serves multiple files — or when URLs do not carry a
usable filename (e.g. FigShare download endpoints) — place a plain-text
URL list at inst/extdata/urls/{id}.txt. Each line is one
URL; an optional tab-separated second column names the
local file:
https://example.com/files/data_part1.nc
https://ndownloader.figshare.com/files/17626052 my_local_name.nc
The second column is only needed when the server does not expose the
filename via Content-Disposition or the post-redirect URL. Helpers:
pg_read_url_list(), pg_format_url_list(),
pg_default_filename(). add_source() calls
new_source$save_url_files() automatically and writes the
file when download_url or prio_mirror is a
local list path.
Resolving opaque filenames.
priogrid:::pg_resolve_filenames(id, write = FALSE) queries
each server (Content-Disposition header, then post-redirect URL) and
prints a proposed filename column, flagging suspicious names (no
extension, login endpoints). Re-run with write = TRUE to
commit the names, then rebuild and run
pg_update_checksums().
priogrid:::pg_resolve_filenames("a1b2c3d4-e5f6-7890-abcd-ef1234567890", write = FALSE)
# Review proposed names, then commit:
priogrid:::pg_resolve_filenames("a1b2c3d4-e5f6-7890-abcd-ef1234567890", write = TRUE)Renaming already-downloaded files. When a source
gains an explicit filename column, use
priogrid:::pg_migrate_rawfiles() to rename
already-downloaded files in place so they are not re-downloaded:
priogrid:::pg_migrate_rawfiles(dry_run = TRUE) # preview renames
priogrid:::pg_migrate_rawfiles(dry_run = FALSE) # applyThe Build Pipeline: From gen_*() to a Dataset
calc_pg() and output layout
calc_pg(varnames = NULL, overwrite = FALSE, config)
calls each gen_<name>(config=) and passes the result
to save_pgvariable(). Outputs land under:
<rawfolder>/priogrid/custom/<pkg_version>/<spatial_hash>/<temporal_hash>/
The folder is created on first build and includes a
_config.R script (a reproducible pg_config()
call). Skips already-built variables unless
overwrite = TRUE; per-variable failures are collected and
reported, never fatal.
After calc_pg(), the folder contains:
-
cog/<varname>.tif— Cloud Optimised GeoTIFF (COG, DEFLATE) withtime()/units()fields andpg_*GDAL metatags -
_checksums.csv— per-file MD5s -
_config.R— reproducible config script
Spatial/temporal hashing. pgout_path()
keys output folders on 6-character MD5 hashes derived from grid
dimensions/CRS/extent (get_spatial_hash()) and from the
date range (get_temporal_hash()). The temporal hash floors
end_date so small date changes do not fork folders into new
locations.
load_pgvariable()
r <- load_pgvariable(
varname = "my_new_variable",
config = cfg, # NULL => release mode (auto-download_priogrid())
extent = c(-20, 50, -10, 40), # optional: windowed spatial read
layers = 1:5 # optional: subset time layers
)Returns a lazy, file-backed SpatRaster;
extent/layers enable windowed
larger-than-memory reads.
Table readers
# Static variables → pg_static.parquet + pg_static.csv.gz
static <- read_pg_static(config = cfg)
# Time-varying → hive-partitioned parquet + pg_timevarying.csv.gz + pg_config.json manifest
# Supports lazy subsetting:
tv <- read_pg_timevarying(
config = cfg,
years = 2000:2010,
variables = c("my_new_variable", "another_var"),
extent = c(-20, 50, -10, 40)
)build_pg_dataset() writes the hive dataset memory-safely
without collecting the full table into RAM.
Listing custom builds
customs <- pg_list_custom()
# Prints a summary of each custom folder; returns an indexed list of pg_config objects.
read_pg_static(config = customs[[1]])
calc_pg("new_var", config = customs[[2]])Memory management for large rasters
pg_configure_terra_memory() (called inside
robust_transformation()) auto-enables terra
on-disk processing when an estimated raster exceeds ~4 GB. This is
relevant when developing high-resolution gen_*()
functions.
File Integrity and Checksums
pgchecksum is a bundled data object
(data/pgchecksum.rda) with reference MD5s for the exact raw
files used to build the official release. Columns:
source_name, source_version, id,
filename, md5.
Runtime verification.
check_pgsourcefiles() compares local raw files to
pgchecksum and warns for files with no reference entry
(sources added since the last tested build). Enable per-read
verification with:
cfg <- pg_config(verify_checksums = TRUE)
# or per file:
get_pgfile(source_name = "My New Dataset", source_version = "1.0",
id = "a1b2c3d4-...", verify_checksums = TRUE)Regenerating checksums (maintainer). After a fully
verified, clean download set, recompute and rewrite
pgchecksum.rda:
priogrid:::pg_update_checksums(only_present = TRUE)New sources have no checksum entry until PRIO-GRID has been built from them once and this is run.
Cutting an Official Release
A release is registered in two places, both keyed
"<version>_<type>":
1. Grid spec — add an entry to
.pg_release_specs in R/config.R:
# Inside .pg_release_specs in R/config.R:
"3.0.2_05deg_yearly" = list(
nrow = 360L,
ncol = 720L,
crs = "epsg:4326",
extent = c(xmin = -180, xmax = 180, ymin = -90, ymax = 90),
temporal_resolution = "1 year",
start_date = as.Date("1850-12-31"),
end_date = as.Date("2026-08-26")
)2. CDN URL — add the corresponding entry to the
releases list inside download_priogrid() in
R/build_priogrid.R:
# Inside the releases list in download_priogrid() in R/build_priogrid.R:
"3.0.2_05deg_yearly" = "https://cdn.cloud.prio.org/files/<uuid>"Then build the release:
build_release(
version = "3.0.2",
type = "05deg_yearly",
nrow = 360,
ncol = 720,
crs = "epsg:4326",
extent = c(xmin = -180, xmax = 180, ymin = -90, ymax = 90),
temporal_resolution = "1 year",
start_date = as.Date("1850-12-31"),
end_date = as.Date("2026-08-26")
)build_release() calculates all variables to the custom
location, builds static and hive time-varying tables, copies the output
to releases/<version>/<type>/, and writes two
zip archives: priogrid_<ver>_<type>.zip and
priogrid_<ver>_<type>_csv.zip.
Retrieve a release config programmatically, or list all published releases:
cfg <- pg_release_config("3.0.2")
download_priogrid(list_releases = TRUE)Developer Utilities Reference
| Function | Purpose | Location |
|---|---|---|
Source |
R6 class: validate and serialise a new source | R/source_class.R |
add_source() |
Append validated source to sources.csv; write URL list
file |
R/utility.R |
priogrid:::pg_resolve_filenames() |
Query servers for filenames of opaque URLs | R/download_data.R |
priogrid:::pg_migrate_rawfiles() |
Rename downloaded files after explicit filename column is added | R/download_data.R |
priogrid:::pg_update_checksums() |
Recompute MD5s and rewrite pgchecksum.rda
|
R/download_data.R |
priogrid:::.pg_restamp_metatags() |
Re-stamp GDAL pg_* metatags without recomputing a
variable |
R/build_priogrid.R |
priogrid:::.pg_build_metatags() |
Build the metatag list from pgvariables for
save_pgvariable()
|
R/build_priogrid.R |
get_spatial_hash() /
get_temporal_hash()
|
6-char MD5 hashes that key output folders | R/build_priogrid.R |
resolve_pg_mode() |
Determine release vs. custom output path | R/build_priogrid.R |
pg_configure_terra_memory() |
Auto-enable on-disk processing for large rasters | R/config.R |
create_pg_indices() |
Create PRIOGRID cell indices | R/utility.R |
rast_to_df() |
Convert a SpatRaster to a data frame | R/utility.R |
pg_dates() |
Sequence of dates for a config | R/utility.R |
pg_date_intervals() |
Date intervals for a config | R/utility.R |
Functions marked priogrid::: lack @export;
call them with the triple-colon operator in dev mode
(devtools::load_all()).