Ingest SWOT + NMFS-DPS sea turtle ranges → global 0.05° cells

Published

2026-07-14 08:13:15

Refined sea-turtle ranges onto the global 0.05° cell grid, replacing the overly broad AquaMaps/IUCN turtle ranges. Range polygons come from SWOT global distributions; NMFS DPS boundaries mark the Endangered sub-populations.

Value = extinction-risk score via msens::compute_er_score() (never hard-coded). For the three species with DPS data (Loggerhead CC, Green CM, Olive Ridley LO): cells inside an Endangered DPS → compute_er_score("NMFS:EN") = 100, the rest of the range (Threatened) compute_er_score("NMFS:TN") = 50. The three pure-Endangered species (Leatherback DC, Hawksbill EI, Kemp’s Ridley LK): 100 throughout. The EN/TN split is done at the cell level (rasterize both, combine) — no fragile geometry intersection. Whole range, no land mask (turtles are marine, so these are ocean cells). mdl_key = "rng_turtle_swot_dps|{code}".

1 Design

Figure 1: SWOT ranges + NMFS DPS boundaries → cell-level EN/TN turtle ranges on the global 0.05° grid

2 Setup

Code
librarian::shelf(
  arrow, DBI, dplyr, duckdb, fs, glue, here, jsonlite, logger, readr, 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)

ds_key   <- "rng_turtle_swot_dps"
dir_swot <- glue("{dir_raw}/swot_seamap.env.duke.edu/swot_distribution")
dir_dps  <- glue("{dir_raw}/fisheries.noaa.gov/turtle-DPS_endangered")
dir_dist <- glue("{dir_big_v}/marine-atlas/dist/dataset={ds_key}")
manifest <- here("data/manifests/ingest_turtles_swot_dps.json")
dir_create(c(dir_dist, path_dir(manifest)))
stopifnot("run build_cell_grid.qmd first" = file_exists(cellid_tif), dir_exists(dir_swot))

# range value = extinction-risk score via msens::compute_er_score() (NEVER hard-code).
# Sea turtles are ESA-listed marine species (NMFS authority): EN=100, TN=50.
er_en <- compute_er_score("NMFS:EN")   # 100
er_tn <- compute_er_score("NMFS:TN")   # 50

swot_spp <- tribble(
  ~code, ~sci_name,                ~common_name,    ~has_dps, ~esa_pure,
  "CC",  "Caretta caretta",        "Loggerhead",    TRUE,     NA,
  "CM",  "Chelonia mydas",         "Green",         TRUE,     NA,
  "DC",  "Dermochelys coriacea",   "Leatherback",   FALSE,    "EN",
  "EI",  "Eretmochelys imbricata", "Hawksbill",     FALSE,    "EN",
  "LK",  "Lepidochelys kempii",    "Kemp's Ridley", FALSE,    "EN",
  "LO",  "Lepidochelys olivacea",  "Olive Ridley",  TRUE,     NA) |>
  mutate(mdl_key = mdl_key_raw(ds_key, code))
write_csv(swot_spp, glue("{dir_dist}/../model_{ds_key}.csv"))

3 Rasterize each range (EN=100 / TN=50) → one Parquet each

Code
if (redo_ingest && dir_exists(dir_dist)) { dir_delete(dir_dist); dir_create(dir_dist) }

for (i in seq_len(nrow(swot_spp))) {
  s      <- swot_spp[i, ]
  out_pq <- fs::path(dir_dist, s$code, ext = "parquet")
  if (file_exists(out_pq)) { log_info("{s$code}: skip (exists)"); next }

  swot <- st_read(glue("{dir_swot}/Global_Distribution_{s$code}.shp"), quiet = TRUE)
  if (s$has_dps) {
    dps     <- st_read(glue("{dir_dps}/{tolower(s$code)}_endangered.gpkg"), quiet = TRUE)
    d_range <- cells_from_ranges(swot, cellid_tif, value = er_tn)  # whole range = Threatened
    d_en    <- cells_from_ranges(dps,  cellid_tif, value = er_en)  # Endangered DPS areas
    d <- d_range |> mutate(val = if_else(cell_id %in% d_en$cell_id, er_en, er_tn))
  } else {
    d <- cells_from_ranges(swot, cellid_tif, value = er_en)        # pure Endangered
  }
  if (nrow(d) == 0) { log_warn("{s$code}: no cells"); next }
  msens::write_atlas_parquet(tibble(mdl_key = s$mdl_key, cell_id = d$cell_id, val = d$val), out_pq)
  log_info("{s$code} ({s$common_name}): {nrow(d)} cells")
}
Spherical geometry (s2) switched off
Spherical geometry (s2) switched on
Spherical geometry (s2) switched off
Spherical geometry (s2) switched on
Spherical geometry (s2) switched off
Spherical geometry (s2) switched on
Spherical geometry (s2) switched off
Spherical geometry (s2) switched on
Spherical geometry (s2) switched off
Spherical geometry (s2) switched on
Spherical geometry (s2) switched off
Spherical geometry (s2) switched on
Spherical geometry (s2) switched off
Spherical geometry (s2) switched on
Spherical geometry (s2) switched off
Spherical geometry (s2) switched on
Spherical geometry (s2) switched off
Spherical geometry (s2) switched on

4 Verify

Code
pq   <- dir_ls(dir_dist, glob = "*.parquet")
con  <- dbConnect(duckdb())
smry <- dbGetQuery(con, glue(
  "SELECT replace(mdl_key,'rng_turtle_swot_dps|','') code, count(*) n_cells,
          count(*) FILTER (WHERE val = 100) n_en, count(*) FILTER (WHERE val = 50) n_tn
     FROM read_parquet('{dir_dist}/*.parquet') GROUP BY mdl_key ORDER BY code"))
# 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 = "rng_turtle_swot_dps model_cell surface")
rng_turtle_swot_dps model_cell surface
code n_cells n_en n_tn
CC 20339916 10945504 9394412
CM 18244111 4461588 13782523
DC 24548372 24548372 0
EI 13410951 13410951 0
LK 1110528 1110528 0
LO 14954346 5845514 9108832

5 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_turtles_swot_dps", content_hash = h,
  stats = list(ds_key = ds_key, ver = ver, n_species = nrow(swot_spp),
               n_parquet = length(pq)),
  force = msens::force_target("ingest_turtles_swot_dps"))