Lean alternative to the 5-host provincial_run_runbook.md for running the 3
FWCP study areas (Peace / Fraser / Skeena) mapping_code parity. Reuses the
proven per-WSG build + cypher lifecycle but is tunnel-free (compare =
local bcfp snapshot, no :63333) and M1-as-dispatcher (no M4). Built for
link#175. Companion: provincial_run_runbook.md (shared mechanics),
RUNBOOK.md (the access/mapping_code machinery), data-raw/README.md.
cd ~/Projects/repo/link
# largest area on the dispatcher (fast/free M1); smaller areas on the cyphers
bash data-raw/study_area_run.sh \
--cy-workspaces=job1,job2 \
--focal=<Fraser focal csv> \ # -> dispatcher (M1)
--focal=<Peace focal csv> \ # -> cy1 (job1)
--focal=<Skeena focal csv> # -> cy2 (job2)Focal lists + study-area definitions: research/study_areas.md.
--focal count MUST equal 1 + N(--cy-workspaces); first = dispatcher, rest =
cyphers in order. Dispatcher-only (no cyphers): omit --cy-workspaces, pass one
--focal. Pre-req: dispatcher has fresh.streams_vw_bcfp
(snapshot_bcfp.sh --with-bcfp-views); branch pushed to origin (cyphers pull it).
- Pre-flight (tunnel-free): local fwapg up,
fresh.streams_vw_bcfppresent, doctl/tofu (only if cyphers). - Drainage-closed DS-first buckets (
study_area_wsgs.R): each focal set → its closure (every WSG it drains through, viapublic.wsg_outlet,f.outlet <@ w.outlet) ordered downstream-first (nlevel(outlet) ASC), then filtered to bundle-species presence (link#157). - Spin + prep cyphers (
cypher_up.sh,cypher_prep.shwithCYPHER_PREP_BRANCH=<dispatcher branch>). - Run each host's bucket DS-first (
wsg_run_one.R=lnk_pipeline_run(mapping_code=TRUE)), dispatcher local + cyphers via ssh, per-WSG soft-fail. - Consolidate cyphers → dispatcher (
schema_consolidate.R, shape-tolerant). - Burn cyphers (then a trap-EXIT safety net).
- Post-consolidate recompute ALL run WSGs on the dispatcher via
wsg_recompute_one.R→ [lnk_access()]merge=TRUE(cheap access-only, reuses persisted streams/habitat/barriers; ~10s/WSG) +lnk_mapping_code. Because it is cheap, every run WSG is re-settled — bucketing is a speed knob, not a correctness lever (link#205). - Compare all run WSGs tunnel-free (
study_area_compare.R) → CSV.
Each WSG's accessibility (hence its mapping_code token1 ACCESS/SPAWN/REAR and
token2 DAM/…) depends on whether a blocking barrier exists downstream —
possibly in a different WSG (the provincial-accumulation property,
RUNBOOK.md §5). When WSGs are distributed across machines, each machine holds
only its own bucket's barriers while it runs, so a WSG's access is computed
against an incomplete barrier set → wrong tokens.
Drainage-closed + DS-first bucketing is NOT sufficient on its own. It reduces divergence (downstream often persists first within a bucket) but does not eliminate it: downstream barriers can be cross-bucket, or arrive late in DS-first order. Caught 2026-05-25 — FINA 75.5% / PARA 68.6% per-host → both 99%+ after re-modelling on the full consolidated barrier set.
So the methodology is distribute (any bucketing) → consolidate → recompute →
compare, and the recompute is what makes it correct regardless of machine
count or WSG assignment. The recompute is lnk_access(merge=TRUE) +
lnk_mapping_code (link#205): cheap access-only, reusing the persisted
streams / habitat / barriers / barrier_overrides — no full pipeline, ~10s/WSG
(FINA: 11.86s wall vs ~90s full pipeline, identical bcfp parity). Two
non-obvious things had to be true for it to be cheap:
- AOI-scope the segments as a real table (with indexes +
ANALYZE), not a view — otherwise the planner picks the ~800k-row barriers as the outer driver and the join cost explodes by ~1000×. - Persist
streams/barriersneed ltree GIST/btree indexes (lnk_persist_initbuilds them; matchesfresh's working-table pattern). lnk_mapping_codemust filter access bywatershed_group_codewhen the table has that column (persist) — the originalid_segment IN (…)query is cartesian against persist becauseid_segmentis per-WSG, not globally unique (link#203).
- A per-WSG FATAL burns the cyphers with un-consolidated data. A
species-less closure WSG (LEUT) errored
No species resolved for AOI→|| exit 1→ driver FATAL → trapcypher_down→ an entire run's Peace+Skeena data gone. Fixes: species-presence filter the closure (link#157) AND per-WSG soft-fail (never abort a host before consolidate). The cyphers were NOT externally destroyed — the driver's own trap burned them. - Cyphers checkout
mainby default. The driver scripts + branch link only exist on the feature branch → cyphers must runCYPHER_PREP_BRANCH=<branch>, and the branch must be pushed first (cypher_prepdoesgit fetch origin && git reset --hard origin/$BRANCH). - Fresh-droplet sshd race.
cypher_upreturns when the IP is assigned, before sshd is up → scpConnection closed. Poll ssh before scp. - Wide-table shape drift across hosts.
streams_access/streams_mapping_codecarry one column per species; a host seeding persist fromparameters_fresh(11 sp) vscfg$species(8 sp) breaks the positional COPY-consolidate.cypher_prepnow usescfg$species;schema_consolidateCOPYs shared columns by name (link#204). - Cypher cost is ~$0.06/hr each. "Minimize idle" means don't leave them up for HOURS (the 2026-05-12 10-hr incident), not shave minutes. Don't over-engineer early-burn for cents.
| Script | Role |
|---|---|
data-raw/study_area_run.sh |
driver: pre-flight → spin → prep → run DS-first → consolidate → burn → compare |
data-raw/study_area_wsgs.R |
focal → drainage-closed, DS-first, species-filtered WSG list |
data-raw/wsg_run_one.R |
one WSG: lnk_pipeline_run(mapping_code=TRUE), local, host-agnostic |
data-raw/wsg_recompute_one.R |
one WSG cheap post-consolidate recompute (lnk_access(merge=TRUE) + lnk_mapping_code) — link#205. Sets statement_timeout/lock_timeout so a runaway/locked query fails fast |
data-raw/study_area_compare.R |
tunnel-free lnk_compare_mapping_code loop → CSV |
The tunnel-free M1-dispatch runner tripped over things the older wsgs_run_host.R
research/provincial_run_runbook.mdalready solved. The worst (2026-05-25): a species-less closure WSG (LEUT) errored "No species resolved for AOI" →|| exit 1→ driver FATAL → the trap'scypher_downburned the cyphers with their un-consolidated data — a whole run's Peace + Skeena gone, self-inflicted (the driver's own trap, not external destruction).
- Always species-filter the WSG set to bundle-species presence (link#157,
wsgs_run_host.R:88pattern) — closure pulls in unmodelable WSGs. - Per-WSG soft-fail; never abort a host before consolidate — one bad WSG must become a compare gap, not total data loss.
- Cyphers checkout
mainby default → passCYPHER_PREP_BRANCH=<branch>AND push the branch first (cypher_prepdoesgit reset --hard origin/$BRANCH). - Wait for sshd before scp to a fresh droplet (
cypher_upreturns pre-sshd). - Cyphers cost ~$0.06/hr each — "minimize idle" means don't leave them up for hours, not shave minutes; don't over-engineer early-burn.
- Read the records first (
RUNBOOK.md,research/provincial_run_runbook.md,data-raw/wsgs_run_host.R) before re-deriving orchestration.
Per-segment access (hence mapping_code) depends on downstream barriers, possibly in another WSG (provincial accumulation, RUNBOOK §5). Distributed hosts each see only their own bucket's barriers mid-run → incomplete → wrong tokens. Drainage-closed + DS-first per-host is NOT sufficient (it only reduces divergence): 2026-05-25 had FINA 75.5% / PARA 68.6% per-host → 99%+ only after re-modelling on the consolidated barrier set. So recompute the diverged WSGs on the dispatcher post-consolidate. Bucketing is a speed knob, not a correctness lever. Authoritative result: median 99.66%; genuine divergences SETN salmon ~94%, UNRS BT 61.8%. The full-pipeline recompute is ~2× on diverged WSGs; a cheap access-only recompute (#205) makes recompute-all bulletproof + ~1×.