Backfill a past release into the multi-version atlas

Published

2026-08-10

Every release v1–v7 exists only as a monolithic sdm.duckdb on the server. To render one in the current app it needs what v8 has: app-facing tables as Parquet, a mdl_seq ↔︎ mdl_key crosswalk, COGs for its surfaces, and a manifest.json describing all of it.

This does that for one release, parameterized by ver, so the same notebook backfills each in turn.

Two halves, deliberately separated. The tables + crosswalk + manifest are cheap (minutes) and are what make a version discoverable. The COG build is expensive — ~30,000 rasters from a 1.18-billion-row surface — and is gated behind BACKFILL_COGS=1. That box also serves the live apps on 4 cores, so the heavy half should be run deliberately, not as a side effect.

1 What makes this affordable

The content-addressed store means a release only pays for surfaces nobody has published yet. Measured across v3/v5/v6/v7: 120,974 model-rows collapse to 19,766 unique surfaces (6.12×), and v6↔︎v7 is 30,061/30,061 identical. So the first usa05 backfill is the expensive one and each subsequent release is mostly hashing.

2 Setup

Code
librarian::shelf(arrow, DBI, dplyr, duckdb, glue, knitr, logger, terra,
                 MarineSensitivity/msens, quiet = TRUE)
source(here::here("libs/paths.R"))

bver     <- params$ver
do_cogs  <- Sys.getenv("BACKFILL_COGS") != ""
test_n   <- suppressWarnings(as.integer(Sys.getenv("BACKFILL_TEST_N", "")))
n_work   <- as.integer(Sys.getenv("BACKFILL_WORKERS", "2"))

grid_id  <- msens::grid_for_ver(bver)
# msens::sdm_db_path() knows where each release's database actually is, per
# platform -- swapping the version segment of the configured `sdm_db` does NOT:
# on the server that resolves to derived/{ver}/, while the release databases
# live under big/{ver}/.
db       <- path.expand(msens::sdm_db_path(bver))
dir_v    <- path.expand(dirname(db))
dir_tbl  <- file.path(dir_v, "tables")
dir_cog  <- file.path(dir_v, "backfill_cogs")
dir.create(dir_tbl, recursive = TRUE, showWarnings = FALSE)

stopifnot("release database not found" = file.exists(db))
log_info("backfilling {bver} (grid {grid_id}) from {db}; cogs={do_cogs}")

con <- dbConnect(duckdb(), db, read_only = TRUE)
val <- if ("val" %in% dbListFields(con, "model_cell")) "val" else "value"   # v8 vs v1-v7
log_info("model_cell value column: {val}")

3 The mdl_seq ↔︎ mdl_key crosswalk

mdl_seq is an autoincrement that renumbers on every rebuild, so a v6 deep link resolves to a different species in v7 — which is why URLs had to move to the stable mdl_key. The old model table happens to carry everything needed to mint one: ds_key plus taxa, which is the source species id (am_0.05 / ITS-Mam-180528). So the crosswalk is mechanical, not hand-built.

