Build global 0.05° cell grid (v8 sampling unit)
Global ocean 0.05° raster cells in [-180,180] with env covariates + US study-area / Program-Area membership
The v8 spatial sampling unit is the global 0.05° raster cell in [-180,180] (rolled back from H3 hex). The grid is anchored to the shared 0.05°, 3600×7200, EPSG:4326 topology of Bio-Oracle v3 AND the AquaX SDM TIFs — so cell_id is the raster cell index (1:ncell), and any SDM raster on this grid (Bio-Oracle, AquaX, resampled AquaMaps) maps to cell_id by position with no projection.
This notebook builds the canonical cell table (ocean cells only) — cell_id, the 12 Bio-Oracle env covariates, area_km2, and boolean in_usa / in_pra membership — plus the global cell-id COG that titiler paints (and that ingest_aquamaps.qmd uses to build its bilinear weight table).
The global build (631 MB Bio-Oracle → 25.9M cells → ~17M-row cell table + 47 MB COG) is skipped when the cell-id COG already exists (resume); force it with REDO_CELL_GRID=1 (libs/vars.R). A bbox prototype run (CELL_BBOX=...) always rebuilds into its own CELL_DB.
1 Design
Code
librarian::shelf(DBI, dplyr, duckdb, glue, here, jsonlite, sf, terra, quiet = TRUE)
source(here("libs/paths.R")) # ver, sdm_db, bio_oracle_tif, cellid_tif, ply_usa_gpkg, ply_pra_gpkg
source(here("libs/vars.R")) # redo_cell_grid
stopifnot(file.exists(bio_oracle_tif), file.exists(ply_usa_gpkg), file.exists(ply_pra_gpkg))
# optional bbox restriction for prototype runs: CELL_BBOX="xmin,xmax,ymin,ymax"
cell_bbox <- Sys.getenv("CELL_BBOX", "")
cell_db <- Sys.getenv("CELL_DB", sdm_db)
dir.create(dirname(cell_db), recursive = TRUE, showWarnings = FALSE)
dir.create(here("data/manifests"), recursive = TRUE, showWarnings = FALSE)
# skip the expensive global build if the cell-id COG already exists (resume),
# unless forced; a bbox prototype always rebuilds into its own CELL_DB
need_build <- redo_cell_grid || nzchar(cell_bbox) || !file.exists(cellid_tif)2 Read Bio-Oracle, assign global cell_id, membership + area
Code
if (need_build) {
r <- rast(bio_oracle_tif) # global [-180,180], 3600 x 7200, 12 env layers
r$cell_id <- 1:ncell(r) # global raster-position id (shared with AquaX/AquaMaps grids)
r$area_km2 <- cellSize(r$depth_mean, unit = "km")
# membership: rasterize the study-area + Program-Area polygons onto the same grid
r$in_usa <- rasterize(vect(st_read(ply_usa_gpkg, quiet = TRUE)), r$depth_mean, field = 1, background = 0)
r$in_pra <- rasterize(vect(st_read(ply_pra_gpkg, quiet = TRUE)), r$depth_mean, field = 1, background = 0)
if (nzchar(cell_bbox)) {
bb <- as.numeric(strsplit(cell_bbox, ",")[[1]])
r <- crop(r, ext(bb[1], bb[2], bb[3], bb[4]))
message(glue("restricted to bbox [{cell_bbox}]"))
}
}3 Write cell table (ocean cells) + global cell-id COG
Code
if (need_build) {
# ocean cells only (depth_mean non-NA); cell_id stays the global raster index
d_cell <- as.data.frame(r, xy = TRUE, na.rm = FALSE) |>
filter(!is.na(depth_mean)) |>
transmute(
cell_id = as.integer(cell_id),
lon = x, lat = y,
across(c(depth_mean, depth_min, depth_max, oxy_b_mean, oxy_mean, prim_prod_mean,
ice_con_ann, salinity_b_mean, salinity_mean, sbt_an_mean, sst_an_mean, fao_area_m)),
area_km2,
in_usa = in_usa == 1,
in_pra = in_pra == 1)
con <- dbConnect(duckdb(cell_db))
dbWriteTable(con, "cell", d_cell, overwrite = TRUE)
dbExecute(con, "ALTER TABLE cell ADD PRIMARY KEY (cell_id)")
dbDisconnect(con, shutdown = TRUE)
# global cell-id COG in [-180,180] for titiler (uint32; no overviews to keep ids intact)
writeRaster(r$cell_id, cellid_tif, filetype = "COG", datatype = "INT4U", overwrite = TRUE,
gdal = c("COMPRESS=LZW", "OVERVIEWS=NONE"))
} else {
message(glue("cell grid up to date (cellid_tif exists); set REDO_CELL_GRID=1 to rebuild"))
}cell grid up to date (cellid_tif exists); set REDO_CELL_GRID=1 to rebuild
4 Summary + manifest
Code
con <- dbConnect(duckdb(cell_db, read_only = TRUE))
smry <- dbGetQuery(con, "
SELECT count(*) AS n_cell,
count(*) FILTER (WHERE in_usa) AS n_usa,
count(*) FILTER (WHERE in_pra) AS n_pra,
round(min(area_km2), 3) AS area_min_km2,
round(max(area_km2), 3) AS area_max_km2,
round(min(lon),1) AS lon_min, round(max(lon),1) AS lon_max,
count(*) FILTER (WHERE depth_mean IS NULL) AS n_null_depth
FROM cell")
# content fingerprint of the cell table (before disconnect)
h <- msens::hash_query(con, "cell")
dbDisconnect(con, shutdown = TRUE)
msens::report_table(smry, caption = "global 0.05° cell grid summary")| n_cell | n_usa | n_pra | area_min_km2 | area_max_km2 | lon_min | lon_max | n_null_depth |
|---|---|---|---|---|---|---|---|
| 17072105 | 634208 | 343576 | 0.014 | 30.773 | -180 | 180 | 0 |
Code
# content-addressed manifest: deterministic (no wall-clock, no machine paths) ->
# downstream targets re-run only when the cell table's content actually changes
msens::write_manifest(
here("data/manifests/build_cell_grid.json"), target = "build_cell_grid", content_hash = h,
stats = c(as.list(smry), list(version = ver, bbox = cell_bbox,
source = basename(bio_oracle_tif))),
force = msens::force_target("build_cell_grid"))
message(glue("cell grid: {smry$n_cell} cells ({smry$n_usa} in US, {smry$n_pra} in PRAs) -> {cell_db}"))cell grid: 17072105 cells (634208 in US, 343576 in PRAs) -> ~/_big/msens/derived/v8/sdm.duckdb
