Ingest BirdLife BOTW → global 0.05° cells (whole range)
Rasterize each bird species’ BirdLife BOTW range onto the global 0.05° cell grid and write one Parquet per species, keyed by mdl_key = "bl|{sisid}".
Whole range, no land mask. A bird’s global home range is largely terrestrial, so v8 captures the entire distribution (land + ocean) — a departure from prior versions that masked to the ocean. msens::cells_from_ranges() (exactextractr) is fast and tolerant of BOTW’s messy geometry (self-touching / MULTISURFACE loops), using extant polygons (presence ∈ {1,2,3}). The val is the IUCN extinction-risk score via msens::compute_er_score("IUCN:{code}") (never hard-coded; CR=50, EN=25, VU=5, NT=2, LC/DD=1) — the Red List category normalized to that vocabulary. How marine a range is (pct_marine) is a derived metric computed at merge, where the cell table’s is_ocean joins cleanly for every dataset — not a filter here.
Parallelized with parallel::mclapply (fork); resumable (skip species whose Parquet exists). REDO_INGEST=1 rebuilds all.
1 Design
2 Setup
Code
librarian::shelf(
arrow, DBI, dplyr, duckdb, fs, glue, here, jsonlite, logger, readr, RSQLite, sf,
terra, tibble, MarineSensitivity/msens, quiet = T)
source(here("libs/paths.R")) # dir_raw, dir_big_v, cellid_tif, ver
source(here("libs/vars.R")) # redo_ingest
options(readr.show_col_types = F)
# BOTW stores some ranges as curved MULTISURFACE, which GEOS's st_make_valid cannot
# handle (it errored/hung on ~159 wide-ranging birds). Linearize curves -> MULTIPOLYGON
# on read so cells_from_ranges can rasterize them.
Sys.setenv(OGR_STROKE_CURVE = "TRUE")
ds_key <- "bl"
# prefer a fast LOCAL copy off Google Drive — per-species queries throttle badly on Drive
botw_gpkg <- {
f_local <- path.expand("~/_big/msens/raw/BOTW_2024_2.gpkg")
if (file.exists(f_local)) f_local else glue("{dir_raw}/birdlife.org/BOTW_GPKG_2024_2/BOTW_2024_2.gpkg")
}
dir_dist <- glue("{dir_big_v}/marine-atlas/dist/dataset={ds_key}")
manifest <- here("data/manifests/ingest_birdlife_botw.json")
n_workers <- max(1L, parallel::detectCores())
dir_create(c(dir_dist, path_dir(manifest)))
stopifnot(
"run build_cell_grid.qmd first (need cellid_tif)" = file_exists(cellid_tif),
"BOTW gpkg not found" = file_exists(botw_gpkg))
# ensure sisid is indexed so the ~11k per-species queries are fast (0.03s vs a 10.9s
# full scan of the 17,379-row all_species table — 300x)
if (file.access(botw_gpkg, mode = 2) == 0) { # writable copy
con_gpkg <- dbConnect(RSQLite::SQLite(), botw_gpkg)
try(dbExecute(con_gpkg, "CREATE INDEX IF NOT EXISTS idx_all_species_sisid ON all_species(sisid)"),
silent = TRUE)
dbDisconnect(con_gpkg)
}3 Species list + model crosswalk
One model per sisid (extant polygons unioned at rasterize time); skip extinct (EX). IUCN category from the checklist is carried as merge metadata.
Code
a_attr <- st_read(botw_gpkg, quiet = TRUE,
query = "SELECT DISTINCT sisid, sci_name FROM all_species WHERE presence IN (1,2,3)")
chk <- st_read(botw_gpkg, quiet = TRUE,
query = "SELECT ScientificName AS sci_name, FamilyName AS family, CommonName AS common,
IUCN_RedList_Category_2024 AS iucn
FROM main_BL_HBW_Checklist_V9") |> st_drop_geometry()
# normalize the IUCN Red List category to compute_er_score()'s vocabulary
# (CR/EN/VU/NT/LC/DD): "CR (PE)"/"CR (PEW)" -> CR; drop extinct (EX/EW); blank -> LC.
# Value = the IUCN extinction-risk score (never hard-coded); merge may add MBTA etc.
sp <- a_attr |> st_drop_geometry() |>
left_join(chk, by = "sci_name") |>
mutate(iucn = toupper(trimws(iucn))) |>
filter(is.na(iucn) | !iucn %in% c("EX", "EW")) |>
mutate(
code = case_when(
is.na(iucn) | iucn == "" ~ "LC",
grepl("^CR", iucn) ~ "CR",
iucn %in% c("EN","VU","NT","LC","DD") ~ iucn,
TRUE ~ "LC"),
er_code = paste0("IUCN:", code),
er_score = compute_er_score(er_code),
mdl_key = mdl_key_raw(ds_key, sisid)) |>
arrange(sci_name)
stopifnot(!any(duplicated(sp$sisid)))
write_csv(sp, glue("{dir_dist}/../model_{ds_key}.csv"))
nrow(sp)[1] 10995
4 Rasterize each range → one Parquet each
Code
if (redo_ingest && dir_exists(dir_dist)) { dir_delete(dir_dist); dir_create(dir_dist) }
done <- as.integer(path_ext_remove(path_file(dir_ls(dir_dist, glob = "*.parquet"))))
todo <- sp |> filter(!sisid %in% done)
log_info("{ds_key}: {length(done)} done; rasterizing {nrow(todo)} of {nrow(sp)} ranges on {n_workers} cores ...")
one <- function(i) {
sisid <- todo$sisid[i]
out_pq <- fs::path(dir_dist, sisid, ext = "parquet")
p <- tryCatch(sf::st_read(botw_gpkg, quiet = TRUE, query = sprintf(
"SELECT sisid, geom FROM all_species WHERE sisid = %d AND presence IN (1,2,3)", sisid)),
error = function(e) NULL)
if (is.null(p) || nrow(p) == 0) return("empty")
d <- tryCatch(cells_from_ranges(p, cellid_tif, value = todo$er_score[i]),
error = function(e) NULL)
if (is.null(d) || nrow(d) == 0) return("empty")
msens::write_atlas_parquet(
tibble::tibble(mdl_key = todo$mdl_key[i], cell_id = d$cell_id, val = d$val), out_pq)
"done"
}
res <- unlist(parallel::mclapply(seq_len(nrow(todo)),
function(i) tryCatch(one(i), error = function(e) paste0("error: ", conditionMessage(e))),
mc.cores = n_workers, mc.preschedule = FALSE))
print(table(res))res
done
10995
5 Verify
Code
pq <- dir_ls(dir_dist, glob = "*.parquet")
con <- dbConnect(duckdb())
smry <- dbGetQuery(con, glue(
"SELECT count(DISTINCT mdl_key) n_models, count(*) n_cells, max(val) v_max
FROM read_parquet('{dir_dist}/*.parquet')"))
# order-independent content fingerprint of the on-disk surface (not the ingest run)
h <- msens::hash_parquet(glue("{dir_dist}/*.parquet"), con)
dbDisconnect(con, shutdown = TRUE)
msens::report_table(smry, caption = "bl model_cell surface")| n_models | n_cells | v_max |
|---|---|---|
| 10995 | 1693299770 | 50 |
6 Manifest (target output)
Code
# content-addressed manifest: deterministic, no wall-clock, no machine paths ->
# downstream targets re-run only when this surface's content actually changes
msens::write_manifest(
manifest, target = "ingest_birdlife_botw", content_hash = h,
stats = list(ds_key = ds_key, ver = ver, n_species = nrow(sp),
n_models = smry$n_models, n_cells = smry$n_cells,
v_max = smry$v_max, n_parquet = length(pq),
source = path_file(botw_gpkg)),
force = msens::force_target("ingest_birdlife_botw"))