Code
# MERGED models need the taxon's identity, not the model row's `taxa`: v7 stores
# "ms_merge:430645" there, which would mint ms_merge|ms_merge:430645 and match
# nothing. The merged key is ms_merge|{AUTHORITY}:{taxon_id} -- so a v7 merged
# model and a v8 one for the SAME species share a key, which is the whole point.
model <- dbGetQuery(con, "
  SELECT m.mdl_seq, m.ds_key, m.taxa, t.taxon_authority, t.taxon_id
  FROM model m LEFT JOIN taxon t ON t.mdl_seq = m.mdl_seq")

# v1-v7 spell AquaMaps 'am_0.05'; the mdl_key grammar uses 'am'
norm_ds <- function(x) sub("^am_0\\.05$", "am", x)

is_merged <- model$ds_key == "ms_merge"
xwalk <- model |>
  mutate(ds_key  = norm_ds(ds_key),
         mdl_key = ifelse(
           is_merged & !is.na(taxon_authority),
           msens::mdl_key_merged(ifelse(is.na(taxon_authority), "WORMS", taxon_authority),
                                 taxon_id),
           msens::mdl_key_raw(ds_key, taxa)))

# a merged model with no taxon row cannot be keyed; report rather than mint a
# plausible-looking key that resolves to nothing
n_orphan <- sum(is_merged & is.na(model$taxon_authority))
if (n_orphan) log_warn("{n_orphan} merged model(s) have no taxon row - keyed from `taxa` instead")
stopifnot("mdl_key collision" = !any(duplicated(xwalk$mdl_key)))

log_info("{nrow(xwalk)} models crosswalked")
knitr::kable(head(xwalk, 5))
mdl_seq ds_key taxa taxon_authority taxon_id mdl_key
341 am Fis-34805 worms 158854 am|Fis-34805
346 am Fis-32351 worms 1525496 am|Fis-32351
348 am Fis-139575 worms 126616 am|Fis-139575
349 am Fis-26541 worms 158890 am|Fis-26541
353 am Fis-25151 worms 1525960 am|Fis-25151

4 Content-hash every surface

Code
t0 <- Sys.time()
h  <- msens::content_hashes(con, "model_cell", by = "mdl_seq", cols = c("cell_id", val))
log_info("hashed {nrow(h)} models in {round(as.numeric(difftime(Sys.time(), t0, units='secs')))}s")

ENC <- "int1u-nd0-noovr"   # encoding is part of the object identity; see content_hash_encoded()
h <- h |>
  mutate(payload_hash = content_hash,
         content_hash = msens::content_hash_encoded(content_hash, ENC)) |>
  left_join(xwalk, by = "mdl_seq") |>
  mutate(cog_url = msens::content_url(grid_id, content_hash))

store <- tryCatch(msens::cog_store_index(grid_id), error = function(e) character())
log_info("{length(store)} object(s) already in cog/{grid_id}/; {sum(!h$content_hash %in% store)} new")

knitr::kable(
  h |> summarise(models = n(), unique_surfaces = n_distinct(content_hash),
                 already_published = sum(content_hash %in% store)))
models unique_surfaces already_published
30061 19752 0

5 Build the COGs (gated)

Code
built <- 0L
if (do_cogs) {
  dir.create(dir_cog, recursive = TRUE, showWarnings = FALSE)
  grid <- msens::grid_spec_for(grid_id)

  todo <- h |> distinct(content_hash, .keep_all = TRUE) |> filter(!content_hash %in% store)
  if (!is.na(test_n) && test_n > 0) { todo <- head(todo, test_n); log_warn("TEST_N={test_n}") }
  log_info("{nrow(todo)} raster(s) to build")

  for (i in seq_len(nrow(todo))) {
    r   <- todo[i, ]
    out <- file.path(dir_cog, basename(msens::content_key(grid_id, r$content_hash)))
    if (file.exists(out)) next
    d <- dbGetQuery(con, glue("SELECT cell_id, {val} AS val FROM model_cell WHERE mdl_seq = {r$mdl_seq}"))
    if (nrow(d)) {
      msens::publish_cog(d$cell_id, d$val, out, grid,
                         datatype = "INT1U", nodata = 0, overview = FALSE)
      built <- built + 1L
    }
    if (i %% 500 == 0) log_info("  {i}/{nrow(todo)}")
  }
  log_info("built {built} COG(s), {round(sum(file.info(list.files(dir_cog, full.names=TRUE))$size, na.rm=TRUE)/1024^2)} MB")
} else {
  log_info("BACKFILL_COGS unset - skipping the expensive half")
}

6 Export the app-facing tables

Code
want <- c("cell", "taxon", "model", "metric", "cell_metric", "zone", "zone_cell",
          "zone_metric", "zone_taxon", "dataset", "taxon_model", "listing")
have <- intersect(want, dbListTables(con))

sizes <- vapply(have, function(t) {
  d <- dbGetQuery(con, glue("SELECT * FROM {t}"))
  f <- file.path(dir_tbl, paste0(t, ".parquet"))
  arrow::write_parquet(d, f, compression = "zstd")
  nrow(d)
}, numeric(1))

# the crosswalk ships as a table of its own: it is what lets a legacy
# ?mdl_seq= link resolve, and what a merged COG href hangs off
model_asset <- h |>
  transmute(mdl_key, mdl_seq, ds_key, sp_id = taxa, n_cells = n,
            content_hash, cog_url, grid_id = grid_id, ver = bver)
arrow::write_parquet(model_asset, file.path(dir_tbl, "model_asset.parquet"), compression = "zstd")

knitr::kable(data.frame(table = c(have, "model_asset"), rows = c(sizes, nrow(model_asset))),
             caption = glue("{bver} tables exported to {dir_tbl}"))
v7 tables exported to /share/data/big/v7/tables
table rows
cell cell 662075
taxon taxon 17561
model model 32315
metric metric 43
cell_metric cell_metric 21961492
zone zone 73
zone_cell zone_cell 5214822
zone_metric zone_metric 1021
zone_taxon zone_taxon 126835
dataset dataset 9
taxon_model taxon_model 31690
listing listing 718
model_asset 30061

7 Manifest

Code
versions <- readr::read_csv(here::here("data/versions.csv"), show_col_types = FALSE)
v_row    <- versions |> filter(ver == bver)
stopifnot("version missing from data/versions.csv" = nrow(v_row) == 1)

mfst <- msens::manifest_build(
  con, bver,
  status = v_row$status,
  base   = msens::atlas_base_url(),
  # COGs are declared only once they exist; until then the app must not offer a
  # layer it cannot draw
  capabilities = list(score_cogs = FALSE, model_cogs = built > 0 || length(store) > 0),
  extra = list(title = v_row$title, released = as.character(v_row$released),
               models = glue("{msens::atlas_base_url()}/{bver}/tables/model_asset.parquet")))

f_mfst <- file.path(dir_v, "manifest.json")
writeLines(as.character(jsonlite::toJSON(mfst, auto_unbox = TRUE, pretty = TRUE, na = "null")), f_mfst)
msens::validate_manifest(jsonlite::fromJSON(f_mfst), ver = bver)
$ver
[1] "v7"

$status
[1] "released"

$grid_id
[1] "usa05"

$id_field
[1] "mdl_seq"

$capabilities
$capabilities$cell_species_list
[1] TRUE

$capabilities$native_representation
[1] FALSE

$capabilities$programareas
[1] TRUE

$capabilities$planareas
[1] TRUE

$capabilities$zone_taxon
[1] TRUE

$capabilities$score_cogs
[1] FALSE

$capabilities$model_cogs
[1] FALSE


$tables
$tables$cell
[1] "https://s3.us-east-1.amazonaws.com/oceanmetrics.io-public/marine-atlas/v7/tables/cell.parquet"

$tables$taxon
[1] "https://s3.us-east-1.amazonaws.com/oceanmetrics.io-public/marine-atlas/v7/tables/taxon.parquet"

$tables$model
[1] "https://s3.us-east-1.amazonaws.com/oceanmetrics.io-public/marine-atlas/v7/tables/model.parquet"

$tables$metric
[1] "https://s3.us-east-1.amazonaws.com/oceanmetrics.io-public/marine-atlas/v7/tables/metric.parquet"

$tables$cell_metric
[1] "https://s3.us-east-1.amazonaws.com/oceanmetrics.io-public/marine-atlas/v7/tables/cell_metric.parquet"

$tables$zone
[1] "https://s3.us-east-1.amazonaws.com/oceanmetrics.io-public/marine-atlas/v7/tables/zone.parquet"

$tables$zone_cell
[1] "https://s3.us-east-1.amazonaws.com/oceanmetrics.io-public/marine-atlas/v7/tables/zone_cell.parquet"

$tables$zone_metric
[1] "https://s3.us-east-1.amazonaws.com/oceanmetrics.io-public/marine-atlas/v7/tables/zone_metric.parquet"

$tables$zone_taxon
[1] "https://s3.us-east-1.amazonaws.com/oceanmetrics.io-public/marine-atlas/v7/tables/zone_taxon.parquet"

$tables$dataset
[1] "https://s3.us-east-1.amazonaws.com/oceanmetrics.io-public/marine-atlas/v7/tables/dataset.parquet"

$tables$taxon_model
[1] "https://s3.us-east-1.amazonaws.com/oceanmetrics.io-public/marine-atlas/v7/tables/taxon_model.parquet"

$tables$listing
[1] "https://s3.us-east-1.amazonaws.com/oceanmetrics.io-public/marine-atlas/v7/tables/listing.parquet"


$metrics
                                                   metric_key
1                                                    primprod
2                                 primprod_ecoregion_rescaled
3                                                extrisk_bird
4                                               extrisk_coral
5                                                extrisk_fish
6                                        extrisk_invertebrate
7                                              extrisk_mammal
8                                               extrisk_other
9                                              extrisk_turtle
10                            extrisk_bird_ecoregion_rescaled
11                           extrisk_coral_ecoregion_rescaled
12                            extrisk_fish_ecoregion_rescaled
13                    extrisk_invertebrate_ecoregion_rescaled
14                          extrisk_mammal_ecoregion_rescaled
15                           extrisk_other_ecoregion_rescaled
16                          extrisk_turtle_ecoregion_rescaled
17 score_extriskspcat_primprod_ecoregionrescaled_equalweights
                                                                                                                                                                                                                                                                                                                      description
1  Primary productivity: Oregon State Vertically Generalized Production Model (VGPM) from Visible Infrared Imaging Radiometer Suite (VIIRS) satellite data (mg C / m^2 / day) from daily averages available as monthly averaged to annual and averaged to overall for the most recently available full years of data 2014 to 2023
2                                                                                                                                                                                                                                                    Primary productivity rescaled to [0,100] based on Ecoregional min/max values
3                                                                                                                                                                                                                                                                                                        Extinction risk for bird
4                                                                                                                                                                                                                                                                                                       Extinction risk for coral
5                                                                                                                                                                                                                                                                                                        Extinction risk for fish
6                                                                                                                                                                                                                                                                                                Extinction risk for invertebrate
7                                                                                                                                                                                                                                                                                                      Extinction risk for mammal
8                                                                                                                                                                                                                                                                                                       Extinction risk for other
9                                                                                                                                                                                                                                                                                                      Extinction risk for turtle
10                                                                                                                                                                                                                                              Extinction risk for bird, rescaled to [0,100] based on Ecoregional min/max values
11                                                                                                                                                                                                                                             Extinction risk for coral, rescaled to [0,100] based on Ecoregional min/max values
12                                                                                                                                                                                                                                              Extinction risk for fish, rescaled to [0,100] based on Ecoregional min/max values
13                                                                                                                                                                                                                                      Extinction risk for invertebrate, rescaled to [0,100] based on Ecoregional min/max values
14                                                                                                                                                                                                                                            Extinction risk for mammal, rescaled to [0,100] based on Ecoregional min/max values
15                                                                                                                                                                                                                                             Extinction risk for other, rescaled to [0,100] based on Ecoregional min/max values
16                                                                                                                                                                                                                                            Extinction risk for turtle, rescaled to [0,100] based on Ecoregional min/max values
17                                                                                                                                                   Combined score of extinction risk per species category and primary productivity, equally weighted (and each previously rescaled [0,100] based on Ecoregional min/max values)

$zones
                       tbl             fld  n
1      ply_ecoregions_2025   ecoregion_key 12
2       ply_planareas_2025    planarea_key 36
3 ply_programareas_2026_v7 programarea_key 20
4   ply_subregions_2026_v7   subregion_key  5

$title
[1] "Validity decoupled from Program Areas"

$released
[1] "2026-06-12"

$models
[1] "https://s3.us-east-1.amazonaws.com/oceanmetrics.io-public/marine-atlas/v7/tables/model_asset.parquet"
Code
dbDisconnect(con, shutdown = TRUE)

cat(glue("{bver}: id_field={mfst$id_field} grid={mfst$grid_id} tables={length(mfst$tables)} ",
         "metrics={nrow(mfst$metrics)} zones={nrow(mfst$zones)}\n"))
v7: id_field=mdl_seq grid=usa05 tables=12 metrics=17 zones=4

8 Target manifest

Code
msens::write_manifest(
  here::here("data/manifests/backfill_versions.json"),
  target       = "backfill_versions",
  content_hash = digest::digest(list(bver, h$content_hash), algo = "xxhash64"),
  stats = list(ver = bver, grid_id = grid_id, n_models = nrow(h),
               n_unique_surfaces = length(unique(h$content_hash)),
               n_cogs_built = built, n_tables = length(have)),
  force = msens::force_target("backfill_versions"))