From d024b2c4e66efabebc064d3c922e53e3f664e30b Mon Sep 17 00:00:00 2001 From: almac2022 Date: Wed, 1 Jul 2026 16:55:12 -0700 Subject: [PATCH 01/16] Initialize PWF baseline for #221 Per-WSG accessible_km roll-up + parity compare. Coho proof validated live (MORR 0.09%, BULK 0.27% vs tunnel-free fresh.streams_vw_bcfp). Snapshot barrier columns are comma-joined text, so accessible predicate is = '', not = array[]::text[]. Co-Authored-By: Claude Opus 4.7 --- planning/active/findings.md | 56 ++++++++++++++++++++++++++++++++++++ planning/active/progress.md | 14 +++++++++ planning/active/task_plan.md | 55 +++++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 planning/active/findings.md create mode 100644 planning/active/progress.md create mode 100644 planning/active/task_plan.md diff --git a/planning/active/findings.md b/planning/active/findings.md new file mode 100644 index 00000000..de9c2e4e --- /dev/null +++ b/planning/active/findings.md @@ -0,0 +1,56 @@ +# Findings — Per-WSG habitat/access km roll-up (accessible_km) (#221) + +## Premise verified live (2026-07-01, local docker fwapg localhost:5432) + +- `fresh.streams_vw_bcfp` exposes `barriers_ch_cm_co_pk_sk_dnstr`, + `barriers_bt_dnstr`, `barriers_st_dnstr`, `barriers_wct_dnstr`, + `barriers_ct_dv_rb_dnstr`, `barriers_dams_dnstr`, `barriers_pscis_dnstr`, + `barriers_anthropogenic_dnstr`, `length_metre`, `spawning_`, `rearing_`, + `watershed_group_code`, `segmented_stream_id`. + +## Predicate correction to the issue body + +The `barriers__dnstr` columns in `fresh.streams_vw_bcfp` are stored as +`character varying` (comma-joined string), **not** `text[]`. `= array[]::text[]` +errors (`operator does not exist: character varying = text[]`). The accessible +predicate is `barriers_ch_cm_co_pk_sk_dnstr = ''` — empty string means no +barrier downstream. In MORR, the empty-string bucket holds 12,485 segments; every +other value is a barrier id / comma list. + +## Coho accessible_km proof (validated) + +- link side: `fresh.streams s JOIN fresh.streams_access a ON + s.id_segment=a.id_segment AND s.watershed_group_code=a.watershed_group_code` + (full PK, #203), `WHERE a.access_co IN (1,2)`, `sum(s.length_metre)/1000`. +- ref side: `fresh.streams_vw_bcfp WHERE barriers_ch_cm_co_pk_sk_dnstr = ''`, + `sum(length_metre)/1000`. +- Result (only WSGs with local `streams_access`): **MORR 0.09%, BULK 0.27%** — + both far inside ≤5%. + +## Per-species vs salmon-group reconciliation + +link models `access_co` per species; bcfp models CO inside the salmon group +`barriers_ch_cm_co_pk_sk_dnstr` (shared barrier table — a barrier that blocks CH +but not CO cannot exist in bcfp). The ≤0.27% agreement **empirically confirms** +they agree for CO on the tested WSGs. Watch for divergence if `blocks_species` +encoding (link#152/#200 per-species barrier views) drifts from bcfp's shared +salmon group at scale. + +## Plan-agent review (sonnet) — resolved items + +- B1/A1 (unverified snapshot columns) → resolved: columns confirmed present; only + the array-vs-text predicate needed correction. +- B2 (length join) → the draft SQL already joins `streams` for `length_metre`; + correct. +- O2 / row-count test breakage → captured as an explicit Phase 2 task. +- AC1 (single-WSG) → proof runs over all locally-available WSGs (2 today). +- AC2 (untestable ad-hoc SQL) → Phase 1 deliverable is a reproducible + `data-raw/` script with a hard ≤5% assertion. + +## Issue context + +Issue #221 body: goal is per-WSG length roll-up emitting accessible_km + +spawning_km + rearing_km per (WSG, species), tunnel-free reference, coho-first, +abstract into a reusable function, extend to all bcfp species, then a separate +MORR vignette. Relates to #175 (parity methodology), #204 (persist shape). bcfp +ref: `smnorris/bcfishpass@2917790` (archived `wsg_linear_summary.sql`). diff --git a/planning/active/progress.md b/planning/active/progress.md new file mode 100644 index 00000000..54b568bb --- /dev/null +++ b/planning/active/progress.md @@ -0,0 +1,14 @@ +# Progress — Per-WSG habitat/access km roll-up (accessible_km) (#221) + +## Session 2026-07-01 + +- Plan-mode exploration of `lnk_compare_rollup.R`, `lnk_compare_wsg.R`, + `lnk_pipeline_access.R`, `lnk_persist_init.R`, bcfp `wsg_linear_summary.sql`. +- Spawned Plan agent to pressure-test the draft — surfaced the unverified-columns + blocker; resolved live against local fwapg. +- Verified `fresh.streams_vw_bcfp` columns; corrected the accessible predicate to + `= ''` (text, not `text[]`); ran the coho proof: MORR 0.09%, BULK 0.27%. +- Phases approved by user. +- Created branch `221-per-wsg-habitat-access-km-rollup` off main. +- Scaffolded PWF baseline with approved phases. +- Next: Phase 1 — write `data-raw/accessible_km_proof_co.R`. diff --git a/planning/active/task_plan.md b/planning/active/task_plan.md new file mode 100644 index 00000000..610c2b66 --- /dev/null +++ b/planning/active/task_plan.md @@ -0,0 +1,55 @@ +# Task: Per-WSG habitat/access km roll-up + parity compare (accessible_km) (#221) + +We report per-segment habitat parity today but not per-WSG km totals. link already +rolls up + compares `spawning_km` / `rearing_km` per `(WSG, species)` at ~99.66% +median parity, but **`accessible_km` is never rolled up or compared** — link +persists `streams_access.access_` yet never sums it to km. Goal: a clean, +abstracted per-WSG length roll-up emitting `accessible_km` + `spawning_km` + +`rearing_km` per `(WSG, species)`, compared against a tunnel-free bcfp reference +(`fresh.streams_vw_bcfp`). Prove coho first, then generalize. WSG-level first; +per-crossing roll-up is a later phase. Morice vignette is separate/later. + +## Phase 1 — Coho `accessible_km` proof (reproducible, no new abstraction) +- [ ] Add `data-raw/accessible_km_proof_co.R` that runs the validated two-sided + query for every WSG present in both `fresh.streams_access` and + `fresh.streams_vw_bcfp` (today: BULK, MORR) and asserts `abs(pct_diff) <= 5` per + WSG, printing the table. Uses `lnk_db_conn()` local-docker args. +- [ ] Record predicate correction (`= ''` not `= array[]::text[]`) + per-species- + vs-salmon-group reconciliation in `findings.md`. + +## Phase 2 — Abstract the roll-up into one reusable, predicate-driven function +- [ ] Introduce a single roll-up primitive emitting `accessible_km` + + `spawning_km` + `rearing_km` per `(WSG, species)`, data-driven over species. + Recommend link `lnk_rollup_wsg` (mirror `frs_aggregate`'s `metrics`/`where` + shape); promote to fresh later if reuse demands. +- [ ] Fold `.lnk_compare_rollup_link` habitat km sums into that single path. +- [ ] Emit `accessible_km` as an 8th habitat_type in + `.lnk_compare_wsg_assemble_rollup`; update row-count assertions in + `tests/testthat/test-lnk_compare_wsg.R`. +- [ ] Keep habitat ref tunnel-based; add tunnel-free `accessible_km` ref path. Do + NOT force-unify the two reference sources this phase. + +## Phase 3 — All bcfp-modelled species into the parity compare +- [ ] Extend `accessible_km` to salmon (`barriers_ch_cm_co_pk_sk_dnstr`), BT + (`barriers_bt_dnstr`), ST (`barriers_st_dnstr`) — data-driven link `access_` + ↔ bcfp `barriers__dnstr` mapping. +- [ ] Wire `accessible_km` into the parity compare + `lnk_parity_annotate()`. + +## Phase 4 — Morice (MORR) vignette (separate, later) +- [ ] New MORR vignette reporting these totals; leave the existing vignette in + place. Gated on phases 1–3. Likely its own follow-up issue, not this PR. + +## Validation +- [ ] `Rscript data-raw/accessible_km_proof_co.R` prints per-WSG + link_km/ref_km/pct_diff, exits non-zero if any `|pct_diff| > 5` (confirmed live: + MORR 0.09%, BULK 0.27%). +- [ ] `devtools::test()` green incl. updated `test-lnk_compare_wsg.R`; + `lintr::lint_package()` clean; `devtools::document()` re-run. +- [ ] `/code-check` clean on each commit; PWF checkboxes match landed work. +- [ ] `/planning-archive` on completion; SRED ref in PR body only. + +## Out of scope +- Per-crossing roll-up (`lnk_aggregate()`). +- `accessible_a` / `accessible_b` anthropogenic sub-variants. +- Observed (`obsrvd_`) habitat variant. +- Full tunnel-free rebuild of the habitat km reference. From 510ec08f36e80f13d1ecdd4c835c8845c39b11f7 Mon Sep 17 00:00:00 2001 From: almac2022 Date: Wed, 1 Jul 2026 19:08:45 -0700 Subject: [PATCH 02/16] #221 Phase 1: coho accessible_km parity proof (tunnel-free bcfp) Add data-raw/accessible_km_proof_co.R: reproducible +/-5% assertion that link's per-WSG coho accessible_km (streams_access.access_co IN (1,2), joined to streams on full PK for length) matches the tunnel-free bcfp snapshot (fresh.streams_vw_bcfp, barriers_ch_cm_co_pk_sk_dnstr = ''). 19/20 local WSGs within +/-5% (most <1%); SETN allowlisted as the documented bcfp-side stale-subsurfaceflow divergence. Comparison universe is the coho-present lnk CTE (per-species vs salmon-group semantics), with a null_ref guard for the link-built/bcfp-empty case. Co-Authored-By: Claude Opus 4.7 --- data-raw/accessible_km_proof_co.R | 120 ++++++++++++++++++++++++++++++ planning/active/findings.md | 17 ++++- planning/active/progress.md | 6 +- planning/active/task_plan.md | 11 +-- 4 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 data-raw/accessible_km_proof_co.R diff --git a/data-raw/accessible_km_proof_co.R b/data-raw/accessible_km_proof_co.R new file mode 100644 index 00000000..143e0b96 --- /dev/null +++ b/data-raw/accessible_km_proof_co.R @@ -0,0 +1,120 @@ +# data-raw/accessible_km_proof_co.R +# +# Phase-1 proof for link#221: link's coho `accessible_km` per WSG matches a +# tunnel-free bcfp reference within +/- 5%. +# +# link side: sum(streams.length_metre)/1000 where streams_access.access_co +# IN (1,2), joined on the full PK (id_segment, watershed_group_code) +# (#203 discipline; length lives on streams, not streams_access). +# ref side: sum(fresh.streams_vw_bcfp.length_metre)/1000 where +# barriers_ch_cm_co_pk_sk_dnstr = '' (empty = no barrier downstream). +# +# The snapshot stores barriers__dnstr as comma-joined TEXT, so the +# accessible predicate is `= ''`, NOT `= array[]::text[]` (which errors: +# "operator does not exist: character varying = text[]"). +# +# Runs for every WSG present in BOTH fresh.streams_access and +# fresh.streams_vw_bcfp. Prints the per-WSG table and stops non-zero if any +# UNEXPECTED WSG exceeds |pct_diff| > 5. Validated 2026-07-01 on local docker +# fwapg: 19/20 WSGs within +/-5% (most < 1%; MORR 0.09%, BULK 0.27%). +# +# Known divergences (excluded from the hard-fail, still printed + flagged): +# SETN — bcfp `barriers_subsurfaceflow` is stale for ~95% of SETN segments, +# propagating into bcfp's `barriers_ch_cm_co_pk_sk_dnstr` so bcfp UNDER-credits +# accessible habitat; link correctly applies user_barriers_definite_control. +# link > ref here is the expected direction. Taxonomy: setn-anadr-*-stale in +# research/bcfp_divergence_taxonomy.yml. Not a link defect (bcfp-side stale). +# +# Usage: Rscript data-raw/accessible_km_proof_co.R + +main <- function() { + conn <- link::lnk_db_conn( + dbname = "fwapg", host = "localhost", port = 5432L, + user = "postgres", password = "postgres") + on.exit(try(DBI::dbDisconnect(conn), silent = TRUE), add = TRUE) + + tolerance_pct <- 5 + known_divergence <- c("SETN") + + # Comparison universe = coho-PRESENT WSGs: those with >= 1 coho-accessible + # segment (the `lnk` CTE). This is deliberate. link models access per species + # (`access_co`), so coho-absent WSGs have access_co = -9 everywhere and 0 + # accessible km; bcfp models CO inside the salmon group `barriers_ch_cm_co_pk_sk`, + # which is accessible wherever ANY of CH/CM/CO/PK/SK reaches. Including + # coho-absent WSGs would compare link's 0 against bcfp's other-salmon km -> false + # -100% divergence. Scoping to `lnk` compares only where a coho km number is + # meaningful. LEFT JOIN ref still surfaces the dangerous case (link has coho km, + # bcfp shows none) as a NULL ref_km caught by the null_ref guard below. + sql <- " +WITH lnk AS ( + SELECT s.watershed_group_code AS wsg, + sum(s.length_metre) / 1000.0 AS link_km + FROM fresh.streams s + JOIN fresh.streams_access a + ON s.id_segment = a.id_segment + AND s.watershed_group_code = a.watershed_group_code + WHERE a.access_co IN (1, 2) + GROUP BY s.watershed_group_code +), +ref AS ( + SELECT watershed_group_code AS wsg, + sum(length_metre) / 1000.0 AS ref_km + FROM fresh.streams_vw_bcfp + WHERE barriers_ch_cm_co_pk_sk_dnstr = '' + GROUP BY watershed_group_code +) +SELECT lnk.wsg, + round(lnk.link_km::numeric, 2) AS link_km, + round(ref.ref_km::numeric, 2) AS ref_km, + round((100.0 * (lnk.link_km - ref.ref_km) + / nullif(ref.ref_km, 0))::numeric, 2) AS pct_diff +FROM lnk +LEFT JOIN ref USING (wsg) +ORDER BY lnk.wsg; +" + + res <- DBI::dbGetQuery(conn, sql) + + if (nrow(res) == 0L) { + stop("No WSGs present in both fresh.streams_access and fresh.streams_vw_bcfp - ", + "nothing to prove. Persist at least one WSG first.", call. = FALSE) + } + + res$known_divergence <- res$wsg %in% known_divergence + + cat("\nCoho accessible_km parity (link vs tunnel-free bcfp snapshot)\n") + print(res, row.names = FALSE) + + # ref_km NULL -> nullif(0) -> NA pct_diff: bcfp reports no accessible km for a + # WSG link DID build (link_km may be > 0). Either a data problem (missing + # snapshot rows) or a genuine 0-vs-nonzero divergence. Fail loud before the + # tolerance check, where an NA index would otherwise slip through as exit 0. + null_ref <- res[is.na(res$pct_diff), , drop = FALSE] + if (nrow(null_ref) > 0L) { + stop(sprintf("bcfp ref_km is 0/NULL for WSG(s) link built: %s - check ", + paste(null_ref$wsg, collapse = ", ")), + "streams_vw_bcfp populate or investigate as divergence.", call. = FALSE) + } + + over <- res[abs(res$pct_diff) > tolerance_pct, , drop = FALSE] + flagged_known <- over[over$known_divergence, , drop = FALSE] + if (nrow(flagged_known) > 0L) { + cat(sprintf("\nKnown divergence(s) over +/-%g%% (expected, bcfp-side):\n", + tolerance_pct)) + print(flagged_known[, c("wsg", "link_km", "ref_km", "pct_diff")], + row.names = FALSE) + } + + unexpected <- over[!over$known_divergence, , drop = FALSE] + if (nrow(unexpected) > 0L) { + stop(sprintf("%d UNEXPECTED WSG(s) exceed +/-%g%%: %s", + nrow(unexpected), tolerance_pct, + paste(unexpected$wsg, collapse = ", ")), call. = FALSE) + } + + n_ok <- sum(!res$known_divergence) + cat(sprintf("\nPASS: all %d non-divergence WSG(s) within +/-%g%%.\n", + n_ok, tolerance_pct)) +} + +main() diff --git a/planning/active/findings.md b/planning/active/findings.md index de9c2e4e..f0639eee 100644 --- a/planning/active/findings.md +++ b/planning/active/findings.md @@ -24,8 +24,21 @@ other value is a barrier id / comma list. (full PK, #203), `WHERE a.access_co IN (1,2)`, `sum(s.length_metre)/1000`. - ref side: `fresh.streams_vw_bcfp WHERE barriers_ch_cm_co_pk_sk_dnstr = ''`, `sum(length_metre)/1000`. -- Result (only WSGs with local `streams_access`): **MORR 0.09%, BULK 0.27%** — - both far inside ≤5%. +- Result: **20 WSGs** present locally in both tables. **19/20 within ±5%**, most + under 1% (COTR/LKEL/WILL 0.00; MORR 0.09; BULK 0.27; largest passing FRCN 4.30, + LFRA 4.16). Full table in `data-raw/accessible_km_proof_co.R` header / run output. + +### SETN — known divergence (excluded from hard-fail) +- SETN pct_diff **+109.75%** (link 1497.55 km vs ref 713.95 km). This is the + **expected direction**: bcfp's `barriers_subsurfaceflow` is stale for ~95% of + SETN segments (74,816 / 78,937), propagating into + `barriers_ch_cm_co_pk_sk_dnstr` so bcfp UNDER-credits accessible habitat. link + correctly applies `user_barriers_definite_control`. Documented as + `setn-anadr-*-stale` in `research/bcfp_divergence_taxonomy.yml` + (status INTENTIONAL) + `research/provincial_parity_2026_05_01.md`. **Not a link + defect** — reproducing it is validation that accessible_km faithfully tracks the + access model. The proof script allowlists SETN (`known_divergence`) so it prints + + flags but does not hard-fail. ## Per-species vs salmon-group reconciliation diff --git a/planning/active/progress.md b/planning/active/progress.md index 54b568bb..6a060912 100644 --- a/planning/active/progress.md +++ b/planning/active/progress.md @@ -11,4 +11,8 @@ - Phases approved by user. - Created branch `221-per-wsg-habitat-access-km-rollup` off main. - Scaffolded PWF baseline with approved phases. -- Next: Phase 1 — write `data-raw/accessible_km_proof_co.R`. +- **Phase 1 done.** Wrote `data-raw/accessible_km_proof_co.R`; ran across all 20 + locally-persisted WSGs: 19/20 within ±5% (most < 1%), SETN +109.75% correctly + flagged as documented bcfp-side stale-subsurfaceflow divergence (link correct). + Predicate corrected to `= ''` (snapshot stores barrier arrays as text). +- Next: Phase 2 — abstract the roll-up into `lnk_rollup_wsg`. diff --git a/planning/active/task_plan.md b/planning/active/task_plan.md index 610c2b66..316d41a3 100644 --- a/planning/active/task_plan.md +++ b/planning/active/task_plan.md @@ -10,12 +10,13 @@ abstracted per-WSG length roll-up emitting `accessible_km` + `spawning_km` + per-crossing roll-up is a later phase. Morice vignette is separate/later. ## Phase 1 — Coho `accessible_km` proof (reproducible, no new abstraction) -- [ ] Add `data-raw/accessible_km_proof_co.R` that runs the validated two-sided +- [x] Add `data-raw/accessible_km_proof_co.R` that runs the validated two-sided query for every WSG present in both `fresh.streams_access` and - `fresh.streams_vw_bcfp` (today: BULK, MORR) and asserts `abs(pct_diff) <= 5` per - WSG, printing the table. Uses `lnk_db_conn()` local-docker args. -- [ ] Record predicate correction (`= ''` not `= array[]::text[]`) + per-species- - vs-salmon-group reconciliation in `findings.md`. + `fresh.streams_vw_bcfp` and asserts `abs(pct_diff) <= 5` per WSG (allowlisting + documented divergences), printing the table. Uses `lnk_db_conn()` local-docker + args. Run: 19/20 WSGs within ±5%; SETN flagged as known bcfp-side stale. +- [x] Record predicate correction (`= ''` not `= array[]::text[]`) + per-species- + vs-salmon-group reconciliation + SETN known-divergence in `findings.md`. ## Phase 2 — Abstract the roll-up into one reusable, predicate-driven function - [ ] Introduce a single roll-up primitive emitting `accessible_km` + From 883c0e3c2cdcb53ed3049ee8c5063b890c24de0c Mon Sep 17 00:00:00 2001 From: almac2022 Date: Wed, 1 Jul 2026 22:49:40 -0700 Subject: [PATCH 03/16] #221 Phase 2 (1/4): add lnk_rollup_wsg reusable per-WSG roll-up New exported lnk_rollup_wsg: predicate-driven per-(WSG, species) length roll-up over persisted streams + streams_access + streams_habitat_, joined on full PK (#203). Aliases the species-varying columns to generic access/spawning/rearing so the metrics SQL stays species-agnostic -- mirrors fresh::frs_aggregate's metrics/where shape. Default metrics emit accessible_km + spawning_km + rearing_km. accessible_km sums streams_access.access_ IN (1,2) (the Phase-1 proof-validated source), NOT the streams_habitat_.accessible bool, which carries different pre-gating semantics and diverges (MORR coho 3424 vs the validated 3330 km). 27 tests (arg validation + injection guards + offline SQL build via DBI::ANSI()). Live-verified MORR coho accessible_km = 3330.25. Co-Authored-By: Claude Opus 4.7 --- NAMESPACE | 1 + R/lnk_rollup_wsg.R | 144 +++++++++++++++++++++++++++ man/lnk_rollup_wsg.Rd | 95 ++++++++++++++++++ planning/active/progress.md | 15 ++- planning/active/task_plan.md | 9 +- tests/testthat/test-lnk_rollup_wsg.R | 108 ++++++++++++++++++++ 6 files changed, 368 insertions(+), 4 deletions(-) create mode 100644 R/lnk_rollup_wsg.R create mode 100644 man/lnk_rollup_wsg.Rd create mode 100644 tests/testthat/test-lnk_rollup_wsg.R diff --git a/NAMESPACE b/NAMESPACE index 877b3093..6754fcc2 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -42,6 +42,7 @@ export(lnk_pipeline_setup) export(lnk_pipeline_species) export(lnk_points_snap) export(lnk_presence) +export(lnk_rollup_wsg) export(lnk_rules_build) export(lnk_score) export(lnk_source) diff --git a/R/lnk_rollup_wsg.R b/R/lnk_rollup_wsg.R new file mode 100644 index 00000000..5784c84a --- /dev/null +++ b/R/lnk_rollup_wsg.R @@ -0,0 +1,144 @@ +#' Roll up per-(WSG, species) length metrics from persisted state +#' +#' Reusable, predicate-driven roll-up over link's persisted per-species +#' tables. For each species it joins `.streams` (length + edge +#' type) to `.streams_access` (per-species `access_` code) +#' and `.streams_habitat_` (spawning / rearing flags) on the +#' full PK `(id_segment, watershed_group_code)` (#203), exposes the +#' three species-varying inputs under **generic aliases** — `access` +#' (int: -9 absent / 0 blocked / 1 modelled / 2 observed), `spawning`, +#' `rearing` (bool) — then aggregates by `(watershed_group_code, +#' species_code)`. +#' +#' Because the per-species columns are aliased to fixed names, the +#' `metrics` SQL is written **once**, species-agnostic — mirroring +#' `fresh::frs_aggregate()`'s `metrics` / `where` shape. Adding a species +#' is a `species` vector edit, not a query edit. +#' +#' `accessible_km` sums `access IN (1, 2)` — link's per-species access +#' model on `streams_access`, the number validated against the +#' tunnel-free bcfp reference in `data-raw/accessible_km_proof_co.R` +#' (coho, 19/20 WSGs within +/-5%). It deliberately does **not** use the +#' `accessible` boolean on `streams_habitat_`, which carries +#' different (pre-gating) semantics and diverges from the access model +#' (MORR coho: 3424 km vs the validated 3330 km). +#' +#' @param conn A [DBI::DBIConnection-class] object (from [lnk_db_conn()]). +#' @param aoi Watershed group code (e.g. `"MORR"`). Uppercase 3-5 letters. +#' @param species Character vector of species codes (e.g. `c("CO","BT")`). +#' Each must name existing `.streams_habitat_` and +#' `.streams_access.access_`. Restricted to alpha +#' characters — interpolated into identifiers, so validated to make +#' SQL injection structurally impossible. +#' @param schema Persist schema holding `streams`, `streams_access`, +#' `streams_habitat_`. Default `"fresh"`. Validated against the SQL +#' identifier whitelist. +#' @param metrics Named character vector: names are output columns, +#' values are SQL aggregate expressions over the generic aliases +#' `length_metre`, `access`, `spawning`, `rearing`. Default emits +#' `accessible_km`, `spawning_km`, `rearing_km`. Raw SQL — trusted +#' caller input, like `frs_aggregate()`. +#' @param where Character or `NULL`. Optional SQL predicate applied to the +#' per-species rows before aggregation (aliases available). Default +#' `NULL`. +#' +#' @return A data.frame with one row per `(wsg, species)` and one column +#' per metric. Columns: `wsg`, `species`, then `names(metrics)`. +#' +#' @examples +#' \dontrun{ +#' conn <- lnk_db_conn() +#' # Coho accessible / spawning / rearing km for Morice, from persisted state. +#' lnk_rollup_wsg(conn, aoi = "MORR", species = "CO") +#' +#' # Custom metric: count accessible segments per species. +#' lnk_rollup_wsg(conn, aoi = "MORR", species = c("CO", "BT"), +#' metrics = c(n_accessible = "COUNT(*) FILTER (WHERE access IN (1, 2))")) +#' } +#' +#' @family compare +#' @seealso [lnk_compare_rollup()], [fresh::frs_aggregate()] +#' @export +# nolint start: indentation_linter +lnk_rollup_wsg <- function(conn, aoi, species, + schema = "fresh", + metrics = c( + accessible_km = + "round(sum(length_metre) FILTER (WHERE access IN (1, 2))::numeric / 1000, 2)", # nolint: line_length_linter + spawning_km = + "round(sum(length_metre) FILTER (WHERE spawning)::numeric / 1000, 2)", # nolint: line_length_linter + rearing_km = + "round(sum(length_metre) FILTER (WHERE rearing)::numeric / 1000, 2)" # nolint: line_length_linter + ), + where = NULL) { + stopifnot( + inherits(conn, "DBIConnection"), + is.character(aoi), length(aoi) == 1L, nzchar(aoi), + grepl("^[A-Z]{3,5}$", aoi), + is.character(species), length(species) >= 1L, all(nzchar(species)), + # Species suffix is interpolated into `streams_habitat_` and + # `access_`; alpha-only makes injection structurally impossible. + all(grepl("^[A-Za-z]+$", species)), + is.character(schema), length(schema) == 1L, + grepl("^[a-z_][a-z0-9_]*$", schema), + is.character(metrics), length(metrics) >= 1L, + !is.null(names(metrics)), all(nzchar(names(metrics))), + is.null(where) || (is.character(where) && length(where) == 1L) + ) + + sql <- .lnk_rollup_wsg_sql( + conn = conn, aoi = aoi, species = species, + schema = schema, metrics = metrics, where = where) + DBI::dbGetQuery(conn, sql) +} + + +# --------------------------------------------------------------------------- +# Internal helpers +# --------------------------------------------------------------------------- + +#' Build the per-(WSG, species) roll-up SQL. +#' +#' Split out from [lnk_rollup_wsg()] so the query text is unit-testable +#' without a live connection (arg validation happens in the exported fn). +#' +#' @noRd +.lnk_rollup_wsg_sql <- function(conn, aoi, species, schema, metrics, where) { + aoi_lit <- DBI::dbQuoteLiteral(conn, aoi) + + per_species <- paste(vapply(species, function(sp) { + sp_lit <- DBI::dbQuoteLiteral(conn, toupper(sp)) + sprintf( + "SELECT %s AS species_code, s.watershed_group_code, + s.length_metre, s.edge_type, + a.access_%s AS access, h.spawning, h.rearing + FROM %s.streams s + JOIN %s.streams_access a + ON s.id_segment = a.id_segment + AND s.watershed_group_code = a.watershed_group_code + JOIN %s.streams_habitat_%s h + ON s.id_segment = h.id_segment + AND s.watershed_group_code = h.watershed_group_code + WHERE s.watershed_group_code = %s", + sp_lit, tolower(sp), + schema, schema, schema, tolower(sp), aoi_lit) + }, character(1)), collapse = "\n UNION ALL\n ") + + cols_metric <- paste( + sprintf("%s AS %s", unname(metrics), names(metrics)), + collapse = ",\n ") + + where_clause <- if (!is.null(where)) paste("\n WHERE", where) else "" + + sprintf( + "SELECT watershed_group_code AS wsg, + species_code AS species, + %s + FROM ( + %s + ) per_species%s + GROUP BY watershed_group_code, species_code + ORDER BY species_code", + cols_metric, per_species, where_clause) +} +# nolint end: indentation_linter diff --git a/man/lnk_rollup_wsg.Rd b/man/lnk_rollup_wsg.Rd new file mode 100644 index 00000000..ef6f2558 --- /dev/null +++ b/man/lnk_rollup_wsg.Rd @@ -0,0 +1,95 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lnk_rollup_wsg.R +\name{lnk_rollup_wsg} +\alias{lnk_rollup_wsg} +\title{Roll up per-(WSG, species) length metrics from persisted state} +\usage{ +lnk_rollup_wsg( + conn, + aoi, + species, + schema = "fresh", + metrics = c(accessible_km = + "round(sum(length_metre) FILTER (WHERE access IN (1, 2))::numeric / 1000, 2)", + spawning_km = "round(sum(length_metre) FILTER (WHERE spawning)::numeric / 1000, 2)", + rearing_km = "round(sum(length_metre) FILTER (WHERE rearing)::numeric / 1000, 2)"), + where = NULL +) +} +\arguments{ +\item{conn}{A \link[DBI:DBIConnection-class]{DBI::DBIConnection} object (from \code{\link[=lnk_db_conn]{lnk_db_conn()}}).} + +\item{aoi}{Watershed group code (e.g. \code{"MORR"}). Uppercase 3-5 letters.} + +\item{species}{Character vector of species codes (e.g. \code{c("CO","BT")}). +Each must name existing \verb{.streams_habitat_} and +\verb{.streams_access.access_}. Restricted to alpha +characters — interpolated into identifiers, so validated to make +SQL injection structurally impossible.} + +\item{schema}{Persist schema holding \code{streams}, \code{streams_access}, +\verb{streams_habitat_}. Default \code{"fresh"}. Validated against the SQL +identifier whitelist.} + +\item{metrics}{Named character vector: names are output columns, +values are SQL aggregate expressions over the generic aliases +\code{length_metre}, \code{access}, \code{spawning}, \code{rearing}. Default emits +\code{accessible_km}, \code{spawning_km}, \code{rearing_km}. Raw SQL — trusted +caller input, like \code{frs_aggregate()}.} + +\item{where}{Character or \code{NULL}. Optional SQL predicate applied to the +per-species rows before aggregation (aliases available). Default +\code{NULL}.} +} +\value{ +A data.frame with one row per \verb{(wsg, species)} and one column +per metric. Columns: \code{wsg}, \code{species}, then \code{names(metrics)}. +} +\description{ +Reusable, predicate-driven roll-up over link's persisted per-species +tables. For each species it joins \verb{.streams} (length + edge +type) to \verb{.streams_access} (per-species \verb{access_} code) +and \verb{.streams_habitat_} (spawning / rearing flags) on the +full PK \verb{(id_segment, watershed_group_code)} (#203), exposes the +three species-varying inputs under \strong{generic aliases} — \code{access} +(int: -9 absent / 0 blocked / 1 modelled / 2 observed), \code{spawning}, +\code{rearing} (bool) — then aggregates by \verb{(watershed_group_code, species_code)}. +} +\details{ +Because the per-species columns are aliased to fixed names, the +\code{metrics} SQL is written \strong{once}, species-agnostic — mirroring +\code{fresh::frs_aggregate()}'s \code{metrics} / \code{where} shape. Adding a species +is a \code{species} vector edit, not a query edit. + +\code{accessible_km} sums \verb{access IN (1, 2)} — link's per-species access +model on \code{streams_access}, the number validated against the +tunnel-free bcfp reference in \code{data-raw/accessible_km_proof_co.R} +(coho, 19/20 WSGs within +/-5\%). It deliberately does \strong{not} use the +\code{accessible} boolean on \verb{streams_habitat_}, which carries +different (pre-gating) semantics and diverges from the access model +(MORR coho: 3424 km vs the validated 3330 km). +} +\examples{ +\dontrun{ +conn <- lnk_db_conn() +# Coho accessible / spawning / rearing km for Morice, from persisted state. +lnk_rollup_wsg(conn, aoi = "MORR", species = "CO") + +# Custom metric: count accessible segments per species. +lnk_rollup_wsg(conn, aoi = "MORR", species = c("CO", "BT"), + metrics = c(n_accessible = "COUNT(*) FILTER (WHERE access IN (1, 2))")) +} + +} +\seealso{ +\code{\link[=lnk_compare_rollup]{lnk_compare_rollup()}}, \code{\link[fresh:frs_aggregate]{fresh::frs_aggregate()}} + +Other compare: +\code{\link{lnk_access}()}, +\code{\link{lnk_compare_mapping_code}()}, +\code{\link{lnk_compare_rollup}()}, +\code{\link{lnk_compare_wsg}()}, +\code{\link{lnk_mapping_code}()}, +\code{\link{lnk_parity_annotate}()} +} +\concept{compare} diff --git a/planning/active/progress.md b/planning/active/progress.md index 6a060912..9f03e894 100644 --- a/planning/active/progress.md +++ b/planning/active/progress.md @@ -15,4 +15,17 @@ locally-persisted WSGs: 19/20 within ±5% (most < 1%), SETN +109.75% correctly flagged as documented bcfp-side stale-subsurfaceflow divergence (link correct). Predicate corrected to `= ''` (snapshot stores barrier arrays as text). -- Next: Phase 2 — abstract the roll-up into `lnk_rollup_wsg`. +- **Phase 1 committed** (`510ec08`) after 3-round code-check (on.exit wrapped in + `main()` so cleanup fires; null_ref guard; coho-present `lnk` universe). +- **Phase 2 (1/4) done.** Shipped `R/lnk_rollup_wsg.R` — reusable predicate-driven + per-(WSG, species) roll-up mirroring `frs_aggregate`'s `metrics`/`where` shape. + Per species it joins streams+access+habitat on full PK and aliases the species- + varying columns to generic `access`/`spawning`/`rearing` so `metrics` SQL stays + species-agnostic. Default emits accessible_km/spawning_km/rearing_km. 27 unit + tests (arg validation + offline SQL build via `DBI::ANSI()`), code-check clean. + Live: MORR coho accessible_km 3330.25 (= Phase-1 proof). Key: accessible_km + sources `streams_access.access_ IN (1,2)`, not `streams_habitat.accessible`. +- Discovered `streams_habitat_.accessible` bool diverges from the access model + (MORR coho 3424 vs 3330 km) — do NOT sum it for accessible_km. +- Next: Phase 2 (2/4) — fold `.lnk_compare_rollup_link` habitat km sums into the + `lnk_rollup_wsg` path; then emit accessible_km as 8th habitat_type + update tests. diff --git a/planning/active/task_plan.md b/planning/active/task_plan.md index 316d41a3..b2c161ee 100644 --- a/planning/active/task_plan.md +++ b/planning/active/task_plan.md @@ -19,10 +19,13 @@ per-crossing roll-up is a later phase. Morice vignette is separate/later. vs-salmon-group reconciliation + SETN known-divergence in `findings.md`. ## Phase 2 — Abstract the roll-up into one reusable, predicate-driven function -- [ ] Introduce a single roll-up primitive emitting `accessible_km` + +- [x] Introduce a single roll-up primitive emitting `accessible_km` + `spawning_km` + `rearing_km` per `(WSG, species)`, data-driven over species. - Recommend link `lnk_rollup_wsg` (mirror `frs_aggregate`'s `metrics`/`where` - shape); promote to fresh later if reuse demands. + Shipped `lnk_rollup_wsg` (mirrors `frs_aggregate`'s `metrics`/`where` shape, + species-agnostic via generic `access`/`spawning`/`rearing` aliases). Live- + verified MORR coho `accessible_km` = 3330.25 (matches Phase-1 proof). 27 unit + tests. `accessible_km` sources `streams_access.access_ IN (1,2)`, NOT the + divergent `streams_habitat_.accessible` bool (MORR 3330 vs 3424 km). - [ ] Fold `.lnk_compare_rollup_link` habitat km sums into that single path. - [ ] Emit `accessible_km` as an 8th habitat_type in `.lnk_compare_wsg_assemble_rollup`; update row-count assertions in diff --git a/tests/testthat/test-lnk_rollup_wsg.R b/tests/testthat/test-lnk_rollup_wsg.R new file mode 100644 index 00000000..41647129 --- /dev/null +++ b/tests/testthat/test-lnk_rollup_wsg.R @@ -0,0 +1,108 @@ +# Tests for lnk_rollup_wsg — argument validation + SQL construction + +mock_conn <- function() structure(list(), class = "DBIConnection") + +# --------------------------------------------------------------------------- +# Argument validation +# --------------------------------------------------------------------------- + +test_that("lnk_rollup_wsg rejects invalid aoi", { + expect_error(lnk_rollup_wsg(mock_conn(), aoi = "", species = "CO"), "aoi") + expect_error(lnk_rollup_wsg(mock_conn(), aoi = "ab", species = "CO"), "aoi") + expect_error( + lnk_rollup_wsg(mock_conn(), aoi = c("MORR", "BULK"), species = "CO"), + "aoi") +}) + +test_that("lnk_rollup_wsg rejects non-DBI conn", { + expect_error(lnk_rollup_wsg("not-a-conn", aoi = "MORR", species = "CO"), + "DBI") +}) + +test_that("lnk_rollup_wsg rejects empty or non-alpha species", { + expect_error( + lnk_rollup_wsg(mock_conn(), aoi = "MORR", species = character(0)), + "species") + expect_error( + lnk_rollup_wsg(mock_conn(), aoi = "MORR", species = ""), + "species") + # Injection-shaped species suffix must be rejected before it reaches SQL. + expect_error( + lnk_rollup_wsg(mock_conn(), aoi = "MORR", species = "co; DROP TABLE x"), + "species") + expect_error( + lnk_rollup_wsg(mock_conn(), aoi = "MORR", species = "co_1"), + "species") +}) + +test_that("lnk_rollup_wsg rejects schema outside the SQL identifier whitelist", { + expect_error( + lnk_rollup_wsg(mock_conn(), aoi = "MORR", species = "CO", + schema = "Fresh"), # mixed case + "schema") + expect_error( + lnk_rollup_wsg(mock_conn(), aoi = "MORR", species = "CO", + schema = "x; DROP SCHEMA public CASCADE"), + "schema") + expect_error( + lnk_rollup_wsg(mock_conn(), aoi = "MORR", species = "CO", + schema = "1bad"), # leading digit + "schema") +}) + +test_that("lnk_rollup_wsg rejects unnamed metrics", { + expect_error( + lnk_rollup_wsg(mock_conn(), aoi = "MORR", species = "CO", + metrics = c("COUNT(*)")), + "metrics") +}) + +# --------------------------------------------------------------------------- +# SQL construction (offline, via DBI::ANSI() for standard-SQL quoting) +# --------------------------------------------------------------------------- + +test_that(".lnk_rollup_wsg_sql builds one UNION ALL branch per species", { + metrics <- c( + accessible_km = + "round(sum(length_metre) FILTER (WHERE access IN (1, 2))::numeric / 1000, 2)", # nolint: line_length_linter + spawning_km = "round(sum(length_metre) FILTER (WHERE spawning)::numeric / 1000, 2)") # nolint: line_length_linter + + sql <- link:::.lnk_rollup_wsg_sql( + conn = DBI::ANSI(), aoi = "MORR", species = c("CO", "BT"), + schema = "fresh", metrics = metrics, where = NULL) + + # Per-species table + access column, correctly lower-cased. + expect_match(sql, "fresh\\.streams_habitat_co", fixed = FALSE) + expect_match(sql, "fresh\\.streams_habitat_bt", fixed = FALSE) + expect_match(sql, "a\\.access_co AS access") + expect_match(sql, "a\\.access_bt AS access") + # species_code literal upper-cased. + expect_match(sql, "'CO' AS species_code") + expect_match(sql, "'BT' AS species_code") + # One UNION ALL joining the two branches. + expect_equal(lengths(regmatches(sql, gregexpr("UNION ALL", sql))), 1L) + # Full-PK join (#203) on both access and habitat. + expect_match(sql, "s\\.watershed_group_code = a\\.watershed_group_code") + expect_match(sql, "s\\.watershed_group_code = h\\.watershed_group_code") + # Metric aliases + grouping. + expect_match(sql, "AS accessible_km") + expect_match(sql, "AS spawning_km") + expect_match(sql, "GROUP BY watershed_group_code, species_code") + # aoi literal bound. + expect_match(sql, "'MORR'") +}) + +test_that(".lnk_rollup_wsg_sql appends an optional where predicate", { + metrics <- c(n = "COUNT(*)") + + sql_no_where <- link:::.lnk_rollup_wsg_sql( + conn = DBI::ANSI(), aoi = "MORR", species = "CO", + schema = "fresh", metrics = metrics, where = NULL) + expect_false(grepl("access IN \\(1, 2\\)", sql_no_where)) + + sql_where <- link:::.lnk_rollup_wsg_sql( + conn = DBI::ANSI(), aoi = "MORR", species = "CO", + schema = "fresh", metrics = metrics, where = "access IN (1, 2)") + # Outer WHERE on the aggregated subquery, before GROUP BY. + expect_match(sql_where, "per_species\\s*\\n\\s*WHERE access IN \\(1, 2\\)") +}) From f8ce11c8b38ac3885fb32cbd380933f08f7b6fe5 Mon Sep 17 00:00:00 2001 From: almac2022 Date: Thu, 2 Jul 2026 06:54:49 -0700 Subject: [PATCH 04/16] #221 Phase 2: doc-clarify lnk_rollup_wsg vs lnk_aggregate grain Add @seealso [lnk_aggregate()] and a note that lnk_rollup_wsg is a flat per-WSG GROUP BY (whole-WSG length by species), distinct from lnk_aggregate() / fresh::frs_aggregate() which roll habitat up the network upstream of individual crossings. Makes the rollup-vs-aggregate grain split intentional rather than an accidental shared-token clash. Regenerated Rd family-index back-references so the compare family lists lnk_rollup_wsg on each member (missed in the prior commit). Co-Authored-By: Claude Opus 4.7 --- R/lnk_rollup_wsg.R | 9 ++++++++- man/lnk_access.Rd | 3 ++- man/lnk_compare_mapping_code.Rd | 3 ++- man/lnk_compare_rollup.Rd | 3 ++- man/lnk_compare_wsg.Rd | 3 ++- man/lnk_mapping_code.Rd | 3 ++- man/lnk_parity_annotate.Rd | 3 ++- man/lnk_rollup_wsg.Rd | 9 ++++++++- 8 files changed, 28 insertions(+), 8 deletions(-) diff --git a/R/lnk_rollup_wsg.R b/R/lnk_rollup_wsg.R index 5784c84a..a8c1ff9a 100644 --- a/R/lnk_rollup_wsg.R +++ b/R/lnk_rollup_wsg.R @@ -15,6 +15,12 @@ #' `fresh::frs_aggregate()`'s `metrics` / `where` shape. Adding a species #' is a `species` vector edit, not a query edit. #' +#' This is a **flat per-WSG `GROUP BY`** — it sums whole-WSG length by +#' `(watershed_group_code, species_code)`. It is distinct from +#' [lnk_aggregate()] / [fresh::frs_aggregate()], which roll habitat up the +#' network *upstream of individual crossings* (point-based traversal). Use +#' this for WSG totals; use those for per-crossing upstream summaries. +#' #' `accessible_km` sums `access IN (1, 2)` — link's per-species access #' model on `streams_access`, the number validated against the #' tunnel-free bcfp reference in `data-raw/accessible_km_proof_co.R` @@ -57,7 +63,8 @@ #' } #' #' @family compare -#' @seealso [lnk_compare_rollup()], [fresh::frs_aggregate()] +#' @seealso [lnk_compare_rollup()], [lnk_aggregate()], +#' [fresh::frs_aggregate()] #' @export # nolint start: indentation_linter lnk_rollup_wsg <- function(conn, aoi, species, diff --git a/man/lnk_access.Rd b/man/lnk_access.Rd index c4f81a08..0e61c3dd 100644 --- a/man/lnk_access.Rd +++ b/man/lnk_access.Rd @@ -128,6 +128,7 @@ Other compare: \code{\link{lnk_compare_rollup}()}, \code{\link{lnk_compare_wsg}()}, \code{\link{lnk_mapping_code}()}, -\code{\link{lnk_parity_annotate}()} +\code{\link{lnk_parity_annotate}()}, +\code{\link{lnk_rollup_wsg}()} } \concept{compare} diff --git a/man/lnk_compare_mapping_code.Rd b/man/lnk_compare_mapping_code.Rd index 7e64f183..fe301e9a 100644 --- a/man/lnk_compare_mapping_code.Rd +++ b/man/lnk_compare_mapping_code.Rd @@ -103,6 +103,7 @@ Other compare: \code{\link{lnk_compare_rollup}()}, \code{\link{lnk_compare_wsg}()}, \code{\link{lnk_mapping_code}()}, -\code{\link{lnk_parity_annotate}()} +\code{\link{lnk_parity_annotate}()}, +\code{\link{lnk_rollup_wsg}()} } \concept{compare} diff --git a/man/lnk_compare_rollup.Rd b/man/lnk_compare_rollup.Rd index 6e4bec23..b086c7f8 100644 --- a/man/lnk_compare_rollup.Rd +++ b/man/lnk_compare_rollup.Rd @@ -94,6 +94,7 @@ Other compare: \code{\link{lnk_compare_mapping_code}()}, \code{\link{lnk_compare_wsg}()}, \code{\link{lnk_mapping_code}()}, -\code{\link{lnk_parity_annotate}()} +\code{\link{lnk_parity_annotate}()}, +\code{\link{lnk_rollup_wsg}()} } \concept{compare} diff --git a/man/lnk_compare_wsg.Rd b/man/lnk_compare_wsg.Rd index 1a05626c..564266e7 100644 --- a/man/lnk_compare_wsg.Rd +++ b/man/lnk_compare_wsg.Rd @@ -152,6 +152,7 @@ Other compare: \code{\link{lnk_compare_mapping_code}()}, \code{\link{lnk_compare_rollup}()}, \code{\link{lnk_mapping_code}()}, -\code{\link{lnk_parity_annotate}()} +\code{\link{lnk_parity_annotate}()}, +\code{\link{lnk_rollup_wsg}()} } \concept{compare} diff --git a/man/lnk_mapping_code.Rd b/man/lnk_mapping_code.Rd index 6accf152..a8879d06 100644 --- a/man/lnk_mapping_code.Rd +++ b/man/lnk_mapping_code.Rd @@ -125,6 +125,7 @@ Other compare: \code{\link{lnk_compare_mapping_code}()}, \code{\link{lnk_compare_rollup}()}, \code{\link{lnk_compare_wsg}()}, -\code{\link{lnk_parity_annotate}()} +\code{\link{lnk_parity_annotate}()}, +\code{\link{lnk_rollup_wsg}()} } \concept{compare} diff --git a/man/lnk_parity_annotate.Rd b/man/lnk_parity_annotate.Rd index 6e088b0a..d8cf8623 100644 --- a/man/lnk_parity_annotate.Rd +++ b/man/lnk_parity_annotate.Rd @@ -64,6 +64,7 @@ Other compare: \code{\link{lnk_compare_mapping_code}()}, \code{\link{lnk_compare_rollup}()}, \code{\link{lnk_compare_wsg}()}, -\code{\link{lnk_mapping_code}()} +\code{\link{lnk_mapping_code}()}, +\code{\link{lnk_rollup_wsg}()} } \concept{compare} diff --git a/man/lnk_rollup_wsg.Rd b/man/lnk_rollup_wsg.Rd index ef6f2558..20dfd967 100644 --- a/man/lnk_rollup_wsg.Rd +++ b/man/lnk_rollup_wsg.Rd @@ -61,6 +61,12 @@ Because the per-species columns are aliased to fixed names, the \code{fresh::frs_aggregate()}'s \code{metrics} / \code{where} shape. Adding a species is a \code{species} vector edit, not a query edit. +This is a \strong{flat per-WSG \verb{GROUP BY}} — it sums whole-WSG length by +\verb{(watershed_group_code, species_code)}. It is distinct from +\code{\link[=lnk_aggregate]{lnk_aggregate()}} / \code{\link[fresh:frs_aggregate]{fresh::frs_aggregate()}}, which roll habitat up the +network \emph{upstream of individual crossings} (point-based traversal). Use +this for WSG totals; use those for per-crossing upstream summaries. + \code{accessible_km} sums \verb{access IN (1, 2)} — link's per-species access model on \code{streams_access}, the number validated against the tunnel-free bcfp reference in \code{data-raw/accessible_km_proof_co.R} @@ -82,7 +88,8 @@ lnk_rollup_wsg(conn, aoi = "MORR", species = c("CO", "BT"), } \seealso{ -\code{\link[=lnk_compare_rollup]{lnk_compare_rollup()}}, \code{\link[fresh:frs_aggregate]{fresh::frs_aggregate()}} +\code{\link[=lnk_compare_rollup]{lnk_compare_rollup()}}, \code{\link[=lnk_aggregate]{lnk_aggregate()}}, +\code{\link[fresh:frs_aggregate]{fresh::frs_aggregate()}} Other compare: \code{\link{lnk_access}()}, From 4be5b873e40a8ec2b4381d0c0735fabe0d336235 Mon Sep 17 00:00:00 2001 From: almac2022 Date: Thu, 2 Jul 2026 07:07:25 -0700 Subject: [PATCH 05/16] #221 Phase 2: fold habitat km rollup into lnk_rollup_wsg .lnk_compare_rollup_link's km block now delegates to lnk_rollup_wsg via a COALESCE'd 5-metric vector, preserving the historical CASE-WHEN measured-0 semantics (FILTER over an empty set yields NULL, not 0), then renames species->species_code and reselects columns to keep the list(km, lake_ha, wetland_ha) contract. lake_ha/wetland_ha (DISTINCT-waterbody polygon joins) stay as-is. lnk_rollup_wsg's streams_access join changed JOIN->LEFT JOIN: access is optional metadata for a length rollup, so habitat length is never dropped when access is unbuilt (absent access -> NULL -> accessible_km 0). Safe even for mapping_code=FALSE persists since lnk_pipeline_access runs unconditionally. Byte-identical vs the old CASE-WHEN SQL on MORR BT+CO across all 5 metrics; lnk_rollup_wsg accessible_km still 3330.25. Co-Authored-By: Claude Opus 4.7 --- R/lnk_compare_rollup.R | 59 +++++++++++++++------------- R/lnk_rollup_wsg.R | 30 ++++++++------ man/lnk_rollup_wsg.Rd | 20 +++++++--- planning/active/progress.md | 21 +++++++++- planning/active/task_plan.md | 11 +++++- tests/testthat/test-lnk_rollup_wsg.R | 4 ++ 6 files changed, 97 insertions(+), 48 deletions(-) diff --git a/R/lnk_compare_rollup.R b/R/lnk_compare_rollup.R index d6f72b43..73bc7daa 100644 --- a/R/lnk_compare_rollup.R +++ b/R/lnk_compare_rollup.R @@ -195,35 +195,38 @@ lnk_compare_rollup <- function(conn, aoi, cfg, et_lake_sql <- "(1500, 1525)" et_wetland_sql <- "(1700)" - union_streams <- paste(vapply(species, function(sp) { - sp_lit <- DBI::dbQuoteLiteral(conn, sp) - sprintf( - "SELECT %s AS species_code, s.id_segment, s.length_metre, - s.edge_type, h.spawning, h.rearing - FROM %s.streams s - JOIN %s.streams_habitat_%s h ON s.id_segment = h.id_segment AND s.watershed_group_code = h.watershed_group_code - WHERE s.watershed_group_code = %s", - sp_lit, tn$schema, tn$schema, tolower(sp), aoi_lit) # nolint: indentation_linter - }, character(1)), collapse = "\n UNION ALL\n ") + # Habitat km sums delegate to the single predicate-driven roll-up + # primitive (link#221) so there is one per-(WSG, species) km query + # builder. lnk_rollup_wsg exposes `length_metre` / `edge_type` / + # `spawning` / `rearing` under generic aliases; these five metrics + # reproduce the historical shape. It returns `wsg` + `species` + + # metrics — drop `wsg` and rename `species` -> `species_code` to + # preserve this helper's `list(km, lake_ha, wetland_ha)` contract. + # COALESCE(..., 0): an empty FILTER set sums to NULL, but the + # historical CASE-WHEN-ELSE-0 form returned a measured 0 for a + # species present in the WSG with no segments in that slice. Preserve + # that 0 (not NA) so downstream parity diff_pct is unchanged. + km_metrics <- c( + spawning_km = + "round(COALESCE(sum(length_metre) FILTER (WHERE spawning), 0)::numeric / 1000, 2)", # nolint: line_length_linter + rearing_km = + "round(COALESCE(sum(length_metre) FILTER (WHERE rearing), 0)::numeric / 1000, 2)", # nolint: line_length_linter + rearing_stream_km = sprintf( + "round(COALESCE(sum(length_metre) FILTER (WHERE rearing AND edge_type IN %s), 0)::numeric / 1000, 2)", # nolint: line_length_linter + et_stream_sql), + rearing_lake_centerline_km = sprintf( + "round(COALESCE(sum(length_metre) FILTER (WHERE rearing AND edge_type IN %s), 0)::numeric / 1000, 2)", # nolint: line_length_linter + et_lake_sql), + rearing_wetland_centerline_km = sprintf( + "round(COALESCE(sum(length_metre) FILTER (WHERE rearing AND edge_type IN %s), 0)::numeric / 1000, 2)", # nolint: line_length_linter + et_wetland_sql)) - km <- DBI::dbGetQuery(conn, sprintf(" - SELECT species_code, - round(SUM(CASE WHEN spawning THEN length_metre ELSE 0 END)::numeric - / 1000, 2) AS spawning_km, - round(SUM(CASE WHEN rearing THEN length_metre ELSE 0 END)::numeric - / 1000, 2) AS rearing_km, - round(SUM(CASE WHEN rearing AND edge_type IN %s - THEN length_metre ELSE 0 END)::numeric / 1000, 2) - AS rearing_stream_km, - round(SUM(CASE WHEN rearing AND edge_type IN %s - THEN length_metre ELSE 0 END)::numeric / 1000, 2) - AS rearing_lake_centerline_km, - round(SUM(CASE WHEN rearing AND edge_type IN %s - THEN length_metre ELSE 0 END)::numeric / 1000, 2) - AS rearing_wetland_centerline_km - FROM (%s) per_species - GROUP BY species_code ORDER BY species_code", - et_stream_sql, et_lake_sql, et_wetland_sql, union_streams)) # nolint: indentation_linter + km <- lnk_rollup_wsg( # nolint: object_usage_linter + conn = conn, aoi = aoi, species = species, + schema = tn$schema, metrics = km_metrics) + km$wsg <- NULL + names(km)[names(km) == "species"] <- "species_code" + km <- km[, c("species_code", names(km_metrics))] # Lake / wetland ha — DISTINCT waterbody_key joins to fwa polygon # tables avoid double-counting multi-segment lakes/wetlands. diff --git a/R/lnk_rollup_wsg.R b/R/lnk_rollup_wsg.R index a8c1ff9a..0ea0b033 100644 --- a/R/lnk_rollup_wsg.R +++ b/R/lnk_rollup_wsg.R @@ -2,13 +2,21 @@ #' #' Reusable, predicate-driven roll-up over link's persisted per-species #' tables. For each species it joins `.streams` (length + edge -#' type) to `.streams_access` (per-species `access_` code) -#' and `.streams_habitat_` (spawning / rearing flags) on the -#' full PK `(id_segment, watershed_group_code)` (#203), exposes the -#' three species-varying inputs under **generic aliases** — `access` -#' (int: -9 absent / 0 blocked / 1 modelled / 2 observed), `spawning`, -#' `rearing` (bool) — then aggregates by `(watershed_group_code, -#' species_code)`. +#' type) to `.streams_habitat_` (spawning / rearing flags) +#' and **left-joins** `.streams_access` (per-species +#' `access_` code) on the full PK `(id_segment, +#' watershed_group_code)` (#203), exposes the three species-varying +#' inputs under **generic aliases** — `access` (int: -9 absent / 0 +#' blocked / 1 modelled / 2 observed), `spawning`, `rearing` (bool) — +#' then aggregates by `(watershed_group_code, species_code)`. +#' +#' The `streams_access` join is a LEFT join: access is optional metadata +#' for a length roll-up. When a segment has no `streams_access` row +#' (access not yet built for the WSG), `access` is `NULL` and +#' `accessible_km` resolves to 0 — build it via +#' `lnk_pipeline_run(mapping_code = TRUE)` (or the unconditional access +#' phase). Length is never dropped by a missing access row, so the +#' habitat metrics (`spawning_km`, `rearing_km`) are unaffected. #' #' Because the per-species columns are aliased to fixed names, the #' `metrics` SQL is written **once**, species-agnostic — mirroring @@ -120,15 +128,15 @@ lnk_rollup_wsg <- function(conn, aoi, species, s.length_metre, s.edge_type, a.access_%s AS access, h.spawning, h.rearing FROM %s.streams s - JOIN %s.streams_access a - ON s.id_segment = a.id_segment - AND s.watershed_group_code = a.watershed_group_code JOIN %s.streams_habitat_%s h ON s.id_segment = h.id_segment AND s.watershed_group_code = h.watershed_group_code + LEFT JOIN %s.streams_access a + ON s.id_segment = a.id_segment + AND s.watershed_group_code = a.watershed_group_code WHERE s.watershed_group_code = %s", sp_lit, tolower(sp), - schema, schema, schema, tolower(sp), aoi_lit) + schema, schema, tolower(sp), schema, aoi_lit) }, character(1)), collapse = "\n UNION ALL\n ") cols_metric <- paste( diff --git a/man/lnk_rollup_wsg.Rd b/man/lnk_rollup_wsg.Rd index 20dfd967..2bd05c27 100644 --- a/man/lnk_rollup_wsg.Rd +++ b/man/lnk_rollup_wsg.Rd @@ -48,14 +48,22 @@ per metric. Columns: \code{wsg}, \code{species}, then \code{names(metrics)}. \description{ Reusable, predicate-driven roll-up over link's persisted per-species tables. For each species it joins \verb{.streams} (length + edge -type) to \verb{.streams_access} (per-species \verb{access_} code) -and \verb{.streams_habitat_} (spawning / rearing flags) on the -full PK \verb{(id_segment, watershed_group_code)} (#203), exposes the -three species-varying inputs under \strong{generic aliases} — \code{access} -(int: -9 absent / 0 blocked / 1 modelled / 2 observed), \code{spawning}, -\code{rearing} (bool) — then aggregates by \verb{(watershed_group_code, species_code)}. +type) to \verb{.streams_habitat_} (spawning / rearing flags) +and \strong{left-joins} \verb{.streams_access} (per-species +\verb{access_} code) on the full PK \verb{(id_segment, watershed_group_code)} (#203), exposes the three species-varying +inputs under \strong{generic aliases} — \code{access} (int: -9 absent / 0 +blocked / 1 modelled / 2 observed), \code{spawning}, \code{rearing} (bool) — +then aggregates by \verb{(watershed_group_code, species_code)}. } \details{ +The \code{streams_access} join is a LEFT join: access is optional metadata +for a length roll-up. When a segment has no \code{streams_access} row +(access not yet built for the WSG), \code{access} is \code{NULL} and +\code{accessible_km} resolves to 0 — build it via +\code{lnk_pipeline_run(mapping_code = TRUE)} (or the unconditional access +phase). Length is never dropped by a missing access row, so the +habitat metrics (\code{spawning_km}, \code{rearing_km}) are unaffected. + Because the per-species columns are aliased to fixed names, the \code{metrics} SQL is written \strong{once}, species-agnostic — mirroring \code{fresh::frs_aggregate()}'s \code{metrics} / \code{where} shape. Adding a species diff --git a/planning/active/progress.md b/planning/active/progress.md index 9f03e894..007f5478 100644 --- a/planning/active/progress.md +++ b/planning/active/progress.md @@ -27,5 +27,22 @@ sources `streams_access.access_ IN (1,2)`, not `streams_habitat.accessible`. - Discovered `streams_habitat_.accessible` bool diverges from the access model (MORR coho 3424 vs 3330 km) — do NOT sum it for accessible_km. -- Next: Phase 2 (2/4) — fold `.lnk_compare_rollup_link` habitat km sums into the - `lnk_rollup_wsg` path; then emit accessible_km as 8th habitat_type + update tests. +- **Phase 2 (1/4) doc tweak committed** (`f8ce11c`): `@seealso [lnk_aggregate()]` + + a flat-per-WSG-GROUP-BY vs per-crossing-upstream-network note, so the + rollup/aggregate split is intentional. Regenerated compare-family Rd + back-references (missed in the prior commit). +- **Phase 2 (2/4) done.** Folded `.lnk_compare_rollup_link`'s `km` block into + `lnk_rollup_wsg`: it now passes a 5-metric `metrics` vector (COALESCE→0 to + keep the old CASE-WHEN measured-zero, not NULL/NA), renames + `species`→`species_code`, drops `wsg`, preserving the + `list(km, lake_ha, wetland_ha)` contract. `lnk_rollup_wsg` streams_access + join `JOIN`→`LEFT JOIN` (access is optional metadata; absent → accessible_km + 0, habitat length never dropped). Verified byte-identical vs the old + CASE-WHEN SQL on MORR BT+CO across all 5 metrics (BT 951.33/1824.66/1272.03/ + 0/0, CO 915.97/1228.72/927.70/0/0); `lnk_rollup_wsg` accessible_km still + 3330.25. `lnk_pipeline_access` runs unconditionally so persist + `streams_access` always exists — fold is safe even for mapping_code=FALSE. + Dead working-schema `.lnk_compare_wsg_rollup_link` left untouched. +- Next: Phase 2 (3/4) — emit accessible_km as 8th habitat_type in + `.lnk_compare_wsg_assemble_rollup` + update row-count assertions + (7→8, 14→16) in `test-lnk_compare_wsg.R`. diff --git a/planning/active/task_plan.md b/planning/active/task_plan.md index b2c161ee..7bde2909 100644 --- a/planning/active/task_plan.md +++ b/planning/active/task_plan.md @@ -26,7 +26,16 @@ per-crossing roll-up is a later phase. Morice vignette is separate/later. verified MORR coho `accessible_km` = 3330.25 (matches Phase-1 proof). 27 unit tests. `accessible_km` sources `streams_access.access_ IN (1,2)`, NOT the divergent `streams_habitat_.accessible` bool (MORR 3330 vs 3424 km). -- [ ] Fold `.lnk_compare_rollup_link` habitat km sums into that single path. +- [x] Fold `.lnk_compare_rollup_link` habitat km sums into that single + path. `.lnk_compare_rollup_link`'s `km` block now delegates to + `lnk_rollup_wsg` via a 5-metric `metrics` vector (COALESCE→0 to + preserve the historical CASE-WHEN measured-zero semantics), renaming + `species`→`species_code` to keep the `list(km, lake_ha, wetland_ha)` + contract. `lnk_rollup_wsg`'s `streams_access` join changed + `JOIN`→`LEFT JOIN` so habitat length is never dropped when access is + unbuilt. Byte-identical vs the old form on MORR (BT/CO, all 5 + metrics). lake_ha / wetland_ha (DISTINCT-waterbody polygon joins) + stay as-is — different shape, not a per-segment length sum. - [ ] Emit `accessible_km` as an 8th habitat_type in `.lnk_compare_wsg_assemble_rollup`; update row-count assertions in `tests/testthat/test-lnk_compare_wsg.R`. diff --git a/tests/testthat/test-lnk_rollup_wsg.R b/tests/testthat/test-lnk_rollup_wsg.R index 41647129..a7ab093f 100644 --- a/tests/testthat/test-lnk_rollup_wsg.R +++ b/tests/testthat/test-lnk_rollup_wsg.R @@ -84,6 +84,10 @@ test_that(".lnk_rollup_wsg_sql builds one UNION ALL branch per species", { # Full-PK join (#203) on both access and habitat. expect_match(sql, "s\\.watershed_group_code = a\\.watershed_group_code") expect_match(sql, "s\\.watershed_group_code = h\\.watershed_group_code") + # streams_access is LEFT-joined (optional) so habitat length is never + # dropped when access is unbuilt; habitat is an inner join. + expect_match(sql, "LEFT JOIN fresh\\.streams_access") + expect_match(sql, "JOIN fresh\\.streams_habitat_co") # Metric aliases + grouping. expect_match(sql, "AS accessible_km") expect_match(sql, "AS spawning_km") From 0ced991cc8776a11aafc89326b87ed2652342a4f Mon Sep 17 00:00:00 2001 From: almac2022 Date: Thu, 2 Jul 2026 07:16:30 -0700 Subject: [PATCH 06/16] #221 Phase 2: emit accessible_km as 8th habitat_type in the rollup .lnk_compare_rollup_link appends accessible_km to km_metrics: round(COALESCE(sum(length_metre) FILTER (WHERE access IN (1,2)),0)/1000,2). It sources link's per-species access model via lnk_rollup_wsg's LEFT-joined `access` alias (streams_access.access_), NOT the divergent streams_habitat.accessible bool. streams_access's PK (id_segment, watershed_group_code) matches the join key, so the LEFT JOIN is 1:1 and cannot inflate any length sum. .lnk_compare_wsg_assemble_rollup gains `accessible` (unit km) in its four parallel named vectors. The bcfp habitat reference has no accessible column, so accessible's ref_value/diff_pct are NA by design until the tunnel-free reference path lands (Phase 2 4/4). Row-count assertions bumped 7->8 / 14->16 in test-lnk_compare_wsg.R and 7->8 in test-lnk_compare_rollup.R, each with a new accessible-row check (link value populated, ref NA). Live MORR coho accessible_km 3330.25 (= Phase-1 proof). Co-Authored-By: Claude Opus 4.7 --- R/lnk_compare_rollup.R | 23 +++++++++++------- R/lnk_compare_wsg.R | 30 ++++++++++++++---------- man/lnk_compare_rollup.Rd | 8 ++++--- man/lnk_compare_wsg.Rd | 9 +++---- planning/active/progress.md | 23 +++++++++++++++--- planning/active/task_plan.md | 12 ++++++++-- tests/testthat/test-lnk_compare_rollup.R | 14 +++++++++-- tests/testthat/test-lnk_compare_wsg.R | 23 +++++++++++++----- 8 files changed, 102 insertions(+), 40 deletions(-) diff --git a/R/lnk_compare_rollup.R b/R/lnk_compare_rollup.R index 73bc7daa..bc9da96c 100644 --- a/R/lnk_compare_rollup.R +++ b/R/lnk_compare_rollup.R @@ -41,10 +41,12 @@ #' (e.g. `c("BT","CO")`) to restrict the rollup to. Default `NULL` #' discovers the set from PG. #' -#' @return A tibble with one row per (species, habitat_type) — 7 -#' habitat types per species. Columns: `wsg`, `species`, +#' @return A tibble with one row per (species, habitat_type) — 8 +#' habitat types per species (the 7 habitat km/ha types plus +#' `accessible` km, link#221). Columns: `wsg`, `species`, #' `habitat_type`, `unit` (`km` | `ha`), `link_value`, `ref_value`, -#' `diff_pct`. +#' `diff_pct`. `accessible`'s `ref_value` is `NA` until the tunnel-free +#' reference path lands. #' #' @examples #' \dontrun{ @@ -198,10 +200,13 @@ lnk_compare_rollup <- function(conn, aoi, cfg, # Habitat km sums delegate to the single predicate-driven roll-up # primitive (link#221) so there is one per-(WSG, species) km query # builder. lnk_rollup_wsg exposes `length_metre` / `edge_type` / - # `spawning` / `rearing` under generic aliases; these five metrics - # reproduce the historical shape. It returns `wsg` + `species` + - # metrics — drop `wsg` and rename `species` -> `species_code` to - # preserve this helper's `list(km, lake_ha, wetland_ha)` contract. + # `access` / `spawning` / `rearing` under generic aliases; the first + # five metrics reproduce the historical shape, and `accessible_km` + # (link#221) sums link's per-species access model + # (`streams_access.access_ IN (1,2)`, LEFT-joined by + # lnk_rollup_wsg). It returns `wsg` + `species` + metrics — drop + # `wsg` and rename `species` -> `species_code` to preserve this + # helper's `list(km, lake_ha, wetland_ha)` contract. # COALESCE(..., 0): an empty FILTER set sums to NULL, but the # historical CASE-WHEN-ELSE-0 form returned a measured 0 for a # species present in the WSG with no segments in that slice. Preserve @@ -219,7 +224,9 @@ lnk_compare_rollup <- function(conn, aoi, cfg, et_lake_sql), rearing_wetland_centerline_km = sprintf( "round(COALESCE(sum(length_metre) FILTER (WHERE rearing AND edge_type IN %s), 0)::numeric / 1000, 2)", # nolint: line_length_linter - et_wetland_sql)) + et_wetland_sql), + accessible_km = + "round(COALESCE(sum(length_metre) FILTER (WHERE access IN (1, 2)), 0)::numeric / 1000, 2)") # nolint: line_length_linter km <- lnk_rollup_wsg( # nolint: object_usage_linter conn = conn, aoi = aoi, species = species, diff --git a/R/lnk_compare_wsg.R b/R/lnk_compare_wsg.R index d0918355..de66eb80 100644 --- a/R/lnk_compare_wsg.R +++ b/R/lnk_compare_wsg.R @@ -51,12 +51,13 @@ #' deprecation warning when supplied. #' #' @return A list with two elements: -#' - `rollup`: tibble with one row per (species, habitat_type) — 7 +#' - `rollup`: tibble with one row per (species, habitat_type) — 8 #' habitat types: `spawning`, `rearing`, `lake_rearing`, #' `wetland_rearing`, `rearing_stream`, `rearing_lake_centerline`, -#' `rearing_wetland_centerline`. Columns: `wsg`, `species`, -#' `habitat_type`, `unit` (`km` | `ha`), `link_value`, -#' `ref_value`, `diff_pct`. +#' `rearing_wetland_centerline`, `accessible` (km, link#221). +#' Columns: `wsg`, `species`, `habitat_type`, `unit` (`km` | `ha`), +#' `link_value`, `ref_value`, `diff_pct`. `accessible`'s `ref_value` +#' is `NA` until the tunnel-free reference path lands. #' - `mapping_code`: tibble with one row per species — segment-level #' match stats vs `bcfishpass.streams_mapping_code`. Columns: #' `wsg`, `species`, `total_segs`, `match_pct`, `n_diffs`, @@ -434,25 +435,28 @@ lnk_compare_wsg <- function(conn, aoi, cfg, loaded, #' Assemble long-format output tibble from link + ref rollup data #' -#' Produces 7 rows per species (spawning, rearing, lake_rearing, +#' Produces 8 rows per species (spawning, rearing, lake_rearing, #' wetland_rearing, rearing_stream, rearing_lake_centerline, -#' rearing_wetland_centerline). `diff_pct = NA` when `ref_value` is -#' `NA` (species not modelled by reference) or `0` (avoid div-by-zero -#' even when the measurement is real). +#' rearing_wetland_centerline, accessible). `diff_pct = NA` when +#' `ref_value` is `NA` (species not modelled by reference, or — for +#' `accessible` — the tunnel-free reference path not yet wired) or `0` +#' (avoid div-by-zero even when the measurement is real). #' #' @noRd .lnk_compare_wsg_assemble_rollup <- function(aoi, species, rollup_link, rollup_ref) { habitat_types <- c( "spawning", "rearing", "lake_rearing", "wetland_rearing", - "rearing_stream", "rearing_lake_centerline", "rearing_wetland_centerline" + "rearing_stream", "rearing_lake_centerline", "rearing_wetland_centerline", + "accessible" ) units <- c( spawning = "km", rearing = "km", lake_rearing = "ha", wetland_rearing = "ha", rearing_stream = "km", rearing_lake_centerline = "km", - rearing_wetland_centerline = "km" + rearing_wetland_centerline = "km", + accessible = "km" ) col_suffix <- c( spawning = "spawning_km", rearing = "rearing_km", @@ -460,7 +464,8 @@ lnk_compare_wsg <- function(conn, aoi, cfg, loaded, wetland_rearing = "wetland_rearing_ha", rearing_stream = "rearing_stream_km", rearing_lake_centerline = "rearing_lake_centerline_km", - rearing_wetland_centerline = "rearing_wetland_centerline_km" + rearing_wetland_centerline = "rearing_wetland_centerline_km", + accessible = "accessible_km" ) sp_col <- rep(species, each = length(habitat_types)) @@ -483,7 +488,8 @@ lnk_compare_wsg <- function(conn, aoi, cfg, loaded, wetland_rearing = rollup_link$wetland_ha, rearing_stream = rollup_link$km, rearing_lake_centerline = rollup_link$km, - rearing_wetland_centerline = rollup_link$km + rearing_wetland_centerline = rollup_link$km, + accessible = rollup_link$km ) for (i in seq_len(nrow(out))) { diff --git a/man/lnk_compare_rollup.Rd b/man/lnk_compare_rollup.Rd index b086c7f8..042ebf25 100644 --- a/man/lnk_compare_rollup.Rd +++ b/man/lnk_compare_rollup.Rd @@ -34,10 +34,12 @@ when \code{reference = "bcfishpass"} (bcfp tunnel at discovers the set from PG.} } \value{ -A tibble with one row per (species, habitat_type) — 7 -habitat types per species. Columns: \code{wsg}, \code{species}, +A tibble with one row per (species, habitat_type) — 8 +habitat types per species (the 7 habitat km/ha types plus +\code{accessible} km, link#221). Columns: \code{wsg}, \code{species}, \code{habitat_type}, \code{unit} (\code{km} | \code{ha}), \code{link_value}, \code{ref_value}, -\code{diff_pct}. +\code{diff_pct}. \code{accessible}'s \code{ref_value} is \code{NA} until the tunnel-free +reference path lands. } \description{ Comparison-only counterpart to \code{\link[=lnk_pipeline_run]{lnk_pipeline_run()}}. Reads the diff --git a/man/lnk_compare_wsg.Rd b/man/lnk_compare_wsg.Rd index 564266e7..df8f34d0 100644 --- a/man/lnk_compare_wsg.Rd +++ b/man/lnk_compare_wsg.Rd @@ -65,12 +65,13 @@ deprecation warning when supplied.} \value{ A list with two elements: \itemize{ -\item \code{rollup}: tibble with one row per (species, habitat_type) — 7 +\item \code{rollup}: tibble with one row per (species, habitat_type) — 8 habitat types: \code{spawning}, \code{rearing}, \code{lake_rearing}, \code{wetland_rearing}, \code{rearing_stream}, \code{rearing_lake_centerline}, -\code{rearing_wetland_centerline}. Columns: \code{wsg}, \code{species}, -\code{habitat_type}, \code{unit} (\code{km} | \code{ha}), \code{link_value}, -\code{ref_value}, \code{diff_pct}. +\code{rearing_wetland_centerline}, \code{accessible} (km, link#221). +Columns: \code{wsg}, \code{species}, \code{habitat_type}, \code{unit} (\code{km} | \code{ha}), +\code{link_value}, \code{ref_value}, \code{diff_pct}. \code{accessible}'s \code{ref_value} +is \code{NA} until the tunnel-free reference path lands. \item \code{mapping_code}: tibble with one row per species — segment-level match stats vs \code{bcfishpass.streams_mapping_code}. Columns: \code{wsg}, \code{species}, \code{total_segs}, \code{match_pct}, \code{n_diffs}, diff --git a/planning/active/progress.md b/planning/active/progress.md index 007f5478..e6163c84 100644 --- a/planning/active/progress.md +++ b/planning/active/progress.md @@ -43,6 +43,23 @@ 3330.25. `lnk_pipeline_access` runs unconditionally so persist `streams_access` always exists — fold is safe even for mapping_code=FALSE. Dead working-schema `.lnk_compare_wsg_rollup_link` left untouched. -- Next: Phase 2 (3/4) — emit accessible_km as 8th habitat_type in - `.lnk_compare_wsg_assemble_rollup` + update row-count assertions - (7→8, 14→16) in `test-lnk_compare_wsg.R`. +- **Phase 2 (2/4) committed** (`4be5b87`) after 3-round code-check clean. +- **Phase 2 (3/4) done.** accessible_km now emits as the 8th + habitat_type. Added `accessible_km` to `.lnk_compare_rollup_link`'s + `km_metrics` (`round(COALESCE(sum(length_metre) FILTER (WHERE access IN + (1,2)),0)/1000,2)`) — sources link's per-species access model via + lnk_rollup_wsg's LEFT-joined `access` alias, NOT the divergent + streams_habitat.accessible bool. Appended `accessible` (unit km) to + habitat_types/units/col_suffix/link_sources in + `.lnk_compare_wsg_assemble_rollup`. Its `ref_value`/`diff_pct` stay NA + (the bcfp habitat ref has no accessible column) until Phase 2 (4/4) + wires the tunnel-free `fresh.streams_vw_bcfp` ref. Row-count assertions + bumped 7→8 / 14→16 in test-lnk_compare_wsg.R + 7→8 in + test-lnk_compare_rollup.R, each with a new accessible-row check + (link populated, ref NA). Live MORR coho accessible_km 3330.25 + (= Phase-1 proof). 108 tests green; lint clean (only pre-existing + helper-name/indent notes); docs regenerated. +- Next: Phase 2 (4/4) — add tunnel-free `accessible_km` reference path + (`fresh.streams_vw_bcfp`, predicate `barriers_ch_cm_co_pk_sk_dnstr = + ''` for coho/salmon) so `accessible`'s ref_value/diff_pct populate. + Do NOT force-unify with the tunnel-based habitat ref. diff --git a/planning/active/task_plan.md b/planning/active/task_plan.md index 7bde2909..fb9409b5 100644 --- a/planning/active/task_plan.md +++ b/planning/active/task_plan.md @@ -36,9 +36,17 @@ per-crossing roll-up is a later phase. Morice vignette is separate/later. unbuilt. Byte-identical vs the old form on MORR (BT/CO, all 5 metrics). lake_ha / wetland_ha (DISTINCT-waterbody polygon joins) stay as-is — different shape, not a per-segment length sum. -- [ ] Emit `accessible_km` as an 8th habitat_type in +- [x] Emit `accessible_km` as an 8th habitat_type in `.lnk_compare_wsg_assemble_rollup`; update row-count assertions in - `tests/testthat/test-lnk_compare_wsg.R`. + `tests/testthat/test-lnk_compare_wsg.R`. Added `accessible_km` to + `.lnk_compare_rollup_link`'s `km_metrics` (COALESCE'd `access IN (1,2)` + via lnk_rollup_wsg's LEFT-joined `access` alias); appended `accessible` + (km) to habitat_types/units/col_suffix/link_sources in + `.lnk_compare_wsg_assemble_rollup`. `ref_value`/`diff_pct` are NA for + `accessible` until the tunnel-free ref lands (4/4). Row-count + assertions 7→8 / 14→16 updated in both test-lnk_compare_wsg.R and + test-lnk_compare_rollup.R (7→8). Live MORR coho accessible_km 3330.25 + (= Phase-1 proof). 108 tests across the 3 files pass; docs regenerated. - [ ] Keep habitat ref tunnel-based; add tunnel-free `accessible_km` ref path. Do NOT force-unify the two reference sources this phase. diff --git a/tests/testthat/test-lnk_compare_rollup.R b/tests/testthat/test-lnk_compare_rollup.R index f28378b8..5b6f571d 100644 --- a/tests/testthat/test-lnk_compare_rollup.R +++ b/tests/testthat/test-lnk_compare_rollup.R @@ -118,6 +118,7 @@ test_that("lnk_compare_rollup composes resolve → link-rollup → ref-rollup rearing_stream_km = 15, rearing_lake_centerline_km = 3, rearing_wetland_centerline_km = 2, + accessible_km = 18, stringsAsFactors = FALSE), lake_ha = data.frame(species_code = "BT", lake_rearing_ha = 100, stringsAsFactors = FALSE), @@ -148,10 +149,19 @@ test_that("lnk_compare_rollup composes resolve → link-rollup → ref-rollup ) expect_equal(calls, c("resolve", "link", "ref")) - # 7 habitat types × 1 species - expect_equal(nrow(result), 7L) + # 8 habitat types × 1 species (7 habitat + accessible, link#221) + expect_equal(nrow(result), 8L) expect_named(result, c("wsg", "species", "habitat_type", "unit", "link_value", "ref_value", "diff_pct")) expect_setequal(unique(result$species), "BT") expect_setequal(unique(result$wsg), "ADMS") + + # accessible row: link value flows from km$accessible_km; ref has no + # accessible column (tunnel-free ref path not yet wired) → NA diff. + acc <- result[result$habitat_type == "accessible", ] + expect_equal(nrow(acc), 1L) + expect_equal(acc$link_value, 18) + expect_equal(acc$unit, "km") + expect_true(is.na(acc$ref_value)) + expect_true(is.na(acc$diff_pct)) }) diff --git a/tests/testthat/test-lnk_compare_wsg.R b/tests/testthat/test-lnk_compare_wsg.R index d9b7bf71..99e0e95e 100644 --- a/tests/testthat/test-lnk_compare_wsg.R +++ b/tests/testthat/test-lnk_compare_wsg.R @@ -169,8 +169,8 @@ test_that("lnk_compare_wsg composes lnk_pipeline_run then lnk_compare_rollup (ro habitat_type = c("spawning", "rearing", "lake_rearing", "wetland_rearing", "rearing_stream", "rearing_lake_centerline", - "rearing_wetland_centerline"), - unit = c("km", "km", "ha", "ha", "km", "km", "km"), + "rearing_wetland_centerline", "accessible"), + unit = c("km", "km", "ha", "ha", "km", "km", "km", "km"), link_value = 1, ref_value = 1, diff_pct = 0) } @@ -200,14 +200,14 @@ test_that("lnk_compare_wsg composes lnk_pipeline_run then lnk_compare_rollup (ro expect_named(result, c("rollup", "mapping_code")) expect_null(result$mapping_code) expect_s3_class(result$rollup, "tbl_df") - expect_equal(nrow(result$rollup), 7L) + expect_equal(nrow(result$rollup), 8L) }) # --------------------------------------------------------------------------- # Rollup tibble shape + diff_pct computation # --------------------------------------------------------------------------- -test_that(".lnk_compare_wsg_assemble_rollup produces 7 rows per species + correct diff_pct", { +test_that(".lnk_compare_wsg_assemble_rollup produces 8 rows per species + correct diff_pct", { link_data <- list( km = data.frame( species_code = c("BT","CH"), @@ -216,6 +216,7 @@ test_that(".lnk_compare_wsg_assemble_rollup produces 7 rows per species + correc rearing_stream_km = c(150, 80), rearing_lake_centerline_km = c(30, 10), rearing_wetland_centerline_km = c(20, 10), + accessible_km = c(180, 90), stringsAsFactors = FALSE ), lake_ha = data.frame(species_code = c("BT","CH"), @@ -242,8 +243,8 @@ test_that(".lnk_compare_wsg_assemble_rollup produces 7 rows per species + correc rollup_link = link_data, rollup_ref = ref_data ) - # 7 habitat types × 2 species - expect_equal(nrow(out), 14L) + # 8 habitat types × 2 species (7 habitat + accessible, link#221) + expect_equal(nrow(out), 16L) expect_named(out, c("wsg", "species", "habitat_type", "unit", "link_value", "ref_value", "diff_pct")) expect_setequal(unique(out$wsg), "TEST") @@ -254,6 +255,14 @@ test_that(".lnk_compare_wsg_assemble_rollup produces 7 rows per species + correc expect_equal(bt_spawn$ref_value, 99) expect_equal(bt_spawn$diff_pct, 1.0) + # accessible: link_value from km$accessible_km; ref lacks the column + # (tunnel-free ref not yet wired) → ref_value + diff_pct NA. + bt_acc <- out[out$species == "BT" & out$habitat_type == "accessible", ] + expect_equal(bt_acc$link_value, 180) + expect_equal(bt_acc$unit, "km") + expect_true(is.na(bt_acc$ref_value)) + expect_true(is.na(bt_acc$diff_pct)) + bt_lake <- out[out$species == "BT" & out$habitat_type == "lake_rearing", ] expect_equal(bt_lake$link_value, 1000) expect_equal(bt_lake$ref_value, 1000) @@ -267,6 +276,7 @@ test_that(".lnk_compare_wsg_assemble_rollup handles NA ref values (not modelled) rearing_stream_km = 180, rearing_lake_centerline_km = 15, rearing_wetland_centerline_km = 5, + accessible_km = 190, stringsAsFactors = FALSE), lake_ha = data.frame(species_code = "RB", lake_rearing_ha = 0, stringsAsFactors = FALSE), @@ -416,6 +426,7 @@ test_that(".lnk_compare_wsg_assemble_rollup handles zero ref values (avoid div-b rearing_stream_km = 180, rearing_lake_centerline_km = 0, rearing_wetland_centerline_km = 0, + accessible_km = 190, stringsAsFactors = FALSE), lake_ha = data.frame(species_code = "BT", lake_rearing_ha = 0, stringsAsFactors = FALSE), From aa65d680afff037070cbb870b6a6f1dbae54ed4c Mon Sep 17 00:00:00 2001 From: almac2022 Date: Thu, 2 Jul 2026 07:31:12 -0700 Subject: [PATCH 07/16] #221 Phase 2: tunnel-free accessible_km reference (salmon group) .lnk_compare_wsg_accessible_ref(conn, aoi, species) sums fresh.streams_vw_bcfp.length_metre where a species' bcfp barrier-group column = '' (empty = no barrier downstream = accessible). Sourced from the local province-wide snapshot (conn), NOT the dead M1 :63333 tunnel. Only the Phase-1-proven salmon group (CH/CM/CO/PK/SK -> barriers_ch_cm_co_pk_sk_dnstr) is wired; other species short-circuit to NA without a query (each needs its own proof, Phase 3). barrier_group values are a trusted constant whitelist; aoi goes through DBI::dbQuoteLiteral. .lnk_compare_wsg_rollup_reference gains a local conn param and merges accessible_km onto the tunnel-based habitat-ref data.frame. The two reference sources stay intentionally decoupled: habitat stays tunnel-based on conn_ref; accessible is tunnel-free on conn. lnk_compare_rollup passes conn = conn. Live: MORR CO ref 3327.38 vs link 3330.25 -> diff_pct 0.1% (= Phase-1 proof); BT ref NA. SK ref = CO ref (bcfp models the salmon group with one shared column; per-species vs per-group coarsening surfaces in the diff, documented for Phase 3). 4 new helper tests (mock DBI::dbGetQuery: salmon -> query with the barriers_ch_cm_co_pk_sk_dnstr predicate; BT/ST/WCT -> short-circuit NA, no query). m_ref mock signature gained conn. 65+20 tests green in the two touched files; lint no new indent notes; docs regenerated. Co-Authored-By: Claude Opus 4.7 --- R/lnk_compare_rollup.R | 7 +-- R/lnk_compare_wsg.R | 54 +++++++++++++++++++++--- man/lnk_compare_rollup.Rd | 5 ++- planning/active/progress.md | 27 ++++++++++-- planning/active/task_plan.md | 16 ++++++- tests/testthat/test-lnk_compare_rollup.R | 2 +- tests/testthat/test-lnk_compare_wsg.R | 49 +++++++++++++++++++++ 7 files changed, 142 insertions(+), 18 deletions(-) diff --git a/R/lnk_compare_rollup.R b/R/lnk_compare_rollup.R index bc9da96c..a281297f 100644 --- a/R/lnk_compare_rollup.R +++ b/R/lnk_compare_rollup.R @@ -45,8 +45,9 @@ #' habitat types per species (the 7 habitat km/ha types plus #' `accessible` km, link#221). Columns: `wsg`, `species`, #' `habitat_type`, `unit` (`km` | `ha`), `link_value`, `ref_value`, -#' `diff_pct`. `accessible`'s `ref_value` is `NA` until the tunnel-free -#' reference path lands. +#' `diff_pct`. `accessible`'s `ref_value` is sourced tunnel-free from +#' `fresh.streams_vw_bcfp` for the salmon group (CH/CM/CO/PK/SK); other +#' species carry `NA` until their reference path lands (link#221 Phase 3). #' #' @examples #' \dontrun{ @@ -122,7 +123,7 @@ lnk_compare_rollup <- function(conn, aoi, cfg, conn = conn, cfg = cfg, aoi = aoi, species = species) rollup_ref <- .lnk_compare_wsg_rollup_reference( # nolint reference = reference, conn_ref = conn_ref, - aoi = aoi, species = species) + aoi = aoi, species = species, conn = conn) .lnk_compare_wsg_assemble_rollup( # nolint aoi = aoi, species = species, diff --git a/R/lnk_compare_wsg.R b/R/lnk_compare_wsg.R index de66eb80..9099cf77 100644 --- a/R/lnk_compare_wsg.R +++ b/R/lnk_compare_wsg.R @@ -312,21 +312,63 @@ lnk_compare_wsg <- function(conn, aoi, cfg, loaded, #' Compute reference-side rollup queries for the requested `reference` #' #' Dispatches on `reference`. Currently only `"bcfishpass"` is wired — -#' queries `bcfishpass.habitat_linear_` per species, joined to the -#' same `fwa_lakes_poly` / `fwa_wetlands_poly` for the ha columns -#' bcfp doesn't materialize natively. +#' queries `bcfishpass.habitat_linear_` per species (on `conn_ref`, +#' the tunnel), joined to the same `fwa_lakes_poly` / `fwa_wetlands_poly` +#' for the ha columns bcfp doesn't materialize natively. The `accessible_km` +#' reference is sourced separately + tunnel-free from `fresh.streams_vw_bcfp` +#' on the local `conn` (see [.lnk_compare_wsg_accessible_ref()]); the two +#' reference sources stay intentionally decoupled (link#221). #' #' @noRd .lnk_compare_wsg_rollup_reference <- function(reference, conn_ref, - aoi, species) { + aoi, species, conn) { if (reference == "bcfishpass") { - return(.lnk_compare_wsg_rollup_bcfishpass( - conn_ref = conn_ref, aoi = aoi, species = species)) + ref <- .lnk_compare_wsg_rollup_bcfishpass( + conn_ref = conn_ref, aoi = aoi, species = species) # nolint: indentation_linter + ref$accessible_km <- .lnk_compare_wsg_accessible_ref( + conn = conn, aoi = aoi, species = ref$species_code) # nolint: indentation_linter + return(ref) } stop("Unknown reference '", reference, "' in dispatch.", call. = FALSE) } +#' Tunnel-free `accessible_km` reference from `fresh.streams_vw_bcfp` +#' +#' Sums `length_metre` where the species' bcfp barrier-group column is +#' empty (`= ''` = no barrier downstream = accessible). Sourced from the +#' local province-wide snapshot (`conn`), NOT the tunnel — the `:63333` +#' tunnel is dead on M1 and the snapshot is authoritative. Only the +#' Phase-1-proven salmon group (CH/CM/CO/PK/SK → +#' `barriers_ch_cm_co_pk_sk_dnstr`) is wired; other species short-circuit +#' to `NA` (their reference lands in a later phase, each needing its own +#' proof). `barrier_group` values are a trusted constant whitelist — safe +#' to interpolate. +#' +#' @noRd +.lnk_compare_wsg_accessible_ref <- function(conn, aoi, species) { + barrier_group <- c( + CH = "barriers_ch_cm_co_pk_sk_dnstr", + CM = "barriers_ch_cm_co_pk_sk_dnstr", + CO = "barriers_ch_cm_co_pk_sk_dnstr", + PK = "barriers_ch_cm_co_pk_sk_dnstr", + SK = "barriers_ch_cm_co_pk_sk_dnstr" + ) + aoi_lit <- DBI::dbQuoteLiteral(conn, aoi) + vapply(species, function(sp) { + grp <- barrier_group[toupper(sp)] + if (is.na(grp)) return(NA_real_) + res <- DBI::dbGetQuery(conn, sprintf(" + SELECT round((COALESCE(sum(length_metre), 0) / 1000)::numeric, 2) + AS accessible_km + FROM fresh.streams_vw_bcfp + WHERE watershed_group_code = %s AND %s = ''", + aoi_lit, unname(grp))) # nolint: indentation_linter + res$accessible_km[1] + }, numeric(1), USE.NAMES = FALSE) +} + + #' Reference dispatch: bcfishpass tunnel #' #' Mirrors the link-side methodology applied to `bcfishpass.habitat_linear_` diff --git a/man/lnk_compare_rollup.Rd b/man/lnk_compare_rollup.Rd index 042ebf25..d6b072fb 100644 --- a/man/lnk_compare_rollup.Rd +++ b/man/lnk_compare_rollup.Rd @@ -38,8 +38,9 @@ A tibble with one row per (species, habitat_type) — 8 habitat types per species (the 7 habitat km/ha types plus \code{accessible} km, link#221). Columns: \code{wsg}, \code{species}, \code{habitat_type}, \code{unit} (\code{km} | \code{ha}), \code{link_value}, \code{ref_value}, -\code{diff_pct}. \code{accessible}'s \code{ref_value} is \code{NA} until the tunnel-free -reference path lands. +\code{diff_pct}. \code{accessible}'s \code{ref_value} is sourced tunnel-free from +\code{fresh.streams_vw_bcfp} for the salmon group (CH/CM/CO/PK/SK); other +species carry \code{NA} until their reference path lands (link#221 Phase 3). } \description{ Comparison-only counterpart to \code{\link[=lnk_pipeline_run]{lnk_pipeline_run()}}. Reads the diff --git a/planning/active/progress.md b/planning/active/progress.md index e6163c84..ecb506fa 100644 --- a/planning/active/progress.md +++ b/planning/active/progress.md @@ -59,7 +59,26 @@ (link populated, ref NA). Live MORR coho accessible_km 3330.25 (= Phase-1 proof). 108 tests green; lint clean (only pre-existing helper-name/indent notes); docs regenerated. -- Next: Phase 2 (4/4) — add tunnel-free `accessible_km` reference path - (`fresh.streams_vw_bcfp`, predicate `barriers_ch_cm_co_pk_sk_dnstr = - ''` for coho/salmon) so `accessible`'s ref_value/diff_pct populate. - Do NOT force-unify with the tunnel-based habitat ref. +- **Phase 2 (4/4) done.** Tunnel-free `accessible_km` reference wired. + New `.lnk_compare_wsg_accessible_ref(conn, aoi, species)` sums + `fresh.streams_vw_bcfp.length_metre WHERE = ''` on the + LOCAL conn (snapshot authoritative; tunnel dead on M1). Only the + Phase-1-proven salmon group is wired (CH/CM/CO/PK/SK → + `barriers_ch_cm_co_pk_sk_dnstr`); BT/ST/WCT/CT-DV-RB short-circuit to + NA (each needs its own proof — Phase 3). Threaded local `conn` through + `.lnk_compare_wsg_rollup_reference` (habitat ref stays tunnel-based on + `conn_ref`; the two reference sources are intentionally decoupled) and + `lnk_compare_rollup`'s call site. Live: MORR CO ref 3327.38 vs link + 3330.25 → diff_pct 0.1% (matches Phase-1); BT ref NA. SK ref = CO ref + (bcfp models the salmon group with one shared column — per-species vs + per-group coarsening surfaces in the diff, documented for Phase 3). + 4 new helper tests (mock `DBI::dbGetQuery`); m_ref mock gained `conn`. + 65+20 tests green in the two touched files; lint no new indent notes + (one object_length note on the >30-char helper name, matching the + established `.lnk_compare_wsg_*` family); docs regenerated. The one + full-suite FAIL (`public.wsg_outlet` missing) is pre-existing + + environmental, unrelated. +- Next: Phase 3 — extend `accessible_km` ref to BT + (`barriers_bt_dnstr`), ST (`barriers_st_dnstr`), WCT/CT-DV-RB; wire + into `lnk_parity_annotate()` + `research/bcfp_divergence_taxonomy.yml`. + Each species-group needs its own Phase-1-style proof before wiring. diff --git a/planning/active/task_plan.md b/planning/active/task_plan.md index fb9409b5..62f98641 100644 --- a/planning/active/task_plan.md +++ b/planning/active/task_plan.md @@ -47,8 +47,20 @@ per-crossing roll-up is a later phase. Morice vignette is separate/later. assertions 7→8 / 14→16 updated in both test-lnk_compare_wsg.R and test-lnk_compare_rollup.R (7→8). Live MORR coho accessible_km 3330.25 (= Phase-1 proof). 108 tests across the 3 files pass; docs regenerated. -- [ ] Keep habitat ref tunnel-based; add tunnel-free `accessible_km` ref path. Do - NOT force-unify the two reference sources this phase. +- [x] Keep habitat ref tunnel-based; add tunnel-free `accessible_km` ref path. Do + NOT force-unify the two reference sources this phase. Added + `.lnk_compare_wsg_accessible_ref(conn, aoi, species)` — sums + `fresh.streams_vw_bcfp.length_metre` where the species' bcfp + barrier-group column `= ''` (salmon group CH/CM/CO/PK/SK → + `barriers_ch_cm_co_pk_sk_dnstr`; other species short-circuit to NA + pending Phase 3). Threaded local `conn` into + `.lnk_compare_wsg_rollup_reference`, which now merges `accessible_km` + onto the tunnel-based habitat ref df (two sources stay decoupled). + `lnk_compare_rollup` passes `conn = conn`. Live MORR CO ref 3327.38 + vs link 3330.25 → diff_pct 0.1% (= Phase-1 proof); BT ref NA. 4 new + helper tests (mock `DBI::dbGetQuery`: salmon → query with + `barriers_ch_cm_co_pk_sk_dnstr = ''` predicate, BT/ST/WCT → + short-circuit NA, no query). m_ref mock signature gained `conn`. ## Phase 3 — All bcfp-modelled species into the parity compare - [ ] Extend `accessible_km` to salmon (`barriers_ch_cm_co_pk_sk_dnstr`), BT diff --git a/tests/testthat/test-lnk_compare_rollup.R b/tests/testthat/test-lnk_compare_rollup.R index 5b6f571d..ec0930b0 100644 --- a/tests/testthat/test-lnk_compare_rollup.R +++ b/tests/testthat/test-lnk_compare_rollup.R @@ -125,7 +125,7 @@ test_that("lnk_compare_rollup composes resolve → link-rollup → ref-rollup wetland_ha = data.frame(species_code = "BT", wetland_rearing_ha = 50, stringsAsFactors = FALSE)) } - m_ref <- function(reference, conn_ref, aoi, species) { + m_ref <- function(reference, conn_ref, aoi, species, conn) { calls <<- c(calls, "ref") data.frame(species_code = "BT", spawning_km = 11, rearing_km = 21, diff --git a/tests/testthat/test-lnk_compare_wsg.R b/tests/testthat/test-lnk_compare_wsg.R index 99e0e95e..35b79c15 100644 --- a/tests/testthat/test-lnk_compare_wsg.R +++ b/tests/testthat/test-lnk_compare_wsg.R @@ -451,3 +451,52 @@ test_that(".lnk_compare_wsg_assemble_rollup handles zero ref values (avoid div-b zero_ref <- out[out$ref_value == 0, ] expect_true(all(is.na(zero_ref$diff_pct))) }) + +# --------------------------------------------------------------------------- +# Tunnel-free accessible_km reference (link#221 Phase 2 4/4) +# --------------------------------------------------------------------------- + +test_that(".lnk_compare_wsg_accessible_ref queries salmon group, short-circuits others", { + queries <- character() + m_get <- function(conn, statement, ...) { + queries <<- c(queries, statement) + data.frame(accessible_km = 3330.25) + } + + with_mocked_bindings( + dbGetQuery = m_get, + dbQuoteLiteral = function(conn, x, ...) paste0("'", x, "'"), + .package = "DBI", + { + out <- link:::.lnk_compare_wsg_accessible_ref( + conn = mock_conn(), aoi = "MORR", + species = c("CO", "BT", "SK")) + } + ) + + # CO + SK are salmon-group → one query each; BT short-circuits to NA + expect_equal(out, c(3330.25, NA_real_, 3330.25)) + expect_length(queries, 2L) + expect_true(all(grepl("barriers_ch_cm_co_pk_sk_dnstr = ''", queries))) + expect_true(all(grepl("fresh\\.streams_vw_bcfp", queries))) + expect_true(all(grepl("watershed_group_code = 'MORR'", queries))) +}) + +test_that(".lnk_compare_wsg_accessible_ref returns all-NA when no salmon species", { + m_get <- function(conn, statement, ...) { + stop("dbGetQuery should not be called for non-salmon species") + } + + with_mocked_bindings( + dbGetQuery = m_get, + dbQuoteLiteral = function(conn, x, ...) paste0("'", x, "'"), + .package = "DBI", + { + out <- link:::.lnk_compare_wsg_accessible_ref( + conn = mock_conn(), aoi = "MORR", + species = c("BT", "ST", "WCT")) + } + ) + + expect_equal(out, rep(NA_real_, 3)) +}) From 1a26f7d92e3538e2391dbc6c3d22e4ccb7562573 Mon Sep 17 00:00:00 2001 From: almac2022 Date: Fri, 3 Jul 2026 09:14:54 -0700 Subject: [PATCH 08/16] research: add blk 359209845 BT accessible_km segmentation proof figure Visual proof of the accessible_km BT over-credit: link's segment [3391,7998] spans the 25% gradient barrier at 3835 and stays accessible whole, while bcfp breaks at 3835 and blocks [3835,7998] (4163 m). Referenced by research/accessible_km_divergence.md and the tracking issue. Co-Authored-By: Claude Opus 4.7 --- research/blk359209845_bt_accessible_km.png | Bin 0 -> 231936 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 research/blk359209845_bt_accessible_km.png diff --git a/research/blk359209845_bt_accessible_km.png b/research/blk359209845_bt_accessible_km.png new file mode 100644 index 0000000000000000000000000000000000000000..d72dce63e103ea2715e5943970d9760d6ae1d490 GIT binary patch literal 231936 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 (azx~d<&l8 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 OHO8kcP8FVXiTFRD;cRXI literal 0 HcmV?d00001 From 4549713ffae80ac653e94faa918c8c44040645a2 Mon Sep 17 00:00:00 2001 From: almac2022 Date: Fri, 3 Jul 2026 09:42:51 -0700 Subject: [PATCH 09/16] research: root-cause the BT/ST accessible_km over-credit (#223) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The accessible_km parity gap for BT/ST is a segmentation defect, not an access-decision defect. The per-species gradient/falls break source is built by running frs_barriers_minimal() (a downstream-most reduction) on each barrier set, which strips the interior break points; a link segment then straddles the accessibility frontier and the whole reach — including the blocked part above the barrier — is labelled accessible. At co-located positions link and bcfp agree on BT accessibility at 99.99%. Documents the code path, the blk 359209845 segment-level proof (4163 m over-credit on one blue line; aggregate FINA +1438 km / PCEA +2021 km), why it stayed hidden at 99%+ parity, and the fix plan: break at the full per-species barrier positions and reclassify like bcfp — no length clipping, no barrier moved. Also captures the accessible_km parity findings from the #221 investigation. Filed as #223. Relates to #221, #203. Co-Authored-By: Claude Opus 4.7 --- planning/active/findings.md | 129 ++++++++ research/accessible_km_divergence.md | 450 +++++++++++++++++++++++++++ 2 files changed, 579 insertions(+) create mode 100644 research/accessible_km_divergence.md diff --git a/planning/active/findings.md b/planning/active/findings.md index f0639eee..c8d56c85 100644 --- a/planning/active/findings.md +++ b/planning/active/findings.md @@ -60,6 +60,135 @@ salmon group at scale. - AC2 (untestable ad-hoc SQL) → Phase 1 deliverable is a reproducible `data-raw/` script with a hard ≤5% assertion. +## Phase 3 — per-species proofs (2026-07-02, local docker fwapg) + +Local persist coverage: `access_bt` populated (47 WSGs), `access_st` (14 WSGs), +`access_wct` column present but **0 rows populated**, `access_ct/dv/rb` columns +**absent**. So only BT + ST are provable locally; WCT/CT/DV/RB have no data. + +### ST (`access_st` ↔ `barriers_st_dnstr`) — passes +14 WSGs; 3 over ±5%: **SETN +90.6%** (known `setn-anadr-*` salmon-stale class; +ST is in that taxonomy entry), **FRCN +6.19%**, **USKE +9.45%** (link ≥ bcfp; +same steep-reach class as BT below, smaller at ST's 0.20 threshold). + +### BT (`access_bt` ↔ `barriers_bt_dnstr`) — does NOT pass ±5% +47 WSGs; **12 over ±5%, every WSG link ≥ bcfp** (systematic positive bias): +PCEA +40.4, UFRA +24.5, FINA +23.6, FIRE +13.3, UNRS +12.1, FOXR/USKE/LFRA/FRCN +~8, BBAR +7.2, HARR +5.8, MORK +5.4. LBTN/LPCE NULL-ref = **snapshot coverage +gap** (0 rows in `streams_vw_bcfp`), not a divergence. + +### Root-cause investigation (PCEA + USKE) +- The link-extra (~2020 km on PCEA) is **diffuse** — spread across 1676 + blue_line_keys, top single-blk only 2 km. Not a handful of miscoded barriers. +- **Gradient histogram (PCEA link BT-accessible km):** the gap is almost + entirely in steep reaches — link credits ~2315 km at gradient ≥20% vs bcfp + ~577 km (measure-join). link 1293 km at ≥30% vs bcfp ~218. +- **Not a persist/recompute artifact.** Control (USKE, km above each species' + OWN `access_gradient_max`): CO 0.1% (thr 0.15), BT 6.4% (0.25), ST 7.9% + (0.20). link gates correctly at every threshold; the small above-threshold + residual is a segment-averaging boundary effect that scales with the + threshold (more steep terrain reachable → more boundary segments). +- **Divergence magnitude ranks monotonically by `access_gradient_max`:** + CO 0.15 (clean, proven ≤0.27%) < ST 0.20 (minor) < BT 0.25 (material). link + and bcfp share the same 0.25 BT threshold in `configs/bcfishpass/parameters_fresh.csv`, + so the gap is in gradient-barrier **placement / access-tracing**, not the + threshold value. + +**Verdict:** BT accessible_km divergence is a genuine, systematic, BT-specific +**steep-reach gradient-barrier methodology divergence** (link credits more steep +access than bcfp; likely link-defensible but unadjudicated). It is NOT a bug, +NOT a persist artifact, NOT barrier miscoding. Adjudicating link-vs-bcfp gradient +barrier placement needs a fresh-side reconciliation — out of scope for #221. +Recommendation: keep the accessible_km ref salmon-group-only (proven) for this +PR; do not wire BT/ST past their proofs yet; file a follow-up issue for the +steep-reach reconciliation covering the higher-threshold species (BT/ST/WCT/CT/DV/RB). + +## Phase 3 mechanism deep-dive (2026-07-02) — CORRECTS the verdict above + +The "gradient-barrier placement methodology divergence" verdict above is +**wrong / incomplete**. This session traced the actual code on both sides and +ruled placement OUT: + +### Gradient-barrier DETECTION is byte-identical link↔bcfp +- fresh `frs_break_find()` multiclass (`fresh/R/frs_break.R`, + `.frs_break_find_multiclass`) is a faithful port of bcfp + `gradient_barriers_load.sql` (`smnorris/bcfishpass@f4ae29d`): same 100 m + upstream window, same per-FWA-vertex `ST_LocateAlong` elevation-delta + method on `whse_basemapping.fwa_stream_networks_sp`, same + `blue_line_key = watershed_key` mainstem filter, `min_length=0` default. +- **FWA input is byte-identical** between local docker fwapg (:5432) and the + bcfp tunnel (:63333): USKE = 23458 streams / 9,436,876 m on both. +- **BT effective cutoff identical:** bcfp blocks BT on `GRADIENT_25 + GRADIENT_30` + (`model_access_bt.sql:16`) = gradient ≥ 0.25; fresh's open-ended top class + `[0.25,∞)` = same. Salmon: bcfp `GRADIENT_15/20/25/30` = ≥0.15 (matches + link 0.15). CT/DV/RB: `GRADIENT_25/30` = ≥0.25. +- ⇒ Same method + same data + same cutoff ⇒ gradient barriers **land at the + same positions**. Placement is NOT the divergence. + +### The real suspect: BT barrier-SET composition + km measurement +bcfp's `barriers_bt_dnstr` (`model_access_bt.sql`) is: +`gradient(≥0.25) ∪ barriers_falls ∪ barriers_subsurfaceflow ∪ barriers_user_definite`, +**MINUS** any barrier with a BT/salmon/steelhead **observation upstream** (20 m +tol) OR confirmed **habitat upstream** (200 m tol). link ≥ bcfp (link credits +more) ⇒ link applies FEWER effective downstream barriers than bcfp. Leading +candidates (UNRESOLVED — this is the fresh-issue diagnosis): +1. Non-gradient barrier types bcfp includes that link's `access_bt` weights + differently (falls, subsurfaceflow — note SETN subsurface-stale is already + documented; but USKE/PCEA gap is steep-reach-concentrated, not obviously + subsurface). +2. Per-segment km measurement on different segment definitions + + downstream-trace: same barrier vertex, but a link segment spanning the + barrier gets credited accessible as a unit (segment-averaging boundary + effect) — scales with threshold because steeper terrain = more/longer + boundary segments. +3. bcfp's obs/habitat-upstream barrier EXCLUSION differing from link's. + +### Diagnostic RAN 2026-07-02 (FINA) — placement DECISIVELY ruled out +Compared raw gradient-barrier landing positions link-vs-bcfp on FINA BT: +link `working_fina.gradient_barriers_raw` (:5432) vs tunnel +`bcfishpass.gradient_barriers` (:63333). **≥0.25 set: link 23,507 vs bcfp +23,483; all 23,483 bcfp barriers present in link at identical `(blk, DRM±1m)`; +bcfp-only = 0; link +24 (0.1%).** Strict positional superset → raw gradient +placement is NOT the divergence (the 24 extra would make link *more* +restrictive, opposite to the over-credit). The +23.6% is entirely downstream of +raw detection. Full write-up + reproduce commands + narrowed hypotheses (H1 +assembled-set composition / H2 segment measurement / H3 access trace) now live +in the durable **`research/accessible_km_divergence.md`** (this findings.md is +task-scoped + will be archived). Next: H1 — diff `fresh.barriers_bt_unified` vs +`bcfishpass.barriers_bt` (FINA) by `barrier_source`. + +### RESOLVED 2026-07-03 — segmentation granularity, NOT access model +Per-segment confusion matrix (both DBs local on :5432, join on the same FWA +position key `(blue_line_key, round(drm,3))` that `lnk_compare_mapping_code()` +uses) settles it. Link-weighted FINA BT: 26089/26094 link segs co-locate with a +bcfp start; **count-match 99.98%, km-match 99.99%, `link_only` over-credit = 0 +km**. Ref-weighted, bcfp's finer-only (no co-located link start) segments: + +| WSG | agg gap link_bt−ref_bt | bcfp extra-seg km | blocked | +|---|---:|---:|---:| +| FINA | +1436 | 1438 | 1438 (100%) | +| PARS | +234 | 245 | 238 | +| PCEA | +2020 | 2021 | 2021 (100%) | + +The over-credit **equals** bcfp's finer-only blocked km, 3/3. Mechanism: bcfp +sub-segments the identical FWA network finer at gradient-barrier boundaries and +blocks the steep above-barrier tail; link keeps it inside a coarser accessible +segment. link & bcfp AGREE on the access decision at 99.99% of shared positions. +99.57% mapping_code parity coexists because that metric is an INNER join +(`R/lnk_compare_mapping_code.R:229`) + count-weighted (`:276`) — it silently +drops bcfp's finer-only segments. Full evidence + reproduce + verdict in the +durable **`research/accessible_km_divergence.md` ("## RESOLVED")**. This is H2 +from the earlier list; H1 (barrier-set composition) is moot — barriers are +identical and access agrees. + +### Deliverable state +`/tmp/fresh_issue_access.md` drafted but **NOT filed** — and now needs a full +rewrite to the resolved framing: it is a **segmentation-granularity** finding +(link doesn't break at gradient-full boundaries where bcfp does), likely a +**link** issue not a fresh one, or an INTENTIONAL-divergence taxonomy entry. +No code committed; nothing wired in link. #221 decision unchanged + now firmly +grounded: ship salmon-group accessible_km (proven ≤0.27%), keep BT/ST deferred. + ## Issue context Issue #221 body: goal is per-WSG length roll-up emitting accessible_km + diff --git a/research/accessible_km_divergence.md b/research/accessible_km_divergence.md new file mode 100644 index 00000000..585aecce --- /dev/null +++ b/research/accessible_km_divergence.md @@ -0,0 +1,450 @@ +# accessible_km divergence — high-threshold species (BT / ST / CT-DV-RB) + +**Status:** RESOLVED + ROOT-CAUSED (2026-07-03) — mechanism is **segmentation +granularity**: link's streams don't break at gradient barriers because the break source +`gradient_barriers_minimal` is the `frs_barriers_minimal()` downstream-most reduction +(correct for the access DECISION, wrong as a SEGMENTATION source). **Fixable link +defect** — break at the full per-species barrier positions + reclassify like bcfp (do +NOT clip); `barriers__access` already holds the barrier, so no barrier-set change is +needed. See "## ROOT CAUSE" + "## RESOLVED" below. Living doc — append dated entries. +**Relates to:** link#221 (per-WSG `accessible_km` parity), `planning/active/findings.md` +(task-scoped, will be archived), and **link#223** (the tracking issue — filed 2026-07-03, +root-cause framing + embedded PNG). **Visual proof:** `research/blk359209845_bt_accessible_km.png` +(committed `1a26f7d`; generator `/tmp/blk_proof.R`) — blk 359209845 FINA BT, the +`[3835, 7998]` reach link keeps whole & accessible while bcfp blocks it. + +This doc exists because this class of problem (link-vs-bcfp parity divergence) has +historically burned tens of hours in **misdiagnosis-after-misdiagnosis**. Its +primary value is: **what has been DECISIVELY ruled out (with reproducible +evidence), so no one re-chases it**, plus the exact commands to continue. + +--- + +## TL;DR (as of 2026-07-02) + +- **Symptom:** link's `accessible_km` for BT systematically **exceeds** bcfp, up to + **+40% (PCEA)**, **+23.6% (FINA)**; **every WSG link ≥ bcfp**; magnitude scales + **monotonically** with the species `access_gradient_max` (CO 0.15 clean ≤0.27% → + ST 0.20 minor → BT 0.25 material). Extra km is **diffuse** (PCEA: ~2020 km over + 1676 blue_line_keys, no dominant BLK) and **concentrated in steep reaches**. +- **DECISIVELY RULED OUT this session:** raw gradient-barrier **placement**. On FINA, + link's ≥0.25 gradient barriers are a **strict positional superset** of bcfp's — + every bcfp barrier present in link at identical `(blue_line_key, DRM±1m)`, + `bcfp-only = 0`, link +24 (0.1%). See §"Ruled out #4". +- **Therefore the +23.6% is 100% downstream of raw gradient detection.** Narrowed to + three candidates (§"Narrowed hypotheses"): assembled barrier-**set composition**, + per-segment km **measurement/segmentation**, or the access **trace/exclusion**. +- **RESOLVED 2026-07-03 (§"## RESOLVED"):** none of the three narrowed hypotheses in + the "barrier set / access trace" family — it's **H2, segmentation granularity**. + bcfp sub-segments the identical FWA network finer than link at gradient-barrier + boundaries; every bcfp-only sub-segment is the **blocked** steep reach above the + barrier. link keeps it inside a coarser accessible segment. The over-credit km + **equals** bcfp's finer-only blocked km, 3/3 WSGs. link & bcfp agree on the access + DECISION at 99.99% of shared segment positions. +- **ROOT-CAUSED 2026-07-03 (§"## ROOT CAUSE"):** the segmentation difference is one link + code path — `gradient_barriers_minimal` (a stream-break source) is built by + `fresh::frs_barriers_minimal()` (`lnk_pipeline_prepare.R:592`), the downstream-most + per-flow-path reduction. Right for the ACCESS decision, but it strips the interior + break points, so link's segments straddle gradient barriers. **Fixable**: break at the + full barrier positions + reclassify (NOT clip) — `barriers__access` already contains + the barrier. Fix in a separate branch, then prove bcfp-equivalence in the vignette. + +--- + +## RESOLVED (2026-07-03) — segmentation-granularity length-apportionment + +**One-line:** link and bcfp agree on BT accessibility at 99.99% of co-located segment +starts; the aggregate `accessible_km` gap is **entirely** bcfp sub-segmenting the same +network finer at gradient-barrier boundaries and blocking the above-barrier tail that +link keeps whole inside a coarser accessible segment. Not a bug, not barrier placement, +not access logic, not rollup arithmetic — a **measurement granularity** difference. + +### The decisive test (per-segment confusion matrix, both DBs local on :5432) + +Both `fresh.streams`⋈`fresh.streams_access` (link) and `fresh.streams_vw_bcfp` (bcfp +ref) live on `:5432`, so they join directly on the FWA position key +`(blue_line_key, round(downstream_route_measure,3))` — the **same key** +`lnk_compare_mapping_code()` uses (`R/lnk_compare_mapping_code.R:229`). `access_bt IN +(1,2)` = link-accessible; `barriers_bt_dnstr = ''` = bcfp-accessible. + +**Link-weighted (FINA):** 26089/26094 link segments find a co-located bcfp start +(99.98% coverage; 11731/11733 km). Of that, **count-match 99.98%, km-match 99.99%**; +`link_only` (link-acc & bcfp-blocked) = **0 km**. So at shared positions the access +decision is identical — link does NOT flag co-located segments more generously. + +**Ref-weighted, all 3 WSGs** (`ref LEFT JOIN link`, bcfp segments with NO co-located +link start = bcfp's finer-only pieces): + +| WSG | agg gap `link_bt−ref_bt` | bcfp extra-seg n | extra-seg km | of which **blocked** | +|---|---:|---:|---:|---:| +| FINA | 7521−6085 = **+1436** | 2589 | 1438 | **1438** (100%) | +| PARS | 7057−6823 = **+234** | 540 | 245 | 238 (97%) | +| PCEA | 7023−5003 = **+2020** | 4258 | 2021 | **2021** (100%) | + +The over-credit **= bcfp's finer-only blocked km**, to rounding, 3/3. bcfp's extra +segments are ~99–100% blocked (they're the steep reach above the gradient barrier); +link has no break there so the reach stays in a larger accessible segment. + +### Why 99.57% mapping_code parity coexists with +23.6% accessible_km + +`lnk_compare_mapping_code()` is an **INNER** merge on position +(`R/lnk_compare_mapping_code.R:229-232`) and `match_pct = n_match/n_total` is +**segment-COUNT-weighted** (`:276`). The inner join **silently drops bcfp's finer-only +segments** — exactly the 1438 km that diverge — so parity never sees them. +accessible_km sums both full segmentations independently, so it does. Both metrics are +internally correct; they measure different populations. (Implication for #175: the +count-weighted, inner-join parity metric has a blind spot to segmentation granularity +at gradient boundaries for high-threshold species. The habitat-km rollups are unaffected +in kind but share the sensitivity.) + +### Why monotonic in `access_gradient_max` + +Higher threshold → species accesses steeper terrain → more accessible segments abut a +gradient barrier → more boundary sub-segments where link's coarser break over-credits +the above-barrier tail. CO 0.15 accesses little steep terrain (few boundaries → ≤0.27%); +BT 0.25 accesses more (many boundaries → material). Confirms the earlier USKE control +(only 6.4% of link BT-accessible km lies above 0.25 — link isn't leaking broadly; it's a +boundary-resolution effect, mean bcfp extra-seg length ~555 m on FINA). + +### Reproduce (single query, :5432, no tunnel) + +```bash +PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d fwapg -P pager=off -c " +WITH link AS ( + SELECT s.watershed_group_code AS wsg, s.blue_line_key AS blk, + round(s.downstream_route_measure::numeric,3) AS drm, + s.length_metre AS len, (a.access_bt IN (1,2)) AS link_acc + FROM fresh.streams s JOIN fresh.streams_access a + ON s.id_segment=a.id_segment AND s.watershed_group_code=a.watershed_group_code + WHERE s.watershed_group_code IN ('FINA','PARS','PCEA')), +ref AS ( + SELECT watershed_group_code AS wsg, blue_line_key AS blk, + round(downstream_route_measure::numeric,3) AS drm, + length_metre AS len, (barriers_bt_dnstr='') AS ref_acc + FROM fresh.streams_vw_bcfp WHERE watershed_group_code IN ('FINA','PARS','PCEA')) +SELECT r.wsg, + round((sum(r.len) FILTER (WHERE NOT r.ref_acc AND l.blk IS NULL)/1000.0)::numeric,0) AS unmatched_blk_km, + count(*) FILTER (WHERE l.blk IS NULL) AS unmatched_n +FROM ref r LEFT JOIN link l ON l.wsg=r.wsg AND l.blk=r.blk AND l.drm=r.drm +GROUP BY r.wsg ORDER BY r.wsg;" +``` + +Gotcha: `round(/1000.0, n)` fails (`round(double,int) does not exist`) — wrap the +whole expression `(expr)::numeric` INSIDE `round`, not the divisor. + +### Verdict + implications + +- **Not a link defect in placement or access logic.** Barriers land identically + (§"Ruled out #4"); access agrees at 99.99% of shared positions. +- **It IS a granularity accuracy gap in the *product*:** link labels ~1438 km (FINA) / + 2021 km (PCEA) of above-gradient-barrier steep reach as BT-accessible because its + segments span the barrier. Physically that reach is not BT-accessible; bcfp (finer + breaks) blocks it. For salmon (0.15) the gap is negligible (≤0.27%). +- **The reconciliation is a concrete fix, not a defer** (see §"## ROOT CAUSE"): build the + segmentation break source from the **full** per-species barrier positions instead of the + `frs_barriers_minimal()` reduction, so link's streams break at every gradient barrier + like bcfp; the existing access trace then reclassifies the above-barrier segment as + blocked. **Do NOT clip** roll-up lengths and do NOT move barriers — placement is already + identical (§"Ruled out #4") and `barriers__access` already holds the barrier. +- **#221 decision (updated):** ship salmon-group accessible_km now (proven ≤0.27%); BT/ST + unblock **after** the segmentation fix lands on its own branch — then demonstrate + bcfp-equivalence in the vignette. No longer "permanently deferred / documented + artifact"; it's a fixable defect with a known fix. +- **It's a LINK issue, not fresh.** The missing break is in link's segmentation of the + access path (`gradient_barriers_minimal`), not in fresh gradient detection. Filed as + **link#223** (2026-07-03) with the root-cause framing + embedded PNG. + +--- + +## ROOT CAUSE (code-level, 2026-07-03) — `gradient_barriers_minimal` is a minimal reduction used as a segmentation source + +The aggregate "segmentation granularity" difference above is produced by one specific +link code path. link **does** break-and-reclassify like bcfp (it does **not** clip) — but +the break source it feeds the access segmentation is the **downstream-most reduction** of +the barrier set, not the full set, so the interior break points are missing. + +### The path + +1. `lnk_pipeline_run.R:220` — the access phase runs `lnk_pipeline_access()` over the + coarse `.streams` break segmentation, deciding each segment from + `.barriers__access` (`barriers_per_sp`, assembled `:216`). +2. That `streams` segmentation is built from break sources assembled in + `lnk_pipeline_prepare.R` / `_break.R`. One source is `gradient_barriers_minimal`. +3. `lnk_pipeline_prepare.R:574-592` (`.lnk_pipeline_prep_minimal`) builds, per model, + `barriers_` = `gradient_barriers_raw WHERE gradient_class IN (class_filter)` + `UNION ALL` falls, then runs **`fresh::frs_barriers_minimal(conn, from, to)`** on it + (`:592`) and unions the per-model results into `gradient_barriers_minimal` (`:646`). +4. `frs_barriers_minimal.R:120-134` — the reduction `DELETE`s any barrier that has another + barrier **downstream** of it on the same flow path (`whse_basemapping.fwa_upstream()`), + keeping only the **downstream-most** point per path. "Once the downstream-most barrier + is present, any barrier upstream of it is redundant **for access-blocking**" — true for + the access DECISION, false for SEGMENTATION. + +Result: a segment can span a gradient barrier. Its `downstream_route_measure` sits +**below** the barrier, so `frs_network_features(direction="downstream")` in +`lnk_pipeline_access.R:156-165` finds no barrier downstream of the segment start → +`has_barriers__dnstr = FALSE` (`:356`) → `access_ = 1` (`:364`) for the **whole** +segment, including the blocked reach above the barrier. + +### The author already knew this tension — for orphans + +`lnk_pipeline_prepare.R:606-611` (comment, abridged): *"Critically — DO NOT run +frs_barriers_minimal on the orphan set. Minimal reduction keeps only the downstream-most +blocking position per flow path; that's correct semantics for ACCESS barriers, but +orphans are segmentation positions only. We want every detected position to split the +network, not just the downstream-most one."* The same reasoning applies to the per-species +gradient+falls sets that feed `gradient_barriers_minimal` — they are ALSO segmentation +positions — but the minimal reduction was left in for them. **That is the bug.** + +### Decisive evidence on blk 359209845 (FINA, BT) — the PNG segment + +Three distinct barrier sets on this blue line: + +| set | what | n on blk 359209845 | +|---|---|---:| +| `working_fina.barriers_bt` | prep, pre-minimal = gradient ≥2500 ∪ falls | **16** (incl frontier 3834.78) | +| `barriers_bt_min` (→ `gradient_barriers_minimal`) | `frs_barriers_minimal` reduction | **0** | +| `barriers_bt_access` (view) | the ACCESS-decision set | **16** (incl frontier 3834.78) | + +- **Why the minimal set is empty here:** `frs_barriers_minimal` pruned all 16 — even the + frontier at 3834.78 — because **two BT barriers on the parent mainstem blk 359572348** + (wscode `200.948755`) at measures **1684183.02 / 1706109.93** are topologically + downstream of the confluence per `fwa_upstream()`. So there is no break anywhere on + 359209845, and its top segment `[3391, 7998]` (4607 m) stays one accessible unit. +- **Why the fix needs no barrier-set change:** the ACCESS set `barriers_bt_access` + **already contains** the frontier 3834.78. Break the stream at 3835 → new segment + `[3835, 7998]` whose reference measure IS the barrier → `has_barriers_bt_dnstr = TRUE` → + reclassified **blocked**. Nothing added to the barrier set; only the segmentation changes. + +Numbers this segment asserts (matches `/tmp/blk_proof.png`): + +| | segment | length | label | +|---|---|---:|---| +| **LINK** | `[3391, 7998]` | 4607 m | access_bt=1 (accessible) — whole | +| **bcfp** | `[3391, 3835]` | 444 m | accessible | +| **bcfp** | `[3835, 7998]` | **4163 m** | **blocked** | + +Over-credit on this ONE blue line = **4163 m**. Aggregated over FINA that's the +1436 +segment / +1438 km gap in the confusion matrix above. + +### The fix (separate branch) + +Feed the access segmentation the **full** per-species barrier positions (gradient FULL ∪ +falls ∪ definite), not the `frs_barriers_minimal()` reduction — i.e. give the per-species +gradient+falls sets the same "don't-minimize, these are segmentation positions" treatment +`lnk_pipeline_prepare.R:606-611` already gives orphans. Then the existing access trace +reclassifies each above-barrier segment as blocked, exactly like bcfp. **No clipping in +the roll-up; no barrier moved.** Validate: rerun `accessible_km` for BT on FINA/PARS/PCEA +→ expect convergence to bcfp within salmon-group tolerance; then wire BT/ST into +`lnk_parity_annotate()` and demonstrate equivalence in the vignette. + +--- + +## Environment / reproducibility + +Two **separate** postgres instances (cannot SQL-join across them — see gotchas): + +| role | conn | creds | db | +|---|---|---|---| +| link/fresh local | `localhost:5432` | `postgres` / `postgres` | `fwapg` | +| bcfp tunnel | `localhost:63333` | `newgraph` / `postgres` | `bcfishpass` | + +- **Tunnel is intermittent.** Working cred is `newgraph`/`postgres` (NOT the dead + `airvine`/`*_SHARE` path in older notes). Verify live before relying on it. +- **FWA input is byte-identical** between the two DBs (USKE 23458 streams / + 9,436,876 m on both). FINA gradient-barrier class counts match to ≤0.2% (below), + independently reconfirming identical FWA. + +### Table map (what lives where) + +**link — raw gradient barriers** (per working schema; single-WSG, no `wsg` column): +`working_.gradient_barriers_raw` — `(blue_line_key, downstream_route_measure, +gradient_class, label, source, wscode_ltree, localcode_ltree)`. +`gradient_class` encoded **×10000** (0.25 → `2500`). `label='gradient'`, +`source='attribute'`. Also `working_.gradient_barriers_minimal` (island-reduced). +34 working schemas present locally (adms, bbar, …, pcea, …, ufra, unrs, will). + +**link — assembled per-species barriers** (persist, province-wide): +`fresh.barriers__unified` (VIEW: `barrier_source`, `barrier_subtype`, +`passability`, `blocks_species text[]`, `blue_line_key`, `watershed_key`, +`downstream_route_measure`, `watershed_group_code`, `geom`) and +`fresh.barriers__access`. `` ∈ {bt, ch, cm, co, pk, sk, st, …}. + +**link — km inputs** (persist): `fresh.streams` (`length_metre`; PK +`(id_segment, watershed_group_code)` — **#203: join on full PK**), +`fresh.streams_access` (`access_` int: −9 absent / 0 blocked / 1 modelled / 2 +observed), `fresh.streams_vw_bcfp` (the tunnel-free bcfp ref; `barriers__dnstr` +stored as **comma-joined varchar**, accessible predicate `= ''`). + +**bcfp tunnel — raw gradient:** `bcfishpass.gradient_barriers` — `(blue_line_key, +downstream_route_measure, wscode_ltree, localcode_ltree, watershed_group_code, +gradient_class)`. `gradient_class` encoded **×100** (0.25 → `25`), classes +{5,7,10,12,15,20,25,30}. + +**bcfp tunnel — type-mapped / assembled / components:** `barriers_gradient` +(`barrier_type='GRADIENT_NN'`), `barriers_bt` / `barriers_ch_cm_co_pk_sk` / +`barriers_st` / `barriers_ct_dv_rb` / `barriers_wct`, and the component sources +`barriers_falls`, `barriers_subsurfaceflow`, `barriers_user_definite`. +`bcfishpass.streams` stores `barriers__dnstr` as **`array[]::text[]`** (note: +different type than `streams_vw_bcfp`'s varchar). + +Fuller cross-reference: `research/bcfp_table_map.md`. + +### Species → barrier group + gradient cutoff +`inst/extdata/configs/bcfishpass/parameters_fresh.csv` (`access_gradient_max`): +CH/CM/CO/PK/SK **0.15** → `barriers_ch_cm_co_pk_sk_dnstr`; ST **0.20** → +`barriers_st_dnstr`; WCT **0.20** → `barriers_wct_dnstr`; BT **0.25** → +`barriers_bt_dnstr`; CT/DV/RB **0.25** → `barriers_ct_dv_rb_dnstr`. +bcfp side: `model_access_bt.sql` blocks BT on `GRADIENT_25 ∪ GRADIENT_30` (≥0.25); +`model_access_ch_cm_co_pk_sk.sql` on `GRADIENT_15/20/25/30` (≥0.15); +`model_access_ct_dv_rb.sql` on `GRADIENT_25/30` (≥0.25). + +--- + +## Ruled OUT (with evidence — do NOT re-chase) + +**1. Gradient-barrier detection METHOD.** fresh `.frs_break_find_multiclass` +(`fresh/R/frs_break.R`) is a faithful port of bcfp `gradient_barriers_load.sql` +(`smnorris/bcfishpass@f4ae29d`): same 100 m upstream window, same per-FWA-vertex +`ST_LocateAlong` Z-delta on `whse_basemapping.fwa_stream_networks_sp`, same +`blue_line_key = watershed_key` mainstem filter, same island grouping, `min_length=0`. + +**2. FWA input.** Byte-identical between :5432 and :63333 (USKE 23458 / +9,436,876 m). + +**3. Gradient cutoff value.** BT ≥0.25 on both sides (link +`access_gradient_max=0.25`; bcfp `GRADIENT_25 ∪ GRADIENT_30`). + +**4. Raw gradient-barrier PLACEMENT — decisively ruled out 2026-07-02.** +Class-by-class raw counts, FINA (link `gradient_class` ×10000 ↔ bcfp ×100): + +| gradient | link n | bcfp n | Δ | +|---|---|---|---| +| 0.15 | 13110 | 13085 | +25 | +| 0.20 | 13263 | 13241 | +22 | +| 0.25 | 13117 | 13101 | +16 | +| 0.30 | 10390 | 10382 | +8 | + +Positional diff of the **≥0.25** set (blk, DRM→nearest m): link **23,507**, bcfp +**23,483**, **common 23,483**, **bcfp-only 0**, **link-only 24 (0.1%)**. link's raw +gradient barriers are a **strict positional superset** of bcfp's. The 24 extra would +make link *more* restrictive — **opposite** to the observed over-credit. So raw +gradient placement cannot be the cause; the divergence is entirely downstream of it. + +Reproduce (note: `comm` needs **lexically** sorted input — psql `ORDER BY` sorts +numerically, so pipe through `sort`): +```bash +PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d fwapg -At -F',' \ + -c "SELECT blue_line_key, round(downstream_route_measure::numeric) + FROM working_fina.gradient_barriers_raw WHERE gradient_class >= 2500" \ + | sort > /tmp/fina_link_ge25.sorted +PGPASSWORD=postgres psql -h localhost -p 63333 -U newgraph -d bcfishpass -At -F',' \ + -c "SELECT blue_line_key, round(downstream_route_measure::numeric) + FROM bcfishpass.gradient_barriers + WHERE watershed_group_code='FINA' AND gradient_class >= 25" \ + | sort > /tmp/fina_bcfp_ge25.sorted +comm -12 /tmp/fina_link_ge25.sorted /tmp/fina_bcfp_ge25.sorted | wc -l # common +comm -23 /tmp/fina_link_ge25.sorted /tmp/fina_bcfp_ge25.sorted | wc -l # link-only +comm -13 /tmp/fina_link_ge25.sorted /tmp/fina_bcfp_ge25.sorted | wc -l # bcfp-only +``` + +--- + +## Narrowed hypotheses (all downstream of raw gradient detection) + +Direction constraint: **link ≥ bcfp ⇒ link applies FEWER effective downstream +barriers than bcfp** (more accessible = fewer barriers blocking upstream). Since raw +gradient barriers are identical, the missing/differing barriers or measurement is +elsewhere. + +**H1 — Assembled barrier-SET composition.** bcfp BT set +(`model_access_bt.sql`) = `gradient(≥0.25) ∪ barriers_falls ∪ barriers_subsurfaceflow +∪ barriers_user_definite`, **MINUS** barriers with a fish observation upstream (20 m +tol) or confirmed habitat upstream (200 m tol). If link's BT set omits (or +down-weights) falls/subsurfaceflow that bcfp includes, and those sit **below steep +terrain**, bcfp blocks the steep upstream habitat that link credits → matches both +the direction AND the steep-reach concentration. (SETN subsurfaceflow-stale is a +known member of this class — `research/bcfp_divergence_taxonomy.yml`, +`provincial_parity_2026_05_01.md`.) **Test:** diff `fresh.barriers_bt_unified` vs +`bcfishpass.barriers_bt` on FINA, bucketed by `barrier_source`/type. + +**H2 — Per-segment km MEASUREMENT / segmentation.** Identical barrier positions but +**different stream segmentation** → a segment straddling the 0.25 boundary gets +credited accessible **as a whole unit**. Scales with threshold (steeper terrain ⇒ +more/longer boundary segments), which fits the monotonic CO= 2500` ↔ bcfp `gradient_class in + (25,30)`). +- **#203 join discipline:** `id_segment` is NOT globally unique in the consolidated + `fresh` persist; join on full PK `(id_segment, watershed_group_code)`. +- **Tunnel cred:** `newgraph`/`postgres`, intermittent availability. + +--- + +## Session log + +### 2026-07-03 +- **Root-caused** the BT accessible_km over-credit to `gradient_barriers_minimal` being + the `frs_barriers_minimal()` downstream-most reduction used as a segmentation break + source (`lnk_pipeline_prepare.R:592`; the author already excludes orphans from this at + `:606-611`). Proven at segment level on blk 359209845 (FINA, BT): the `[3391, 7998]` + 4607 m segment stays accessible whole because the minimal reduction pruned all 16 + barriers (parent-mainstem blk 359572348 barriers downstream of the confluence), while + `barriers_bt_access` still holds the frontier 3834.78 → break+reclassify fixes it with + no barrier-set change. Over-credit on this blk = 4163 m. +- Built the visual proof (generator `/tmp/blk_proof.R`): schematic linear diagram + + geographic map, LINK vs bcfp, barrier at 3835. Committed to the repo as + `research/blk359209845_bt_accessible_km.png` (`1a26f7d`) — gists don't reliably serve + binary PNGs, so a committed file + pinned `raw.githubusercontent.com` URL is the + hosting mechanism for the issue embed. +- Verdict shifted from "defer BT/ST as documented artifact" → **fixable link defect**; fix + on a separate branch (break at full per-species barrier positions + reclassify, NOT + clip), then demonstrate bcfp-equivalence in the vignette. No pipeline code committed yet. +- **Filed link#223** (2026-07-03) with the root-cause framing + embedded PNG. Pushed the + `221-…` branch (was 8 commits ahead, never pushed) to host the image. +- Next: land the segmentation fix on its own branch. + +### 2026-07-02 +- Ruled out raw gradient-barrier placement decisively (FINA positional superset; + §Ruled-out #4). Established the link/bcfp table map + class encodings. Narrowed to + H1/H2/H3. No code committed; read-only DB work. Next: H1 assembled-set diff on FINA. From 43866a8d4c6572d4fb4493f3207216b127338776 Mon Sep 17 00:00:00 2001 From: almac2022 Date: Fri, 3 Jul 2026 09:52:38 -0700 Subject: [PATCH 10/16] Initialize PWF baseline for #223 Segmentation-frontier fix: gradient_barriers_minimal is fed the frs_barriers_minimal downstream-most reduction as a segmentation source, stripping interior breaks so a segment straddles the accessibility frontier and the whole reach is credited accessible. Phases: reproduce (tests-first), fix prepare.R, validate (no-regression), ship + rename follow-up. Co-Authored-By: Claude Opus 4.7 --- planning/active/findings.md | 62 ++++++++++++++++++++++++++++++++++ planning/active/progress.md | 18 ++++++++++ planning/active/task_plan.md | 65 ++++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 planning/active/findings.md create mode 100644 planning/active/progress.md create mode 100644 planning/active/task_plan.md diff --git a/planning/active/findings.md b/planning/active/findings.md new file mode 100644 index 00000000..7552b119 --- /dev/null +++ b/planning/active/findings.md @@ -0,0 +1,62 @@ +# Findings — accessible_km segmentation-frontier fix (#223) + +## Root cause (from research/accessible_km_divergence.md + code trace 2026-07-03) + +The BT/ST `accessible_km` over-credit is a **segmentation** defect, not an +access-decision defect. At co-located segment positions link and bcfp agree on BT +accessibility at 99.99% (`link_only` = link-acc & bcfp-blocked = 0 km). The gap is +that link's streams don't break at the per-species gradient/falls barriers, so one +segment straddles the frontier and the whole reach is credited accessible. + +## The code path + +1. `R/lnk_pipeline_run.R:216-224` — access set `barriers__access` passed to + `lnk_pipeline_access()`. Correct — holds the frontier barrier. +2. `R/lnk_pipeline_prepare.R:566-594` builds `barriers_` = class-filtered + `gradient_barriers_raw` ∪ falls, then `:592` runs `frs_barriers_minimal()`, and + `:637-651` unions results into `gradient_barriers_minimal` (the break source). +3. `fresh/R/frs_barriers_minimal.R:114-134` — DELETEs every barrier with another + downstream on the same flow path (keeps downstream-most). Correct for a decision, + wrong as a segmentation source. +4. `R/lnk_pipeline_access.R:155-165` / `:356` / `:364` — a segment is blocked only if a + barrier sits downstream of its downstream route measure. A straddling segment has + its measure below the barrier → labelled accessible. + +## Why the fix is safe + isolated + +- `frs_barriers_minimal` used **once** in link (`prepare.R:592`). +- `gradient_barriers_minimal` consumed **only** by `lnk_pipeline_break.R:110`. +- `barriers__access` built independently by `lnk_barriers_unify` + + `lnk_barriers_views.R:171` (anti-join over unified post-override barriers) — does not + touch `gradient_barriers_minimal` / `_min`. + +## Decisive evidence — blk 359209845 (FINA, BT) + +| set | count on blk | frontier 3834.78? | +|---|---|---| +| `working_fina.barriers_bt` (pre-minimal) | 16 | yes | +| `gradient_barriers_minimal` (via `_min`) | 0 | no | +| `barriers_bt_access` (decision) | 16 | yes | + +`frs_barriers_minimal` pruned all 16 tributary barriers because two BT barriers on the +parent mainstem blk 359572348 (measures 1684183.02 / 1706109.93) are downstream of the +confluence per `fwa_upstream()` — but those parents are overridden OUT of +`barriers_bt_access`, so the segmentation frontier and access frontier disagree. + +Result: link `[3391,7998]` = 4607 m accessible (whole); bcfp `[3391,3835]` 444 m +accessible + `[3835,7998]` **4163 m blocked**. Over-credit = 4163 m on this one blk. +Aggregate: FINA +1438 km, PCEA +2021 km, PARS +238 km. + +## bcfp reference identity + +Tunnel-free `fresh.streams_vw_bcfp`: `smnorris/bcfishpass@v0.7.15-41-g2917790` +(head_sha 29177906, date_completed 2026-07-01), `db/model/model_access_bt.sql`. + +## Encodings / gotchas + +- `access_` int: −9 absent / 0 blocked / 1 modelled / 2 observed. accessible = IN(1,2). +- bcfp predicate: `barriers__dnstr = ''` (empty string, char varying — NOT text[]). +- gradient_class: link ×10000 (0.25→2500), bcfp ×100 (0.25→25). +- #203: join persisted `fresh.*` on full PK `(id_segment, watershed_group_code)`. +- Cross-DB: :5432 (link/fresh) and :63333 (bcfp tunnel, intermittent) — no direct join. + accessible_km stays tunnel-free via `fresh.streams_vw_bcfp` on :5432. diff --git a/planning/active/progress.md b/planning/active/progress.md new file mode 100644 index 00000000..d292bc0d --- /dev/null +++ b/planning/active/progress.md @@ -0,0 +1,18 @@ +# Progress — accessible_km segmentation-frontier fix (#223) + +## Session 2026-07-03 + +- Root-caused the BT/ST `accessible_km` over-credit to `gradient_barriers_minimal` + being fed the `frs_barriers_minimal()` downstream-most reduction as a segmentation + source (`lnk_pipeline_prepare.R:592`). Verified `frs_barriers_minimal` is single-use, + `gradient_barriers_minimal` is segmentation-only, and `barriers__access` is built + independently — so the fix is isolated. +- Filed **#223** with root-cause framing + embedded PNG (`research/blk359209845_bt_accessible_km.png`, + committed `1a26f7d` on the 221 branch; raw URL pinned for the issue embed). +- Committed the research doc + 221 findings to the 221 branch (`4549713`), pushed 221. +- Created branch `223-access-segmentation-frontier` off `origin/main`; unset its + main-tracking upstream (safety). +- User decisions: fix branch off main (own PR, closes #223); keep table name now + + file a separate rename issue once confirmed. +- Scaffolded PWF baseline. +- Next: Phase 1 — write `data-raw/accessible_km_fix_validate.R` and confirm it fails pre-fix. diff --git a/planning/active/task_plan.md b/planning/active/task_plan.md new file mode 100644 index 00000000..b750a5cf --- /dev/null +++ b/planning/active/task_plan.md @@ -0,0 +1,65 @@ +# Task: BT/ST accessible_km over-credit — access segmentation drops the frontier (#223) + +The shared segmentation break network `gradient_barriers_minimal` is fed the +**minimal-reduced** per-model barriers (`lnk_pipeline_prepare.R:592-593` runs +`fresh::frs_barriers_minimal()`, which keeps only the downstream-most barrier per +flow path). Correct for an access *decision*, wrong as a *segmentation* source: it +strips interior breaks, so a link segment straddles the accessibility frontier and +the whole reach (incl. the blocked part above the barrier) is credited accessible. +Over-credit: BT `accessible_km` +23.6% (FINA), +40% (PCEA). Root-caused in +`research/accessible_km_divergence.md`; filed as #223. + +The author already applies the correct "break at every position" rule to the +**orphan** sub-threshold classes (`prepare.R:606-611`); the bug is the real +access-threshold classes don't get the same treatment. + +## Verified facts (no assumptions) +- `frs_barriers_minimal` is used **exactly once** in link (`prepare.R:592`) — only + for the per-model segmentation source. +- `gradient_barriers_minimal`'s **only** consumer is `lnk_pipeline_break.R:110` + (the `gradient_minimal` break source) — segmentation-only. +- `barriers__access` (the access **decision**) is built by a separate path: + `lnk_barriers_unify()` → `lnk_barriers_views.R:171` (anti-join over unified + post-override barriers). It does NOT derive from `gradient_barriers_minimal` or + the `_min` tables — so changing the segmentation source cannot disturb the access + decision, which already holds the frontier barrier. +- Design decision (user): keep the table name `gradient_barriers_minimal` + add a + clarifying comment now; file a **separate** rename issue once the fix is confirmed. + +## Phase 1 — Reproduce (tests-first) +- [ ] Add `data-raw/accessible_km_fix_validate.R`: (a) assert `gradient_barriers_minimal` + contains the FINA/BT interior frontier (blk 359209845, ~3835); (b) assert the + segment at measure 3835 has `access_bt = 0`; (c) sum BT `accessible_km` + FINA/PARS/PCEA vs `fresh.streams_vw_bcfp` (`barriers_bt_dnstr = ''`) and assert + convergence; (d) assert salmon (CO) stays ≤ tolerance. +- [ ] Run against current (pre-fix) persisted state → confirm it FAILS (bug reproduces). + +## Phase 2 — Fix +- [ ] `prepare.R:592-593`: union raw `barriers_` (gradient ∪ falls) into + `gradient_barriers_minimal` instead of the `frs_barriers_minimal`-reduced `_min` + table. Remove the now-unused per-model minimal call/table. +- [ ] Update stale docs (keep table name): `prepare.R` roxygen `:19-20`/`:34`/`:36`, + `lnk_pipeline_break.R:24` — say it's now the full per-model break set. + +## Phase 3 — Validate (no-regression) +- [ ] Re-run `lnk_pipeline_run` for FINA / PARS / PCEA (mapping_code = TRUE). +- [ ] Validation script passes: BT `accessible_km` converges to bcfp; segment 3835 blocked. +- [ ] Salmon `accessible_km` stays ≤0.27%. +- [ ] Habitat + mapping_code parity holds or improves vs the 99.66% baseline. +- [ ] `devtools::test()` + `lintr::lint_package()` clean; `devtools::document()` re-run. + +## Phase 4 — Ship + follow-ups +- [ ] `/code-check`; atomic commit(s) (code + checkbox flips). +- [ ] File the separate rename issue (`gradient_barriers_minimal` → `gradient_barriers_break`). +- [ ] `/planning-archive`; `/gh-pr-push` (PR closes #223). NEWS/DESCRIPTION bump as final commit. +- [ ] Rebase 221 on top of merged fix; unblock #221 Phase 3 (wire BT/ST into parity). +- [ ] Return to the accessible_km vignette to demonstrate bcfp equivalence. + +## Validation +- `Rscript data-raw/accessible_km_fix_validate.R` exits non-zero if any WSG BT + `|pct_diff|` exceeds tolerance or segment 3835 is not blocked. +- `Rscript -e 'devtools::test()' 2>&1 | grep -E "(FAIL|ERROR|PASS)"` green. + +## Out of scope +- The rename itself (separate issue, per user). +- Wiring BT/ST into `lnk_parity_annotate()` — that's #221 Phase 3, downstream of this fix. From b32320da5bf54d3a46be779e10fd23516c8e7bb8 Mon Sep 17 00:00:00 2001 From: almac2022 Date: Fri, 3 Jul 2026 10:08:46 -0700 Subject: [PATCH 11/16] #223: record live pre-fix fingerprint + session handoff Confirmed on local fwapg: working_fina.gradient_barriers_minimal empty on blk 359209845; fresh.streams id_segment 4218 = [3391,7998] 4607 m one segment straddling 3835; BT accessible_km FINA link 7520.7 / bcfp 6085.2 = +23.59%. Enriched #223 with the full code trace, the exact fix, and this fingerprint. Handoff note added for the next session to implement Phases 1-4. Co-Authored-By: Claude Opus 4.7 --- planning/active/findings.md | 9 +++++++++ planning/active/progress.md | 24 ++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/planning/active/findings.md b/planning/active/findings.md index 7552b119..f009d2a9 100644 --- a/planning/active/findings.md +++ b/planning/active/findings.md @@ -47,6 +47,15 @@ Result: link `[3391,7998]` = 4607 m accessible (whole); bcfp `[3391,3835]` 444 m accessible + `[3835,7998]` **4163 m blocked**. Over-credit = 4163 m on this one blk. Aggregate: FINA +1438 km, PCEA +2021 km, PARS +238 km. +## Live pre-fix fingerprint (local fwapg :5432, 2026-07-03) + +- `working_fina.gradient_barriers_minimal` on blk 359209845 = **empty** (no break at 3835). +- `fresh.streams` blk 359209845 FINA: `id_segment 4218` = `[3391, 7998]` = **4607 m one + segment** straddling 3835 (segments 4215–4217 break normally below 3391). +- BT `accessible_km` FINA: link **7520.7 km** / bcfp **6085.2 km** = **+23.59%**. +- Post-fix expectation: blk breaks at ~3835; the segment starting there gets + `access_bt = 0`; FINA link BT km drops toward 6085. + ## bcfp reference identity Tunnel-free `fresh.streams_vw_bcfp`: `smnorris/bcfishpass@v0.7.15-41-g2917790` diff --git a/planning/active/progress.md b/planning/active/progress.md index d292bc0d..be4da683 100644 --- a/planning/active/progress.md +++ b/planning/active/progress.md @@ -14,5 +14,25 @@ main-tracking upstream (safety). - User decisions: fix branch off main (own PR, closes #223); keep table name now + file a separate rename issue once confirmed. -- Scaffolded PWF baseline. -- Next: Phase 1 — write `data-raw/accessible_km_fix_validate.R` and confirm it fails pre-fix. +- Scaffolded PWF baseline (`43866a8`). +- **Confirmed the pre-fix fingerprint live** (see findings.md "Live pre-fix fingerprint"): + `working_fina.gradient_barriers_minimal` empty on blk 359209845; `fresh.streams` + id_segment 4218 = `[3391,7998]` 4607 m one segment; BT accessible_km FINA link 7520.7 + / bcfp 6085.2 = +23.59%. FINA/PARS/PCEA all persisted + in `fresh.streams_vw_bcfp`. +- Enriched issue #223 with the full code-trace, the exact fix, and the live fingerprint. + +## Handoff → new session + +Session is being handed off (context-heavy). Pick up here: +1. Read issue #223 (now comprehensive) + this PWF + `research/accessible_km_divergence.md` + (on the 221 branch). +2. Phase 1: write `data-raw/accessible_km_fix_validate.R` (segment 3835 blocked + BT + accessible_km FINA/PARS/PCEA vs bcfp + salmon clean). It should FAIL pre-fix. +3. Phase 2: the one-file fix — `lnk_pipeline_prepare.R:592-593`, union raw `model_tbl` + into `gradient_barriers_minimal` (drop `frs_barriers_minimal` for segmentation); + keep the name, update stale doc comments. +4. Phase 3: re-run FINA/PARS/PCEA (`lnk_pipeline_run(..., mapping_code = TRUE)`), + validate no-regression; `devtools::test()` + `lintr` clean. +5. Phase 4: `/code-check`, atomic commit, file the rename follow-up issue, `/gh-pr-push`. + +Tasks #16–#19 track the remaining phases. From 3ad1829e14ca09a88ef3bf64568cd19cbb4de76d Mon Sep 17 00:00:00 2001 From: almac2022 Date: Fri, 3 Jul 2026 11:20:41 -0700 Subject: [PATCH 12/16] #223 Phase 1: accessible_km fix-validation script (fails pre-fix) Read-only DB validator that reproduces the BT/ST access-segmentation-frontier over-credit. Parity sweep over {FINA,PARS,PCEA,LKEL} x species (link fresh.streams JOIN fresh.streams_access on the full PK) vs the tunnel-free reference fresh.streams_vw_bcfp, plus 4 structural checks on the canonical evidence segment (blk 359209845, BT frontier 3834.78). Exits non-zero while the bug is present; passes once the Phase 2 fix + re-run land. - Validation set adds LKEL (smallest persisted WSG with steelhead + salmon) as the no-regression sentinel: the 3 Peace WSGs carry no salmon/ST, so the plan's item (d) salmon check would be vacuous there. - One-sided-presence gate FAILs a link collapse-to-zero on a bcfp-present species, allowlisting the one known #189 residence case (LKEL:sk). - Confirmed pre-fix: 7 checks fail (FINA/PARS/PCEA BT parity +23.6/+3.4/+40.4%; all 4 FINA structural, incl. the 4163 m above-frontier over-credit). - Code-check (fresh-eyes agents): hardened S3 to isTRUE(all(...)) and closed the one-sided-presence exit-code blind spot. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014kSEJoad4Bc553qU8eFiE6 --- data-raw/accessible_km_fix_validate.R | 219 ++++++++++++++++++++++++++ planning/active/progress.md | 21 +++ planning/active/task_plan.md | 26 ++- 3 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 data-raw/accessible_km_fix_validate.R diff --git a/data-raw/accessible_km_fix_validate.R b/data-raw/accessible_km_fix_validate.R new file mode 100644 index 00000000..388be8eb --- /dev/null +++ b/data-raw/accessible_km_fix_validate.R @@ -0,0 +1,219 @@ +#!/usr/bin/env Rscript +# accessible_km_fix_validate.R — validate the #223 access-segmentation-frontier fix. +# +# THE BUG (segmentation, not access-decision): the stream break source +# `gradient_barriers_minimal` is fed the frs_barriers_minimal() downstream-most +# reduction of each per-species barrier set (lnk_pipeline_prepare.R:592), so +# streams do NOT break at per-species gradient/falls frontiers. One segment +# straddles the frontier and the whole reach — including the blocked part above +# the barrier — is credited accessible. Over-credit scales with the species +# access_gradient_max (BT 0.25 worst). Root cause: research/accessible_km_divergence.md. +# +# THIS SCRIPT IS READ-ONLY. It asserts against the currently-persisted DB state: +# - pre-fix -> FAILS (bug reproduces), exits non-zero. +# - post-fix -> PASSES once the validation-set WSGs are re-run through +# lnk_pipeline_run(..., mapping_code = TRUE) with the fix. +# It never writes; safe to run repeatedly. +# +# Usage: [LNK_LOAD=loadall] Rscript data-raw/accessible_km_fix_validate.R +# LNK_LOAD=loadall -> pkgload::load_all() (dev checkout) +# default -> library(link) +# +# VALIDATION SET: +# FINA / PARS / PCEA — Peace-region WSGs (above the Bennett dam): BT-only, the +# over-credit bug. Pre-fix BT accessible_km diverges +23.6% / +3.4% / +40.4%. +# LKEL — the SMALLEST persisted WSG carrying steelhead AND salmon (BT/ST/CO/CH/ +# CM/PK all present). Pre-fix it is already clean (BT +0.7%, ST/CO 0%). The fix +# changes segmentation for EVERY species' model, so a salmon+ST WSG is required +# to prove no-regression — the Peace WSGs cannot (they carry no salmon/ST). +# +# Expected PRE-FIX failures: FINA-BT, PARS-BT, PCEA-BT parity + all four FINA +# structural checks on the canonical evidence segment (blk 359209845). + +if (identical(Sys.getenv("LNK_LOAD"), "loadall")) { + suppressPackageStartupMessages(pkgload::load_all(quiet = TRUE)) +} else { + suppressPackageStartupMessages(library(link)) +} +suppressPackageStartupMessages({ + library(DBI) + library(RPostgres) +}) + +conn <- lnk_db_conn(dbname = "fwapg", host = "localhost", port = 5432L, + user = "postgres", password = "postgres") +on.exit(try(DBI::dbDisconnect(conn), silent = TRUE), add = TRUE) + +# ---- knobs ---------------------------------------------------------------- +tol_pct <- 1.0 # convergence bar: |link - bcfp| / bcfp * 100 +present_min <- 1.0 # accessible_km above which a (wsg, species) is "present" +frontier_blk <- 359209845L # canonical evidence blue_line_key (FINA, BT) +frontier_m <- 3834.78 # the BT frontier barrier measure on that blk +frontier_eps <- 1.0 # metres — position match tolerance + +wsgs <- c("FINA", "PARS", "PCEA", "LKEL") +species <- c("bt", "st", "co", "ch", "cm", "pk", "sk", "wct") +# bcfp reference predicate: barriers__dnstr = '' (empty = no barrier +# downstream = accessible). Verified identical to bcfp access_ IN (1,2). +# The five salmon species share one combined bcfp barrier column. +bcfp_grp <- c(bt = "barriers_bt_dnstr", + st = "barriers_st_dnstr", + co = "barriers_ch_cm_co_pk_sk_dnstr", + ch = "barriers_ch_cm_co_pk_sk_dnstr", + cm = "barriers_ch_cm_co_pk_sk_dnstr", + pk = "barriers_ch_cm_co_pk_sk_dnstr", + sk = "barriers_ch_cm_co_pk_sk_dnstr", + wct = "barriers_wct_dnstr") +# Known #189 species-residence differences (":"): bcfp models the species +# (shared salmon barrier group populates its access_), link does not — expected, +# NOT a #223 regression. Allowlisted so the one-sided-presence gate does not flag +# them, while an UNEXPECTED link collapse to ~0 km still fails loudly. +residence_exclude <- c("LKEL:sk") + +# ---- failure ledger ------------------------------------------------------- +fails <- character(0) +record_fail <- function(msg) { + fails[[length(fails) + 1L]] <<- msg + cat(" FAIL:", msg, "\n") +} +record_pass <- function(msg) cat(" pass:", msg, "\n") + +# link accessible_km: fresh.streams x fresh.streams_access on the full PK +# (id_segment, watershed_group_code) — the persisted-fresh join discipline (#203). +km_link <- function(wsg, sp) { + DBI::dbGetQuery(conn, sprintf( + "SELECT coalesce(sum(s.length_metre), 0) / 1000.0 AS km + FROM fresh.streams s + JOIN fresh.streams_access a + ON s.id_segment = a.id_segment + AND s.watershed_group_code = a.watershed_group_code + WHERE s.watershed_group_code = '%s' AND a.access_%s IN (1, 2)", + wsg, sp))$km +} +# bcfp accessible_km: the tunnel-free reference view (fresh.streams_vw_bcfp). +km_bcfp <- function(wsg, sp) { + DBI::dbGetQuery(conn, sprintf( + "SELECT coalesce(sum(length_metre), 0) / 1000.0 AS km + FROM fresh.streams_vw_bcfp + WHERE watershed_group_code = '%s' AND %s = ''", + wsg, bcfp_grp[[sp]]))$km +} + +# ---- 1. parity sweep ------------------------------------------------------ +cat("\n== accessible_km parity: link vs bcfp (assert co-present species |diff| <= ", + tol_pct, "%) ==\n", sep = "") +rows <- list() +for (w in wsgs) for (sp in species) { + lk <- km_link(w, sp) + bc <- km_bcfp(w, sp) + lk_present <- lk >= present_min + bc_present <- bc >= present_min + pct <- if (bc > 0) 100 * (lk - bc) / bc else NA_real_ + # Both present -> assert tolerance. A one-sided presence is a divergence the gate + # must catch (link zeroing a bcfp-present species is exactly a no-regression + # failure) — EXCEPT the allowlisted #189 residence cases. Both absent -> skip. + if (lk_present && bc_present) { + scope <- "assert" + status <- if (abs(pct) <= tol_pct) "pass" else "FAIL" + } else if (bc_present) { # bcfp has it, link doesn't + is_known <- paste(w, sp, sep = ":") %in% residence_exclude + scope <- if (is_known) "residence" else "link-absent" + status <- if (is_known) "n/a" else "FAIL" + } else if (lk_present) { # link has it, bcfp doesn't + scope <- "bcfp-absent" + status <- "FAIL" + } else { # absent both sides + scope <- "-" + status <- "n/a" + } + rows[[length(rows) + 1L]] <- data.frame( + wsg = w, sp = toupper(sp), link = round(lk, 1), bcfp = round(bc, 1), + diff_pct = round(pct, 2), scope = scope, status = status) +} +tab <- do.call(rbind, rows) +print(tab[tab$scope != "-", ], row.names = FALSE) +for (i in seq_len(nrow(tab))) { + if (tab$status[i] == "n/a") next + m <- sprintf("%s %s parity %+.2f%% (link %.1f / bcfp %.1f)", + tab$wsg[i], tab$sp[i], tab$diff_pct[i], tab$link[i], tab$bcfp[i]) + if (tab$status[i] == "pass") record_pass(m) else record_fail(m) +} + +# ---- 2. structural checks: canonical evidence segment --------------------- +# FINA blk 359209845, BT frontier 3834.78. Pre-fix a single segment +# [3390.6, 7998.1] straddles the frontier; post-fix it breaks at 3834.78 and the +# reach above is BT-blocked. +cat("\n== structural: FINA blk ", frontier_blk, " BT frontier ", frontier_m, " ==\n", sep = "") + +# S1 mechanism: the break source holds the frontier position (skip if the +# working schema was cleaned after persist — fresh.streams checks stand alone). +gbm_exists <- DBI::dbGetQuery(conn, + "SELECT count(*)::int AS n FROM information_schema.tables + WHERE table_schema = 'working_fina' AND table_name = 'gradient_barriers_minimal'")$n > 0L +if (!gbm_exists) { + cat(" skip: working_fina.gradient_barriers_minimal absent (working schema cleaned)\n") +} else { + fr <- DBI::dbGetQuery(conn, sprintf( + "SELECT count(*)::int AS n FROM working_fina.gradient_barriers_minimal + WHERE blue_line_key = %d AND abs(downstream_route_measure - %f) < %f", + frontier_blk, frontier_m, frontier_eps))$n + if (fr > 0L) { + record_pass("gradient_barriers_minimal holds the frontier break") + } else { + record_fail("gradient_barriers_minimal missing the frontier break (minimal reduction dropped it)") + } +} + +# S2 segmentation: no fresh.streams segment straddles the frontier. +span <- DBI::dbGetQuery(conn, sprintf( + "SELECT count(*)::int AS n FROM fresh.streams + WHERE blue_line_key = %d AND watershed_group_code = 'FINA' + AND downstream_route_measure < %f AND upstream_route_measure > %f", + frontier_blk, frontier_m, frontier_m))$n +if (span == 0L) { + record_pass(sprintf("no segment straddles the frontier (streams break at %.2f)", frontier_m)) +} else { + record_fail(sprintf("%d fresh.streams segment(s) straddle the frontier %.2f", span, frontier_m)) +} + +# S3 reclassification: the segment starting at the frontier is BT-blocked. +seg <- DBI::dbGetQuery(conn, sprintf( + "SELECT a.access_bt FROM fresh.streams s + JOIN fresh.streams_access a + ON s.id_segment = a.id_segment AND s.watershed_group_code = a.watershed_group_code + WHERE s.blue_line_key = %d AND s.watershed_group_code = 'FINA' + AND abs(s.downstream_route_measure - %f) < %f", + frontier_blk, frontier_m, frontier_eps)) +if (nrow(seg) == 0L) { + record_fail(sprintf("no fresh.streams segment starts at frontier %.2f (network did not break there)", frontier_m)) +} else if (isTRUE(all(seg$access_bt == 0L))) { + record_pass(sprintf("segment at %.2f is BT-blocked (access_bt = 0)", frontier_m)) +} else { + record_fail(sprintf("segment at %.2f has access_bt = %s (expected 0)", + frontier_m, paste(seg$access_bt, collapse = ","))) +} + +# S4 over-credit removed: the accessible BT reach on this blk tops out at the +# frontier (pre-fix it runs to 7998.1, ~4163 m of over-credit). +maxupm <- DBI::dbGetQuery(conn, sprintf( + "SELECT coalesce(max(s.upstream_route_measure), 0) AS m FROM fresh.streams s + JOIN fresh.streams_access a + ON s.id_segment = a.id_segment AND s.watershed_group_code = a.watershed_group_code + WHERE s.blue_line_key = %d AND s.watershed_group_code = 'FINA' AND a.access_bt IN (1, 2)", + frontier_blk))$m +if (maxupm <= frontier_m + frontier_eps) { + record_pass(sprintf("accessible BT reach tops out at %.1f (<= frontier)", maxupm)) +} else { + record_fail(sprintf("accessible BT reach extends to %.1f, above frontier %.2f (over-credit %.0f m)", + maxupm, frontier_m, maxupm - frontier_m)) +} + +# ---- verdict -------------------------------------------------------------- +cat("\n== verdict ==\n") +if (length(fails) == 0L) { + cat("ALL CHECKS PASS - #223 access-segmentation-frontier fix validated.\n") + quit(status = 0) +} +cat(sprintf("%d CHECK(S) FAILED - #223 bug present / fix incomplete:\n", length(fails))) +for (f in fails) cat(" -", f, "\n") +quit(status = 1) diff --git a/planning/active/progress.md b/planning/active/progress.md index be4da683..224e5c64 100644 --- a/planning/active/progress.md +++ b/planning/active/progress.md @@ -36,3 +36,24 @@ Session is being handed off (context-heavy). Pick up here: 5. Phase 4: `/code-check`, atomic commit, file the rename follow-up issue, `/gh-pr-push`. Tasks #16–#19 track the remaining phases. + +## Session 2026-07-03 (cont.) — Phase 1 complete + +- Reproduced the full pre-fix fingerprint live on `:5432` fwapg (`working_fina.barriers_bt` + = 16 barriers incl. frontier 3834.78; `barriers_bt_min` = 0 rows on the blk = the + `frs_barriers_minimal` smoking gun; `gradient_barriers_minimal` empty; segment 4218 + `[3390.6, 7998.1]` 4607.5 m straddling, `access_bt = 1`). +- **Discovered FINA/PARS/PCEA carry no salmon/ST** (Peace, above Bennett dam) — the plan's + item (d) "salmon (CO) ≤ tolerance" would be vacuous there. Swept all persisted WSGs and + picked **LKEL** (smallest with ST + salmon: BT 401 / ST 365 / CO 328 km; pre-fix already + clean) as the no-regression sentinel. +- Wrote `data-raw/accessible_km_fix_validate.R` (read-only, exits non-zero on any breach): + parity sweep over {FINA,PARS,PCEA,LKEL} × 8 species with a `both-present` assert gate, + plus 4 structural checks on the canonical blk 359209845. Matches `wsg_run_one.R` conn + recipe + `LNK_LOAD=loadall` guard. +- **Confirmed FAILS pre-fix** (exit 1, 7 checks): FINA/PARS/PCEA BT parity + all 4 FINA + structural; LKEL sentinel passes; bcfp-only SK correctly excluded. +- `lintr`: only house-style hanging-indent nits (siblings in `data-raw/` share them; + `lint_package()` doesn't scan `data-raw/`). No line-length breaches. +- Next: Phase 2 — the one-file fix at `lnk_pipeline_prepare.R:592-593` (union raw `model_tbl` + instead of `frs_barriers_minimal`-reduced `_min`), then re-run + re-validate. diff --git a/planning/active/task_plan.md b/planning/active/task_plan.md index b750a5cf..9e40d138 100644 --- a/planning/active/task_plan.md +++ b/planning/active/task_plan.md @@ -27,12 +27,34 @@ access-threshold classes don't get the same treatment. clarifying comment now; file a **separate** rename issue once the fix is confirmed. ## Phase 1 — Reproduce (tests-first) -- [ ] Add `data-raw/accessible_km_fix_validate.R`: (a) assert `gradient_barriers_minimal` +- [x] Add `data-raw/accessible_km_fix_validate.R`: (a) assert `gradient_barriers_minimal` contains the FINA/BT interior frontier (blk 359209845, ~3835); (b) assert the segment at measure 3835 has `access_bt = 0`; (c) sum BT `accessible_km` FINA/PARS/PCEA vs `fresh.streams_vw_bcfp` (`barriers_bt_dnstr = ''`) and assert convergence; (d) assert salmon (CO) stays ≤ tolerance. -- [ ] Run against current (pre-fix) persisted state → confirm it FAILS (bug reproduces). +- [x] Run against current (pre-fix) persisted state → confirm it FAILS (bug reproduces). + +**Phase 1 result (2026-07-03):** script fails pre-fix, exit 1, 7 checks. Parity: +FINA-BT +23.59%, PARS-BT +3.43%, PCEA-BT +40.36%. Structural (blk 359209845): +gradient_barriers_minimal empty, 1 segment straddles 3834.78, no break at frontier, +accessible reach runs to 7998.1 (over-credit 4163 m — matches issue exactly). +- **Validation-set refinement:** FINA/PARS/PCEA are Peace WSGs above the Bennett dam — + BT-only, **no salmon/ST** (all 0 km), so item (d) is vacuous there. Added **LKEL** + (smallest persisted WSG with ST + salmon; pre-fix already clean BT +0.7% / ST,CO 0%) + as the no-regression sentinel. This fulfills (d), not scope creep. +- **One-sided-presence gate:** both-present → assert tolerance; a species present on only + one side FAILs by default (catches a link collapse-to-zero regression) EXCEPT the + allowlisted #189 residence cases (`residence_exclude = "LKEL:sk"` — link models CO/CH/CM/PK + not SK; bcfp lumps all salmon into one barrier group). Keeps the gate scoped to #223 + segmentation parity without masking a real regression in the exit code. +- **Code-check (2 rounds, fresh-eyes agents):** fixed 2 findings — (1) `isTRUE(all(...))` + on the S3 access_bt check so a hypothetical NULL records a clean FAIL instead of crashing; + (2) the one-sided-presence gate above (was a silent `n/a`, could have exit-0'd a regression). + Lint clean (snake_case constants) bar the shared data-raw SQL hanging-indent style. +- Reference predicate `barriers__dnstr = ''` verified identical to bcfp + `access_ IN (1,2)`. Tolerance `TOL_PCT = 1.0` (tunable in Phase 3 from post-fix numbers). +- Local fwapg is `:5432` (`postgres`/`postgres`/`fwapg`); `:63333` is the bcfp `bcfishpass` + tunnel (no `fresh.*`). Script uses the `:5432` recipe per `wsg_run_one.R`. ## Phase 2 — Fix - [ ] `prepare.R:592-593`: union raw `barriers_` (gradient ∪ falls) into From c86f103802ecfa2e56483aed2afab34753a28b12 Mon Sep 17 00:00:00 2001 From: almac2022 Date: Fri, 3 Jul 2026 13:04:16 -0700 Subject: [PATCH 13/16] #223 Phase 2+3: break streams at every gradient frontier (fix + validate) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix the BT/ST accessible_km over-credit. gradient_barriers_minimal (the stream break source) was fed the frs_barriers_minimal() downstream-most reduction as a SEGMENTATION source, so a single segment straddled the accessibility frontier and its whole reach — including the blocked part above the barrier — was credited accessible. lnk_pipeline_prepare.R: stop minimal-reducing the per-model barrier set; union the RAW per-model (gradient class-filtered + falls) positions into gradient_barriers_minimal so streams break at every frontier — mirroring the orphan path already used for sub-threshold classes, and matching bcfishpass (which segments at every gradient barrier). frs_barriers_minimal is now unused in link; the per-model _min tables are no longer built. Table name kept pending a rename issue; prepare.R + break.R roxygen updated. Validation (FINA/PARS/PCEA/LKEL re-run, mapping_code = TRUE): - BT accessible_km converges to bcfp: FINA +23.59% -> -0.02%, PARS +3.43% -> -0.01%, PCEA +40.36% -> -0.01%, LKEL exact. data-raw/accessible_km_fix_validate.R ALL PASS. - No downstream regression: BT spawning/rearing within ~1% on all 4 WSGs; LKEL ST/CO (salmon + steelhead sentinel) accessible exact, spawning/rearing hold; mapping_code improves. Habitat km is byte-identical pre/post fix — the fix is neutral on habitat. - Cost: segments 2-3.5x (FINA 26k->70k, PCEA 34k->106k); runs still 2-3 min each. Inherent to bcfp-matching; intersects perf issue #205. Also: validator S2 respects integer break rounding (breaks land at round(3834.78) = 3835, as bcfp does); the 4 .lnk_pipeline_prep_minimal unit tests updated to the new contract (raw barriers_ in the union, no frs_barriers_minimal call). devtools::test() green (FAIL 1 = pre-existing environmental public.wsg_outlet, unrelated). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014kSEJoad4Bc553qU8eFiE6 --- R/lnk_pipeline_break.R | 2 +- R/lnk_pipeline_prepare.R | 28 ++++++--- data-raw/accessible_km_fix_validate.R | 9 ++- man/lnk_pipeline_break.Rd | 2 +- man/lnk_pipeline_prepare.Rd | 12 ++-- planning/active/progress.md | 26 ++++++++ planning/active/task_plan.md | 52 +++++++++++++--- tests/testthat/test-lnk_pipeline_prepare.R | 71 ++++++++++++++-------- 8 files changed, 148 insertions(+), 54 deletions(-) diff --git a/R/lnk_pipeline_break.R b/R/lnk_pipeline_break.R index 2440e49f..4273b1eb 100644 --- a/R/lnk_pipeline_break.R +++ b/R/lnk_pipeline_break.R @@ -21,7 +21,7 @@ #' | name | source table | role | classify label | #' |---|---|---|---| #' | `observations` | `.observations_breaks` | fish observations from `bcfishobs.observations`, WSG- and species-filtered, exclusions applied | (informational; not a barrier) | -#' | `gradient_minimal` | `.gradient_barriers_minimal` | minimal-reduced gradient barriers (per-model 15/20/25/30%) | classify uses the FULL set with `gradient_` labels | +#' | `gradient_minimal` | `.gradient_barriers_minimal` | FULL per-model gradient barriers (15/20/25/30%) + falls — every position breaks the network so access gates at each frontier (#223); NOT minimal-reduced despite the legacy name | classify uses the same FULL set with `gradient_` labels | #' | `falls` | `.falls` | natural waterfalls from `whse_basemapping.fwa_obstacles_sp` (loaded by `prep_load_aux`); each fall is its own barrier (NOT minimal-reduced) | `blocked` | #' | `barriers_definite` | `.barriers_definite` | `user_barriers_definite.csv` for the AOI | `blocked` | #' | `subsurfaceflow` | `.barriers_subsurfaceflow` | FWA `edge_type IN (1410, 1425)` start points; honours `user_barriers_definite_control`. Opt-in (only built when listed) | `blocked` | diff --git a/R/lnk_pipeline_prepare.R b/R/lnk_pipeline_prepare.R index 968afc4e..104f3c33 100644 --- a/R/lnk_pipeline_prepare.R +++ b/R/lnk_pipeline_prepare.R @@ -15,9 +15,10 @@ #' `lnk_barrier_overrides()` to compute the per-species skip list. #' User-definite barriers are intentionally excluded here and #' consumed by later phases directly — bcfishpass parity. -#' - Per-model barrier tables reduced to the minimal downstream-most -#' set via [fresh::frs_barriers_minimal()], then unioned into -#' `gradient_barriers_minimal` for segmentation +#' - Per-model barrier tables (gradient class-filtered + falls) unioned +#' into `gradient_barriers_minimal` — the FULL per-model position set +#' (NOT minimal-reduced, despite the legacy name), so streams break at +#' every frontier and per-segment access gating is correct (#223) #' - Base stream segments (`fresh.streams`) loaded from FWA with #' channel width, stream order parent, GENERATED gradient / measures #' / length columns, and a unique `id_segment` @@ -31,9 +32,8 @@ #' - `.barriers_subsurfaceflow` (only when subsurfaceflow opted in #' via `cfg$pipeline$break_order`) #' - `.barrier_overrides` -#' - `.barriers_` + `.barriers__min` -#' per-model pre/post minimal reduction -#' - `.gradient_barriers_minimal` (union of minimal positions) +#' - `.barriers_` — per-model gradient + falls positions +#' - `.gradient_barriers_minimal` (union of per-model raw positions) #' - `fresh.streams` (base segments — not namespaced by AOI; fresh #' owns its output schema) #' - `.dams` (only when `conn_tunnel` is supplied) — pulled @@ -567,9 +567,8 @@ lnk_pipeline_prepare <- function(conn, aoi, cfg, loaded, schema, for (model_name in names(models)) { class_filter <- paste(models[[model_name]], collapse = ", ") model_tbl <- paste0(schema, ".barriers_", model_name) - min_tbl <- paste0(model_tbl, "_min") - # Build pre-minimal set: gradient (class-filtered) + falls + # Build the per-model break set: gradient (class-filtered) + falls. .lnk_db_execute(conn, sprintf("DROP TABLE IF EXISTS %s", model_tbl)) .lnk_db_execute(conn, sprintf( "CREATE TABLE %s AS @@ -589,8 +588,17 @@ lnk_pipeline_prepare <- function(conn, aoi, cfg, loaded, schema, model_tbl, schema, class_filter, schema, .lnk_quote_literal(aoi))) - fresh::frs_barriers_minimal(conn, from = model_tbl, to = min_tbl) - minimal_tbls <- c(minimal_tbls, min_tbl) + # Feed the RAW per-model positions into the break network — every gradient / + # falls barrier must split the streams so the per-segment downstream-barrier + # access check (lnk_pipeline_access) can gate the reach ABOVE each frontier. + # Do NOT frs_barriers_minimal() here: minimal reduction keeps only the + # downstream-most position per flow path (correct for an access DECISION, + # wrong as a SEGMENTATION source — it strips the interior frontier so a single + # segment straddles the barrier and its whole reach is mislabelled accessible; + # #223). This mirrors the orphan-set treatment below and matches bcfishpass, + # which segments at every gradient barrier. Table name kept as + # gradient_barriers_minimal for now; a follow-up issue renames it. + minimal_tbls <- c(minimal_tbls, model_tbl) } # Orphan classes: thresholds in `classes` that fall below every species's diff --git a/data-raw/accessible_km_fix_validate.R b/data-raw/accessible_km_fix_validate.R index 388be8eb..b3c30b75 100644 --- a/data-raw/accessible_km_fix_validate.R +++ b/data-raw/accessible_km_fix_validate.R @@ -164,12 +164,17 @@ if (!gbm_exists) { } } -# S2 segmentation: no fresh.streams segment straddles the frontier. +# S2 segmentation: no fresh.streams segment straddles the frontier. Breaks round +# to integer (measure_precision = 0L in lnk_pipeline_break), so the break lands at +# round(3834.78) = 3835 — exactly as bcfp does. A segment must span BEYOND the +# rounding window to count as a straddle; the clean below-frontier segment that +# ends at 3835 (0.22 m above the un-rounded frontier) is a proper break, not a +# straddle. The genuine pre-fix straddle [3390.6, 7998.1] still fails this. span <- DBI::dbGetQuery(conn, sprintf( "SELECT count(*)::int AS n FROM fresh.streams WHERE blue_line_key = %d AND watershed_group_code = 'FINA' AND downstream_route_measure < %f AND upstream_route_measure > %f", - frontier_blk, frontier_m, frontier_m))$n + frontier_blk, frontier_m - frontier_eps, frontier_m + frontier_eps))$n if (span == 0L) { record_pass(sprintf("no segment straddles the frontier (streams break at %.2f)", frontier_m)) } else { diff --git a/man/lnk_pipeline_break.Rd b/man/lnk_pipeline_break.Rd index b999cf12..6c4d4651 100644 --- a/man/lnk_pipeline_break.Rd +++ b/man/lnk_pipeline_break.Rd @@ -52,7 +52,7 @@ Valid entries for \code{cfg$pipeline$break_order}. Omit an entry to skip both segmentation and access-gating from that source.\tabular{llll}{ name \tab source table \tab role \tab classify label \cr \code{observations} \tab \verb{.observations_breaks} \tab fish observations from \code{bcfishobs.observations}, WSG- and species-filtered, exclusions applied \tab (informational; not a barrier) \cr - \code{gradient_minimal} \tab \verb{.gradient_barriers_minimal} \tab minimal-reduced gradient barriers (per-model 15/20/25/30\%) \tab classify uses the FULL set with \verb{gradient_} labels \cr + \code{gradient_minimal} \tab \verb{.gradient_barriers_minimal} \tab FULL per-model gradient barriers (15/20/25/30\%) + falls — every position breaks the network so access gates at each frontier (#223); NOT minimal-reduced despite the legacy name \tab classify uses the same FULL set with \verb{gradient_} labels \cr \code{falls} \tab \verb{.falls} \tab natural waterfalls from \code{whse_basemapping.fwa_obstacles_sp} (loaded by \code{prep_load_aux}); each fall is its own barrier (NOT minimal-reduced) \tab \code{blocked} \cr \code{barriers_definite} \tab \verb{.barriers_definite} \tab \code{user_barriers_definite.csv} for the AOI \tab \code{blocked} \cr \code{subsurfaceflow} \tab \verb{.barriers_subsurfaceflow} \tab FWA \verb{edge_type IN (1410, 1425)} start points; honours \code{user_barriers_definite_control}. Opt-in (only built when listed) \tab \code{blocked} \cr diff --git a/man/lnk_pipeline_prepare.Rd b/man/lnk_pipeline_prepare.Rd index 36af01ee..5329ee35 100644 --- a/man/lnk_pipeline_prepare.Rd +++ b/man/lnk_pipeline_prepare.Rd @@ -75,9 +75,10 @@ enriched with \code{wscode_ltree} and \code{localcode_ltree} for \code{lnk_barrier_overrides()} to compute the per-species skip list. User-definite barriers are intentionally excluded here and consumed by later phases directly — bcfishpass parity. -\item Per-model barrier tables reduced to the minimal downstream-most -set via \code{\link[fresh:frs_barriers_minimal]{fresh::frs_barriers_minimal()}}, then unioned into -\code{gradient_barriers_minimal} for segmentation +\item Per-model barrier tables (gradient class-filtered + falls) unioned +into \code{gradient_barriers_minimal} — the FULL per-model position set +(NOT minimal-reduced, despite the legacy name), so streams break at +every frontier and per-segment access gating is correct (#223) \item Base stream segments (\code{fresh.streams}) loaded from FWA with channel width, stream order parent, GENERATED gradient / measures / length columns, and a unique \code{id_segment} @@ -93,9 +94,8 @@ Writes to (under the caller's working schema unless noted): \item \verb{.barriers_subsurfaceflow} (only when subsurfaceflow opted in via \code{cfg$pipeline$break_order}) \item \verb{.barrier_overrides} -\item \verb{.barriers_} + \verb{.barriers__min} -per-model pre/post minimal reduction -\item \verb{.gradient_barriers_minimal} (union of minimal positions) +\item \verb{.barriers_} — per-model gradient + falls positions +\item \verb{.gradient_barriers_minimal} (union of per-model raw positions) \item \code{fresh.streams} (base segments — not namespaced by AOI; fresh owns its output schema) \item \verb{.dams} (only when \code{conn_tunnel} is supplied) — pulled diff --git a/planning/active/progress.md b/planning/active/progress.md index 224e5c64..4413f44e 100644 --- a/planning/active/progress.md +++ b/planning/active/progress.md @@ -57,3 +57,29 @@ Tasks #16–#19 track the remaining phases. `lint_package()` doesn't scan `data-raw/`). No line-length breaches. - Next: Phase 2 — the one-file fix at `lnk_pipeline_prepare.R:592-593` (union raw `model_tbl` instead of `frs_barriers_minimal`-reduced `_min`), then re-run + re-validate. + +## Session 2026-07-03 (cont.) — Phase 2 fix + downstream-fallout assessment + +- **Fix landed:** `lnk_pipeline_prepare.R` — dropped the per-model `frs_barriers_minimal` + reduction; `gradient_barriers_minimal` now unions the RAW per-model (gradient ∪ falls) + positions (mirrors the orphan path, matches bcfp). Updated stale roxygen in prepare.R + + break.R. `frs_barriers_minimal` is now unused in link; grep confirmed no other consumer + of the `_min` tables. +- **Downstream-fallout trace (user asked "will the new barriers cause issues downstream?"):** + - Traced fresh's break machinery (subagent): `frs_break_apply` dedups (`DISTINCT`+`round`) + and drops breaks within 1 m of an existing boundary → NO zero-length segments; generated + measure columns make re-breaking idempotent; habitat length conserved under splitting. + - `classify.R:205` already builds `streams_breaks` from `gradient_barriers_raw` (FULL set) — + the fix just aligns the base segmentation to what classify already gates against. + - **Volume:** break positions explode (FINA 11→49,880; PCEA 32→82,931; PARS 10,977→64,483; + LKEL 640→5,145) → segments ~2–3.5×. Real perf/storage cost, inherent to bcfp-matching (#205). +- **LKEL canary (fix) + pre-fix baseline (stash→run→measure→pop):** + - accessible_km LKEL BT +0.72% → −0.00% exact; ST/CO/CH/CM/PK exact. Segments 3,376 → 7,446. + - habitat spawning/rearing km BYTE-IDENTICAL pre/post fix → the fix is neutral on habitat. + - mapping_code improves (711.3 → 714.1 vs bcfp 714.4). + - **Habitat parity vs bcfp is CLEAN** (BT spawning +0.0%, ST −2.6%, CO −8.7%; rearing ~+1%). + - MEASUREMENT-BUG CORRECTION (user caught it): I first reported ST/CO spawning +42%/+24% — WRONG. + `streams_vw_bcfp.spawning_` is coded 0/1/2/3; I'd filtered `= 1` and dropped the 2/3 km, + undercounting the bcfp reference. Correct predicate `spawning_ > 0`. No divergence; nothing to file. +- Next: Phase 3 — run FINA/PARS/PCEA (+ restore LKEL) with the fix, then the validator (expect + all pass); confirm BT habitat holds on the diverged WSGs. diff --git a/planning/active/task_plan.md b/planning/active/task_plan.md index 9e40d138..82002507 100644 --- a/planning/active/task_plan.md +++ b/planning/active/task_plan.md @@ -57,18 +57,52 @@ accessible reach runs to 7998.1 (over-credit 4163 m — matches issue exactly). tunnel (no `fresh.*`). Script uses the `:5432` recipe per `wsg_run_one.R`. ## Phase 2 — Fix -- [ ] `prepare.R:592-593`: union raw `barriers_` (gradient ∪ falls) into +- [x] `prepare.R:566-594`: union raw `barriers_` (gradient ∪ falls) into `gradient_barriers_minimal` instead of the `frs_barriers_minimal`-reduced `_min` - table. Remove the now-unused per-model minimal call/table. -- [ ] Update stale docs (keep table name): `prepare.R` roxygen `:19-20`/`:34`/`:36`, - `lnk_pipeline_break.R:24` — say it's now the full per-model break set. + table. Removed the per-model minimal call + `min_tbl`. Verified `frs_barriers_minimal` + now unused in link and no other consumer of the `_min` tables (grep). +- [x] Update stale docs (keep table name): `prepare.R` roxygen + `lnk_pipeline_break.R:24` + — now describe the FULL per-model break set. `devtools::document()` re-run. + +**LKEL canary (2026-07-03) — fix validated end-to-end, downstream fallout assessed:** +- **accessible_km converges:** LKEL BT +0.72% → **−0.00%** (exact); ST/CO/CH/CM/PK stay exact. + Segments 3,376 → 7,446 (~2.2×). Ran in 1.3 min. +- **Downstream outputs safe.** Proven by a pre-fix baseline re-run (stash → run → measure → pop): + habitat spawning/rearing km are BYTE-IDENTICAL pre-fix and post-fix (ST spawning 108.0, CO 139.2 + both runs) — the fix is neutral on habitat. mapping_code *improves* (mapping_code_bt 711.3 → 714.1 + vs bcfp 714.4). +- **Habitat parity vs bcfp is clean** (corrected measurement — see below): BT spawning +0.0%, + ST −2.6%, CO −8.7%; rearing all ~+1%. + - MEASUREMENT-BUG CORRECTION: an earlier note here claimed ST/CO spawning +42%/+24%. That was a + bad reference predicate — `fresh.streams_vw_bcfp.spawning_` is coded **0/1/2/3**, and filtering + `spawning_st = 1` dropped the 2/3 km, undercounting bcfp. Correct predicate is `spawning_ > 0`. + No such divergence exists; the fix does not touch habitat. +- **Downstream fallout verdict:** no correctness regressions from the denser break set — fresh's + break machinery dedups + drops sub-1m coincident breaks (no zero-length segments), habitat + length is conserved, and classify already gated on the FULL gradient set. The only real cost is + the 2–3.5× segment/storage/runtime increase (inherent to bcfp-matching; intersects perf issue #205). ## Phase 3 — Validate (no-regression) -- [ ] Re-run `lnk_pipeline_run` for FINA / PARS / PCEA (mapping_code = TRUE). -- [ ] Validation script passes: BT `accessible_km` converges to bcfp; segment 3835 blocked. -- [ ] Salmon `accessible_km` stays ≤0.27%. -- [ ] Habitat + mapping_code parity holds or improves vs the 99.66% baseline. -- [ ] `devtools::test()` + `lintr::lint_package()` clean; `devtools::document()` re-run. +- [x] Re-ran `lnk_pipeline_run(mapping_code = TRUE)` for FINA / PARS / PCEA / LKEL. Fast: + 1.9 / 2.6 / 3.1 / 1.3 min (segments 26k→70k, 48k→97k, 34k→106k, 3.4k→7.4k). No perf blowup. +- [x] **Validation script ALL CHECKS PASS.** BT accessible_km: FINA +23.59%→**−0.02%**, + PARS +3.43%→**−0.01%**, PCEA +40.36%→**−0.01%**, LKEL exact. Structural: FINA blk 359209845 + breaks at 3835, segment above is BT-blocked, accessible reach tops at the frontier. + (Fixed validator S2: breaks round to integer via `measure_precision=0L` so the break lands + at round(3834.78)=3835 like bcfp — S2 now uses the eps window; still catches the pre-fix straddle.) +- [x] Salmon/ST no-regression (LKEL): accessible_km exact for BT/ST/CO/CH/CM/PK. +- [x] **Habitat + mapping_code parity holds** (corrected `>0` predicate): BT spawning/rearing within + ~1% on all 4 WSGs; LKEL ST spawning −2.6%, CO spawning −8.7% (modest, link-under, PRE-EXISTING + + fix-neutral); mapping_code improves. No regression from the denser segmentation. +- [x] `devtools::test()` green (`FAIL 1 | PASS 1254`; the 1 FAIL is the pre-existing + environmental `public.wsg_outlet` missing-table in test-lnk_wsg_resolve, unrelated). + Updated the 4 `.lnk_pipeline_prep_minimal` unit tests to the new contract (no + `frs_barriers_minimal`; raw `barriers_` in the union). `devtools::document()` re-run. + No new non-indentation lints on touched files. + +**Note — LKEL CO spawning −8.7%** is the one non-trivial habitat gap; it is pre-existing (identical +pre/post fix), link-conservative, and unrelated to #223 (likely the connected-waterbody spawning +nuance documented in `research/bcfishpass_methodology.md`). Out of scope; flag if it recurs broadly. ## Phase 4 — Ship + follow-ups - [ ] `/code-check`; atomic commit(s) (code + checkbox flips). diff --git a/tests/testthat/test-lnk_pipeline_prepare.R b/tests/testthat/test-lnk_pipeline_prepare.R index 2b1b1311..1d263b0b 100644 --- a/tests/testthat/test-lnk_pipeline_prepare.R +++ b/tests/testthat/test-lnk_pipeline_prepare.R @@ -305,13 +305,18 @@ test_that(".lnk_pipeline_prep_minimal builds per-species tables from access_grad .lnk_pipeline_prep_minimal("mock-conn", aoi = "BULK", cfg = cfg, loaded = loaded, schema = "w_bulk") - # One barrier table per species (BT/CH/SK/ST/WCT) - expect_length(minimal_calls, 5L) - from_tables <- vapply(minimal_calls, `[[`, character(1), "from") - table_names <- sub("^[^.]+\\.", "", from_tables) - expect_setequal(table_names, - c("barriers_bt", "barriers_ch", "barriers_sk", - "barriers_st", "barriers_wct")) + # Fix #223: raw per-species positions feed segmentation directly — + # frs_barriers_minimal is no longer called; each raw barriers_ table + # enters the gradient_barriers_minimal union. + expect_length(minimal_calls, 0L) + union_sql <- captured[grepl( + "CREATE TABLE w_bulk\\.gradient_barriers_minimal", captured)] + expect_length(union_sql, 1L) + for (tbl in c("barriers_bt", "barriers_ch", "barriers_sk", + "barriers_st", "barriers_wct")) { + expect_match(union_sql, sprintf("FROM w_bulk\\.%s\\b", tbl)) + } + expect_no_match(union_sql, "_min\\b") # BT @ 0.25 → classes 2500, 3000 (only those ≥ 0.25) bt_create <- captured[grepl("CREATE TABLE w_bulk\\.barriers_bt\\b", captured)] @@ -335,8 +340,12 @@ test_that(".lnk_pipeline_prep_minimal builds per-species tables from access_grad }) test_that(".lnk_pipeline_prep_minimal skips species with NA / zero access_gradient_max", { + captured <- character(0) local_mocked_bindings( - .lnk_db_execute = function(conn, sql) invisible(NULL) + .lnk_db_execute = function(conn, sql) { + captured <<- c(captured, sql) + invisible(NULL) + } ) minimal_calls <- list() local_mocked_bindings( @@ -361,11 +370,16 @@ test_that(".lnk_pipeline_prep_minimal skips species with NA / zero access_gradie .lnk_pipeline_prep_minimal("mock-conn", aoi = "BULK", cfg = cfg, loaded = loaded, schema = "w") - # BT yields a barrier table; LK + ZR skipped. Orphan branch doesn't - # call frs_barriers_minimal (raw positions used directly), so only - # BT's call appears here. - expect_length(minimal_calls, 1L) - expect_match(minimal_calls[[1]]$from, "barriers_bt$") + # BT yields a barrier table; LK + ZR skipped. Fix #223: raw positions feed + # segmentation, so frs_barriers_minimal is not called — BT's raw table enters + # the union, the skipped species do not. + expect_length(minimal_calls, 0L) + union_sql <- captured[grepl( + "CREATE TABLE w\\.gradient_barriers_minimal", captured)] + expect_length(union_sql, 1L) + expect_match(union_sql, "FROM w\\.barriers_bt\\b") + expect_no_match(union_sql, "barriers_lk") + expect_no_match(union_sql, "barriers_zr") }) test_that(".lnk_pipeline_prep_minimal adds orphan-class break source when low classes present", { @@ -401,13 +415,9 @@ test_that(".lnk_pipeline_prep_minimal adds orphan-class break source when low cl .lnk_pipeline_prep_minimal("mock-conn", aoi = "BULK", cfg = cfg, loaded = loaded, schema = "w", classes = custom) - # Expected: 2 per-species tables get frs_barriers_minimal calls (BT, CH). - # Orphan branch creates barriers_orphan but does NOT call - # frs_barriers_minimal on it (raw positions used directly for segmentation). - expect_length(minimal_calls, 2L) - from_tables <- vapply(minimal_calls, `[[`, character(1), "from") - table_names <- sub("^[^.]+\\.", "", from_tables) - expect_setequal(table_names, c("barriers_bt", "barriers_ch")) + # Fix #223: no per-species frs_barriers_minimal call — the raw barriers_bt/ch + # enter the union directly, exactly as the orphan set already does. + expect_length(minimal_calls, 0L) # Orphan CREATE TABLE statement still emitted, filtering to class 500 orphan_create <- captured[grepl("CREATE TABLE w\\.barriers_orphan\\b", captured)] @@ -419,11 +429,17 @@ test_that(".lnk_pipeline_prep_minimal adds orphan-class break source when low cl union_sql <- captured[grepl("CREATE TABLE w\\.gradient_barriers_minimal", captured)] expect_length(union_sql, 1L) expect_match(union_sql, "FROM w\\.barriers_orphan\\b") + expect_match(union_sql, "FROM w\\.barriers_bt\\b") + expect_match(union_sql, "FROM w\\.barriers_ch\\b") }) test_that(".lnk_pipeline_prep_minimal does NOT add orphan branch when no low classes", { + captured <- character(0) local_mocked_bindings( - .lnk_db_execute = function(conn, sql) invisible(NULL) + .lnk_db_execute = function(conn, sql) { + captured <<- c(captured, sql) + invisible(NULL) + } ) minimal_calls <- list() local_mocked_bindings( @@ -448,10 +464,15 @@ test_that(".lnk_pipeline_prep_minimal does NOT add orphan branch when no low cla .lnk_pipeline_prep_minimal("mock-conn", aoi = "BULK", cfg = cfg, loaded = loaded, schema = "w", classes = .lnk_classes_bcfp) - from_tables <- vapply(minimal_calls, `[[`, character(1), "from") - table_names <- sub("^[^.]+\\.", "", from_tables) - expect_false("barriers_orphan" %in% table_names) - expect_setequal(table_names, c("barriers_bt", "barriers_ch")) + # Fix #223: frs_barriers_minimal not called; the union carries the raw + # per-species tables and NO orphan table (no low classes here). + expect_length(minimal_calls, 0L) + union_sql <- captured[grepl( + "CREATE TABLE w\\.gradient_barriers_minimal", captured)] + expect_length(union_sql, 1L) + expect_match(union_sql, "FROM w\\.barriers_bt\\b") + expect_match(union_sql, "FROM w\\.barriers_ch\\b") + expect_no_match(union_sql, "barriers_orphan") }) test_that(".lnk_pipeline_prep_minimal honours custom classes vector", { From 20f815a6231d7aa8b65296d179d9d5bbb260863c Mon Sep 17 00:00:00 2001 From: almac2022 Date: Fri, 3 Jul 2026 20:41:22 -0700 Subject: [PATCH 14/16] #221+#223 Phase 5: provincial accessible+spawn+rear parity proof (11 WSGs, 8 species) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add data-raw/parity_crosssection.R — a tunnel-free harness that extends the habitat parity rollup with the accessible_km column (#221): link via lnk_rollup_wsg vs bcfp fresh.streams_vw_bcfp, IN(1,2) predicate (bcfp codes access/spawning/rearing 0/1/2/3; a bare = 1 under-counts). Ran across 11 WSGs re-run with the #223 fix (FINA/PARS/PCEA/LKEL + BULK/MORR/KISP/ LFRA/USKE + ELKR/KOTR — Peace/Fraser/Skeena/Columbia, all 8 species): - accessible_km: 44/44 within 1%, max |diff| 0.05% — the fix converges exactly across every species and WSG (FINA/PCEA were +23.6% / +40.4% pre-fix). - spawning 42/43, rearing 34/35 within 5% — the ONLY two exceptions are BULK SK (spawn +11.0%, rear +35.2%) = the documented parked fresh#190 dual-rearing-lake topology (research/bcfishpass_methodology.md:124). Pre-existing, not a #223 regression. - #189 residence species (LKEL SK, BULK/MORR/USKE CM, USKE PK) correctly excluded. research/parity_accessible_habitat_2026_07_03.md records the proof. Combined branch test() green (FAIL 1 = pre-existing environmental wsg_outlet). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014kSEJoad4Bc553qU8eFiE6 --- data-raw/parity_crosssection.R | 107 ++++++++++++++++++ planning/active/task_plan.md | 17 +-- .../parity_accessible_habitat_2026_07_03.md | 68 +++++++++++ 3 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 data-raw/parity_crosssection.R create mode 100644 research/parity_accessible_habitat_2026_07_03.md diff --git a/data-raw/parity_crosssection.R b/data-raw/parity_crosssection.R new file mode 100644 index 00000000..cc5a6c7f --- /dev/null +++ b/data-raw/parity_crosssection.R @@ -0,0 +1,107 @@ +#!/usr/bin/env Rscript +# parity_crosssection.R — tunnel-free accessible + spawning + rearing parity +# across a WSG cross-section. The provincial-parity harness EXTENDED with the +# accessible column (#221) to prove the #223 access-segmentation fix converges +# accessible_km without regressing habitat. +# +# link side = lnk_rollup_wsg() (#221): streams x streams_access x streams_habitat_ +# on the full PK, accessible = access_ IN (1,2). +# bcfp side = fresh.streams_vw_bcfp (tunnel-free snapshot), IN (1,2) predicate on +# access_ / spawning_ / rearing_ (bcfp codes these 0/1/2/3; +# IN (1,2) is the presence predicate — a bare `= 1` UNDER-counts). +# Note: bcfp has no rearing_cm / rearing_pk (chum/pink do not rear in freshwater). +# +# Prereq: each WSG re-run through lnk_pipeline_run(mapping_code = TRUE) with the fix. +# Usage: [LNK_LOAD=loadall] Rscript data-raw/parity_crosssection.R [WSG ...] + +if (identical(Sys.getenv("LNK_LOAD"), "loadall")) { + suppressPackageStartupMessages(pkgload::load_all(quiet = TRUE)) +} else { + suppressPackageStartupMessages(library(link)) +} +suppressPackageStartupMessages({ + library(DBI) + library(glue) +}) + +conn <- lnk_db_conn(dbname = "fwapg", host = "localhost", port = 5432L, + user = "postgres", password = "postgres") +on.exit(try(DBI::dbDisconnect(conn), silent = TRUE), add = TRUE) + +args <- commandArgs(trailingOnly = TRUE) +wsgs <- if (length(args)) toupper(args) else c("FINA", "PARS", "PCEA", "LKEL") +species <- c("bt", "st", "co", "ch", "cm", "pk", "sk", "wct") +rear_sp <- c("bt", "ch", "co", "sk", "st", "wct") +present_min <- 1.0 +tol_acc <- 1.0 # accessible: the fix should make this tight +tol_hab <- 5.0 # spawn / rear: legitimate methodology wiggle +col_map <- c(accessible = "accessible_km", spawning = "spawning_km", rearing = "rearing_km") +bc_prefix <- c(accessible = "acc_", spawning = "spawn_", rearing = "rear_") + +bcfp_rollup <- function(w) { + cols <- unlist(lapply(species, function(sp) { + r <- if (sp %in% rear_sp) { + sprintf(paste0("round((coalesce(sum(length_metre) FILTER (WHERE rearing_%s IN (1,2)),0)", + "/1000)::numeric,2) rear_%s"), sp, sp) + } else { + "NULL::numeric rear_x" + } + sprintf("round((coalesce(sum(length_metre) FILTER (WHERE access_%s IN (1,2)),0)/1000)::numeric,2) acc_%s, + round((coalesce(sum(length_metre) FILTER (WHERE spawning_%s IN (1,2)),0)/1000)::numeric,2) spawn_%s, %s", + sp, sp, sp, sp, r) + })) + DBI::dbGetQuery(conn, glue( + "select {paste(cols, collapse=',')} from fresh.streams_vw_bcfp where watershed_group_code = '{w}'")) +} +num <- function(x) { + x <- suppressWarnings(as.numeric(x[1])) + if (length(x) == 0L || is.na(x)) 0 else x +} + +rows <- list() +absent <- character(0) +for (w in wsgs) { + lk <- lnk_rollup_wsg(conn, aoi = w, species = species, schema = "fresh") + bc <- bcfp_rollup(w) + for (sp in species) { + sp_up <- toupper(sp) + lk_row <- lk[lk$species == sp_up, ] + for (m in names(col_map)) { + if (m == "rearing" && !(sp %in% rear_sp)) next + bcv <- num(bc[[paste0(bc_prefix[m], sp)]]) + lkv <- if (nrow(lk_row)) num(lk_row[[col_map[m]]]) else 0 + if (max(lkv, bcv) < present_min) next + if (lkv < present_min && bcv >= present_min) { + absent <- c(absent, sprintf("%s %s %s (bcfp %.0f, link 0)", w, sp_up, m, bcv)) + next + } + pct <- 100 * (lkv - bcv) / bcv + tol <- if (m == "accessible") tol_acc else tol_hab + rows[[length(rows) + 1L]] <- data.frame( + wsg = w, sp = sp_up, metric = m, link = round(lkv, 1), bcfp = round(bcv, 1), + diff_pct = round(pct, 2), ok = abs(pct) <= tol) + } + } +} +tab <- do.call(rbind, rows) +cat("\n============ accessible + spawn + rear parity (link vs bcfp, tunnel-free) ============\n") +print(tab, row.names = FALSE) + +cat("\n==== summary by metric (accessible tol", tol_acc, "%, habitat tol", tol_hab, "%) ====\n") +for (m in c("accessible", "spawning", "rearing")) { + sub <- tab[tab$metric == m, ] + fails <- sub[!sub$ok, ] + over <- if (nrow(fails)) { + paste0(" OVER: ", paste(sprintf("%s/%s %+.1f%%", fails$wsg, fails$sp, fails$diff_pct), collapse = "; ")) + } else { + "" + } + cat(sprintf(" %-11s: %2d pairs / %d WSGs / %d species, max |diff| %5.2f%%, within-tol %d/%d%s\n", + m, nrow(sub), length(unique(sub$wsg)), length(unique(sub$sp)), + max(abs(sub$diff_pct)), sum(sub$ok), nrow(sub), over)) +} +if (length(absent)) { + cat("\n link-absent (bcfp models species, link does not — #189 residence, excluded):\n") + for (a in absent) cat(" ", a, "\n") +} +cat(sprintf("\nOVERALL: %d/%d pairs within tolerance (%d over)\n", sum(tab$ok), nrow(tab), sum(!tab$ok))) diff --git a/planning/active/task_plan.md b/planning/active/task_plan.md index 39174be2..8fd165e6 100644 --- a/planning/active/task_plan.md +++ b/planning/active/task_plan.md @@ -113,13 +113,16 @@ nuance documented in `research/bcfishpass_methodology.md`). Out of scope; flag i Decision (user): merge #221 (`lnk_compare_rollup` accessible_km column + `lnk_rollup_wsg`) into #223 → **one PR closing both**. #221 surfaced #223; #223 makes #221's accessible parity pass — inseparable for the proof. Merged 221→223 (PWF conflicts only; R/tests/research clean). -- [x] Merge `origin/221-per-wsg-habitat-access-km-rollup` into the #223 branch. -- [ ] Verify combined package loads + `devtools::test()` green (221 rollup tests + 223 pipeline tests). -- [ ] Re-run 11-WSG cross-section with the fix (done: FINA/PARS/PCEA/LKEL; new: BULK/MORR/KISP/ - LFRA/USKE all-7-species + ELKR/KOTR WCT). ~40 min local, no cyphers (2–3 min/WSG measured). -- [ ] Run `lnk_compare_rollup` across them → per-species per-WSG table: **accessible + spawn + rear** - parity vs bcfp (tunnel-free `streams_vw_bcfp`). Prove equivalence before PR. -- [ ] Update `research/provincial_parity_*.md` with the accessible columns + the combined result. +- [x] Merge `origin/221-per-wsg-habitat-access-km-rollup` into the #223 branch (`b53ad46`). +- [x] Combined package: `devtools::test()` **FAIL 1 | PASS 1298** (+44 from 221 rollup tests; the + 1 FAIL is the pre-existing environmental `public.wsg_outlet`). document() clean. +- [x] Re-ran 11-WSG cross-section with the fix (FINA/PARS/PCEA/LKEL + BULK/MORR/KISP/LFRA/USKE + + ELKR/KOTR). ~40 min local, no cyphers (BULK 5.9 / LFRA 9.4 min the largest). +- [x] Built + ran `data-raw/parity_crosssection.R` (tunnel-free; link `lnk_rollup_wsg` vs bcfp + `streams_vw_bcfp` `IN (1,2)`). **Result: accessible 44/44 exact (max 0.05%); spawn 42/43; + rear 34/35. The only 2 over-tol are BULK SK (spawn +11.0%, rear +35.2%) = documented parked + fresh#190 (Elwin+Day Lake topology). Zero #223 regressions.** #189 residence species excluded. +- [x] Wrote `research/parity_accessible_habitat_2026_07_03.md` (the accessible-column parity proof). - [ ] `/planning-archive`; `/gh-pr-push` (PR closes #221 + #223). NEWS/DESCRIPTION bump as final commit. - [ ] Return to the accessible_km vignette to demonstrate bcfp equivalence. diff --git a/research/parity_accessible_habitat_2026_07_03.md b/research/parity_accessible_habitat_2026_07_03.md new file mode 100644 index 00000000..ff381cd7 --- /dev/null +++ b/research/parity_accessible_habitat_2026_07_03.md @@ -0,0 +1,68 @@ +# Provincial parity — accessible + spawning + rearing (2026-07-03) + +Cross-section parity proof for the combined **#221** (per-WSG accessible_km roll-up) ++ **#223** (access-segmentation-frontier fix). Extends the historical spawn/rear +parity harness with the **accessible_km** column and re-runs a species/region +cross-section to prove the fix converges `accessible_km` without regressing habitat. + +## Run metadata + +- **Harness:** `data-raw/parity_crosssection.R`. link side = `lnk_rollup_wsg()` (#221); + bcfp side = tunnel-free `fresh.streams_vw_bcfp`. +- **Predicate:** `IN (1,2)` on `access_` / `spawning_` / `rearing_`. bcfp + codes these `0/1/2/3`; `IN (1,2)` is the presence predicate — a bare `= 1` + under-counts spawning/rearing (caught + corrected this session). +- **Reference:** `smnorris/bcfishpass@v0.7.15-41-g2917790`, `fresh.streams_vw_bcfp`. +- **Scope:** 11 WSGs re-run with the fix (`lnk_pipeline_run(mapping_code = TRUE)`), + spanning Peace / Fraser / Skeena / Columbia, all 8 species: + - Peace (BT-only): FINA, PARS, PCEA + - Fraser/Skeena (BT+ST+salmon): LKEL, BULK, MORR, KISP, LFRA, USKE + - Columbia/Kootenay (BT+WCT): ELKR, KOTR +- **Tolerance:** accessible ≤ 1%, habitat (spawn/rear) ≤ 5%. + +## Headline result — the fix works, no regression + +| metric | pairs | WSGs | species | max \|diff\| | within tol | +|---|---|---|---|---|---| +| **accessible** | 44 | 11 | 8 | **0.05%** | **44/44** | +| spawning | 43 | 11 | 8 | 10.99% | 42/43 | +| rearing | 35 | 11 | 6 | 35.20% | 34/35 | + +**Overall: 120/122 within tolerance.** `accessible_km` is exact everywhere +(max 0.05% across all 8 species × 11 WSGs) — that is the #223 fix, before which +FINA/PCEA were +23.6% / +40.4%. + +## The only two exceptions — both pre-existing, documented, parked + +`BULK SK` spawning **+11.0%** (link 27.1 / bcfp 24.4) and rearing **+35.2%** +(link 64.6 / bcfp 47.8). This is **fresh#190** — the Elwin + Day Lake dual-rearing-lake +topology on `blue_line_key 360846413`, documented at +`research/bcfishpass_methodology.md:124-141` (spawning +11.0% recorded there verbatim). +Pre-existing, unrelated to #223, and exactly the "if the rollup surfaces similar SK +bumps on multi-lake topologies" case that doc predicted. **No #223 regression.** + +## Excluded — species link does not model (#189 residence) + +bcfp lumps all salmon into one barrier group so its `access_` is populated +everywhere; link only models a species where `wsg_species_presence` declares it. These +are one-sided (bcfp present, link 0) and excluded from the parity assertion (tracked in +#189), not silent: + +- LKEL SK; BULK CM; MORR CM; USKE CM; USKE PK. + +## Convergence highlight (accessible_km, BT — the #223 target) + +| WSG | pre-fix | post-fix | +|---|---|---| +| FINA | +23.59% | **−0.02%** | +| PARS | +3.43% | **−0.01%** | +| PCEA | +40.36% | **−0.01%** | +| LKEL | +0.72% | **0.00%** | + +## Reproduce + +``` +# each WSG first re-run with the fix, then: +LNK_LOAD=loadall Rscript data-raw/parity_crosssection.R \ + FINA PARS PCEA LKEL BULK MORR KISP LFRA USKE ELKR KOTR +``` From d74702eb7771eb58c78a2c8c064e3e256e55d8e3 Mon Sep 17 00:00:00 2001 From: almac2022 Date: Fri, 3 Jul 2026 20:47:42 -0700 Subject: [PATCH 15/16] Consolidate 3 accessible-km scripts into one parity_crosssection.R MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduce data-raw script sprawl: fold accessible_km_fix_validate.R (#223 fix gate + structural checks) and accessible_km_proof_co.R (#221 coho-only accessible proof) into the single parity_crosssection.R, which already did the broader accessible + spawn + rear × 8-species × N-WSG sweep. parity_crosssection.R now also runs the #223 structural checks (FINA blk 359209845 frontier: breaks at frontier, above-frontier BT-blocked, accessible reach tops out) and exits non-zero on any accessible-over-tol, structural failure, or non-parked habitat divergence. Habitat parked cases (fresh#190 BULK SK) and #189 residence species (link-absent) are allowlisted. Full 11-WSG run PASSes (exit 0). Repoint lnk_rollup_wsg roxygen + the PWF Validation section to the single script. Net: -344 lines, 3 scripts -> 1. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014kSEJoad4Bc553qU8eFiE6 --- R/lnk_rollup_wsg.R | 5 +- data-raw/accessible_km_fix_validate.R | 224 -------------------------- data-raw/accessible_km_proof_co.R | 120 -------------- data-raw/parity_crosssection.R | 88 ++++++++-- man/lnk_rollup_wsg.Rd | 5 +- planning/active/task_plan.md | 6 +- 6 files changed, 83 insertions(+), 365 deletions(-) delete mode 100644 data-raw/accessible_km_fix_validate.R delete mode 100644 data-raw/accessible_km_proof_co.R diff --git a/R/lnk_rollup_wsg.R b/R/lnk_rollup_wsg.R index 0ea0b033..9e9b9ac2 100644 --- a/R/lnk_rollup_wsg.R +++ b/R/lnk_rollup_wsg.R @@ -31,8 +31,9 @@ #' #' `accessible_km` sums `access IN (1, 2)` — link's per-species access #' model on `streams_access`, the number validated against the -#' tunnel-free bcfp reference in `data-raw/accessible_km_proof_co.R` -#' (coho, 19/20 WSGs within +/-5%). It deliberately does **not** use the +#' tunnel-free bcfp reference in `data-raw/parity_crosssection.R` +#' (accessible + spawning + rearing, 8 species x 11 WSGs). It +#' deliberately does **not** use the #' `accessible` boolean on `streams_habitat_`, which carries #' different (pre-gating) semantics and diverges from the access model #' (MORR coho: 3424 km vs the validated 3330 km). diff --git a/data-raw/accessible_km_fix_validate.R b/data-raw/accessible_km_fix_validate.R deleted file mode 100644 index b3c30b75..00000000 --- a/data-raw/accessible_km_fix_validate.R +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env Rscript -# accessible_km_fix_validate.R — validate the #223 access-segmentation-frontier fix. -# -# THE BUG (segmentation, not access-decision): the stream break source -# `gradient_barriers_minimal` is fed the frs_barriers_minimal() downstream-most -# reduction of each per-species barrier set (lnk_pipeline_prepare.R:592), so -# streams do NOT break at per-species gradient/falls frontiers. One segment -# straddles the frontier and the whole reach — including the blocked part above -# the barrier — is credited accessible. Over-credit scales with the species -# access_gradient_max (BT 0.25 worst). Root cause: research/accessible_km_divergence.md. -# -# THIS SCRIPT IS READ-ONLY. It asserts against the currently-persisted DB state: -# - pre-fix -> FAILS (bug reproduces), exits non-zero. -# - post-fix -> PASSES once the validation-set WSGs are re-run through -# lnk_pipeline_run(..., mapping_code = TRUE) with the fix. -# It never writes; safe to run repeatedly. -# -# Usage: [LNK_LOAD=loadall] Rscript data-raw/accessible_km_fix_validate.R -# LNK_LOAD=loadall -> pkgload::load_all() (dev checkout) -# default -> library(link) -# -# VALIDATION SET: -# FINA / PARS / PCEA — Peace-region WSGs (above the Bennett dam): BT-only, the -# over-credit bug. Pre-fix BT accessible_km diverges +23.6% / +3.4% / +40.4%. -# LKEL — the SMALLEST persisted WSG carrying steelhead AND salmon (BT/ST/CO/CH/ -# CM/PK all present). Pre-fix it is already clean (BT +0.7%, ST/CO 0%). The fix -# changes segmentation for EVERY species' model, so a salmon+ST WSG is required -# to prove no-regression — the Peace WSGs cannot (they carry no salmon/ST). -# -# Expected PRE-FIX failures: FINA-BT, PARS-BT, PCEA-BT parity + all four FINA -# structural checks on the canonical evidence segment (blk 359209845). - -if (identical(Sys.getenv("LNK_LOAD"), "loadall")) { - suppressPackageStartupMessages(pkgload::load_all(quiet = TRUE)) -} else { - suppressPackageStartupMessages(library(link)) -} -suppressPackageStartupMessages({ - library(DBI) - library(RPostgres) -}) - -conn <- lnk_db_conn(dbname = "fwapg", host = "localhost", port = 5432L, - user = "postgres", password = "postgres") -on.exit(try(DBI::dbDisconnect(conn), silent = TRUE), add = TRUE) - -# ---- knobs ---------------------------------------------------------------- -tol_pct <- 1.0 # convergence bar: |link - bcfp| / bcfp * 100 -present_min <- 1.0 # accessible_km above which a (wsg, species) is "present" -frontier_blk <- 359209845L # canonical evidence blue_line_key (FINA, BT) -frontier_m <- 3834.78 # the BT frontier barrier measure on that blk -frontier_eps <- 1.0 # metres — position match tolerance - -wsgs <- c("FINA", "PARS", "PCEA", "LKEL") -species <- c("bt", "st", "co", "ch", "cm", "pk", "sk", "wct") -# bcfp reference predicate: barriers__dnstr = '' (empty = no barrier -# downstream = accessible). Verified identical to bcfp access_ IN (1,2). -# The five salmon species share one combined bcfp barrier column. -bcfp_grp <- c(bt = "barriers_bt_dnstr", - st = "barriers_st_dnstr", - co = "barriers_ch_cm_co_pk_sk_dnstr", - ch = "barriers_ch_cm_co_pk_sk_dnstr", - cm = "barriers_ch_cm_co_pk_sk_dnstr", - pk = "barriers_ch_cm_co_pk_sk_dnstr", - sk = "barriers_ch_cm_co_pk_sk_dnstr", - wct = "barriers_wct_dnstr") -# Known #189 species-residence differences (":"): bcfp models the species -# (shared salmon barrier group populates its access_), link does not — expected, -# NOT a #223 regression. Allowlisted so the one-sided-presence gate does not flag -# them, while an UNEXPECTED link collapse to ~0 km still fails loudly. -residence_exclude <- c("LKEL:sk") - -# ---- failure ledger ------------------------------------------------------- -fails <- character(0) -record_fail <- function(msg) { - fails[[length(fails) + 1L]] <<- msg - cat(" FAIL:", msg, "\n") -} -record_pass <- function(msg) cat(" pass:", msg, "\n") - -# link accessible_km: fresh.streams x fresh.streams_access on the full PK -# (id_segment, watershed_group_code) — the persisted-fresh join discipline (#203). -km_link <- function(wsg, sp) { - DBI::dbGetQuery(conn, sprintf( - "SELECT coalesce(sum(s.length_metre), 0) / 1000.0 AS km - FROM fresh.streams s - JOIN fresh.streams_access a - ON s.id_segment = a.id_segment - AND s.watershed_group_code = a.watershed_group_code - WHERE s.watershed_group_code = '%s' AND a.access_%s IN (1, 2)", - wsg, sp))$km -} -# bcfp accessible_km: the tunnel-free reference view (fresh.streams_vw_bcfp). -km_bcfp <- function(wsg, sp) { - DBI::dbGetQuery(conn, sprintf( - "SELECT coalesce(sum(length_metre), 0) / 1000.0 AS km - FROM fresh.streams_vw_bcfp - WHERE watershed_group_code = '%s' AND %s = ''", - wsg, bcfp_grp[[sp]]))$km -} - -# ---- 1. parity sweep ------------------------------------------------------ -cat("\n== accessible_km parity: link vs bcfp (assert co-present species |diff| <= ", - tol_pct, "%) ==\n", sep = "") -rows <- list() -for (w in wsgs) for (sp in species) { - lk <- km_link(w, sp) - bc <- km_bcfp(w, sp) - lk_present <- lk >= present_min - bc_present <- bc >= present_min - pct <- if (bc > 0) 100 * (lk - bc) / bc else NA_real_ - # Both present -> assert tolerance. A one-sided presence is a divergence the gate - # must catch (link zeroing a bcfp-present species is exactly a no-regression - # failure) — EXCEPT the allowlisted #189 residence cases. Both absent -> skip. - if (lk_present && bc_present) { - scope <- "assert" - status <- if (abs(pct) <= tol_pct) "pass" else "FAIL" - } else if (bc_present) { # bcfp has it, link doesn't - is_known <- paste(w, sp, sep = ":") %in% residence_exclude - scope <- if (is_known) "residence" else "link-absent" - status <- if (is_known) "n/a" else "FAIL" - } else if (lk_present) { # link has it, bcfp doesn't - scope <- "bcfp-absent" - status <- "FAIL" - } else { # absent both sides - scope <- "-" - status <- "n/a" - } - rows[[length(rows) + 1L]] <- data.frame( - wsg = w, sp = toupper(sp), link = round(lk, 1), bcfp = round(bc, 1), - diff_pct = round(pct, 2), scope = scope, status = status) -} -tab <- do.call(rbind, rows) -print(tab[tab$scope != "-", ], row.names = FALSE) -for (i in seq_len(nrow(tab))) { - if (tab$status[i] == "n/a") next - m <- sprintf("%s %s parity %+.2f%% (link %.1f / bcfp %.1f)", - tab$wsg[i], tab$sp[i], tab$diff_pct[i], tab$link[i], tab$bcfp[i]) - if (tab$status[i] == "pass") record_pass(m) else record_fail(m) -} - -# ---- 2. structural checks: canonical evidence segment --------------------- -# FINA blk 359209845, BT frontier 3834.78. Pre-fix a single segment -# [3390.6, 7998.1] straddles the frontier; post-fix it breaks at 3834.78 and the -# reach above is BT-blocked. -cat("\n== structural: FINA blk ", frontier_blk, " BT frontier ", frontier_m, " ==\n", sep = "") - -# S1 mechanism: the break source holds the frontier position (skip if the -# working schema was cleaned after persist — fresh.streams checks stand alone). -gbm_exists <- DBI::dbGetQuery(conn, - "SELECT count(*)::int AS n FROM information_schema.tables - WHERE table_schema = 'working_fina' AND table_name = 'gradient_barriers_minimal'")$n > 0L -if (!gbm_exists) { - cat(" skip: working_fina.gradient_barriers_minimal absent (working schema cleaned)\n") -} else { - fr <- DBI::dbGetQuery(conn, sprintf( - "SELECT count(*)::int AS n FROM working_fina.gradient_barriers_minimal - WHERE blue_line_key = %d AND abs(downstream_route_measure - %f) < %f", - frontier_blk, frontier_m, frontier_eps))$n - if (fr > 0L) { - record_pass("gradient_barriers_minimal holds the frontier break") - } else { - record_fail("gradient_barriers_minimal missing the frontier break (minimal reduction dropped it)") - } -} - -# S2 segmentation: no fresh.streams segment straddles the frontier. Breaks round -# to integer (measure_precision = 0L in lnk_pipeline_break), so the break lands at -# round(3834.78) = 3835 — exactly as bcfp does. A segment must span BEYOND the -# rounding window to count as a straddle; the clean below-frontier segment that -# ends at 3835 (0.22 m above the un-rounded frontier) is a proper break, not a -# straddle. The genuine pre-fix straddle [3390.6, 7998.1] still fails this. -span <- DBI::dbGetQuery(conn, sprintf( - "SELECT count(*)::int AS n FROM fresh.streams - WHERE blue_line_key = %d AND watershed_group_code = 'FINA' - AND downstream_route_measure < %f AND upstream_route_measure > %f", - frontier_blk, frontier_m - frontier_eps, frontier_m + frontier_eps))$n -if (span == 0L) { - record_pass(sprintf("no segment straddles the frontier (streams break at %.2f)", frontier_m)) -} else { - record_fail(sprintf("%d fresh.streams segment(s) straddle the frontier %.2f", span, frontier_m)) -} - -# S3 reclassification: the segment starting at the frontier is BT-blocked. -seg <- DBI::dbGetQuery(conn, sprintf( - "SELECT a.access_bt FROM fresh.streams s - JOIN fresh.streams_access a - ON s.id_segment = a.id_segment AND s.watershed_group_code = a.watershed_group_code - WHERE s.blue_line_key = %d AND s.watershed_group_code = 'FINA' - AND abs(s.downstream_route_measure - %f) < %f", - frontier_blk, frontier_m, frontier_eps)) -if (nrow(seg) == 0L) { - record_fail(sprintf("no fresh.streams segment starts at frontier %.2f (network did not break there)", frontier_m)) -} else if (isTRUE(all(seg$access_bt == 0L))) { - record_pass(sprintf("segment at %.2f is BT-blocked (access_bt = 0)", frontier_m)) -} else { - record_fail(sprintf("segment at %.2f has access_bt = %s (expected 0)", - frontier_m, paste(seg$access_bt, collapse = ","))) -} - -# S4 over-credit removed: the accessible BT reach on this blk tops out at the -# frontier (pre-fix it runs to 7998.1, ~4163 m of over-credit). -maxupm <- DBI::dbGetQuery(conn, sprintf( - "SELECT coalesce(max(s.upstream_route_measure), 0) AS m FROM fresh.streams s - JOIN fresh.streams_access a - ON s.id_segment = a.id_segment AND s.watershed_group_code = a.watershed_group_code - WHERE s.blue_line_key = %d AND s.watershed_group_code = 'FINA' AND a.access_bt IN (1, 2)", - frontier_blk))$m -if (maxupm <= frontier_m + frontier_eps) { - record_pass(sprintf("accessible BT reach tops out at %.1f (<= frontier)", maxupm)) -} else { - record_fail(sprintf("accessible BT reach extends to %.1f, above frontier %.2f (over-credit %.0f m)", - maxupm, frontier_m, maxupm - frontier_m)) -} - -# ---- verdict -------------------------------------------------------------- -cat("\n== verdict ==\n") -if (length(fails) == 0L) { - cat("ALL CHECKS PASS - #223 access-segmentation-frontier fix validated.\n") - quit(status = 0) -} -cat(sprintf("%d CHECK(S) FAILED - #223 bug present / fix incomplete:\n", length(fails))) -for (f in fails) cat(" -", f, "\n") -quit(status = 1) diff --git a/data-raw/accessible_km_proof_co.R b/data-raw/accessible_km_proof_co.R deleted file mode 100644 index 143e0b96..00000000 --- a/data-raw/accessible_km_proof_co.R +++ /dev/null @@ -1,120 +0,0 @@ -# data-raw/accessible_km_proof_co.R -# -# Phase-1 proof for link#221: link's coho `accessible_km` per WSG matches a -# tunnel-free bcfp reference within +/- 5%. -# -# link side: sum(streams.length_metre)/1000 where streams_access.access_co -# IN (1,2), joined on the full PK (id_segment, watershed_group_code) -# (#203 discipline; length lives on streams, not streams_access). -# ref side: sum(fresh.streams_vw_bcfp.length_metre)/1000 where -# barriers_ch_cm_co_pk_sk_dnstr = '' (empty = no barrier downstream). -# -# The snapshot stores barriers__dnstr as comma-joined TEXT, so the -# accessible predicate is `= ''`, NOT `= array[]::text[]` (which errors: -# "operator does not exist: character varying = text[]"). -# -# Runs for every WSG present in BOTH fresh.streams_access and -# fresh.streams_vw_bcfp. Prints the per-WSG table and stops non-zero if any -# UNEXPECTED WSG exceeds |pct_diff| > 5. Validated 2026-07-01 on local docker -# fwapg: 19/20 WSGs within +/-5% (most < 1%; MORR 0.09%, BULK 0.27%). -# -# Known divergences (excluded from the hard-fail, still printed + flagged): -# SETN — bcfp `barriers_subsurfaceflow` is stale for ~95% of SETN segments, -# propagating into bcfp's `barriers_ch_cm_co_pk_sk_dnstr` so bcfp UNDER-credits -# accessible habitat; link correctly applies user_barriers_definite_control. -# link > ref here is the expected direction. Taxonomy: setn-anadr-*-stale in -# research/bcfp_divergence_taxonomy.yml. Not a link defect (bcfp-side stale). -# -# Usage: Rscript data-raw/accessible_km_proof_co.R - -main <- function() { - conn <- link::lnk_db_conn( - dbname = "fwapg", host = "localhost", port = 5432L, - user = "postgres", password = "postgres") - on.exit(try(DBI::dbDisconnect(conn), silent = TRUE), add = TRUE) - - tolerance_pct <- 5 - known_divergence <- c("SETN") - - # Comparison universe = coho-PRESENT WSGs: those with >= 1 coho-accessible - # segment (the `lnk` CTE). This is deliberate. link models access per species - # (`access_co`), so coho-absent WSGs have access_co = -9 everywhere and 0 - # accessible km; bcfp models CO inside the salmon group `barriers_ch_cm_co_pk_sk`, - # which is accessible wherever ANY of CH/CM/CO/PK/SK reaches. Including - # coho-absent WSGs would compare link's 0 against bcfp's other-salmon km -> false - # -100% divergence. Scoping to `lnk` compares only where a coho km number is - # meaningful. LEFT JOIN ref still surfaces the dangerous case (link has coho km, - # bcfp shows none) as a NULL ref_km caught by the null_ref guard below. - sql <- " -WITH lnk AS ( - SELECT s.watershed_group_code AS wsg, - sum(s.length_metre) / 1000.0 AS link_km - FROM fresh.streams s - JOIN fresh.streams_access a - ON s.id_segment = a.id_segment - AND s.watershed_group_code = a.watershed_group_code - WHERE a.access_co IN (1, 2) - GROUP BY s.watershed_group_code -), -ref AS ( - SELECT watershed_group_code AS wsg, - sum(length_metre) / 1000.0 AS ref_km - FROM fresh.streams_vw_bcfp - WHERE barriers_ch_cm_co_pk_sk_dnstr = '' - GROUP BY watershed_group_code -) -SELECT lnk.wsg, - round(lnk.link_km::numeric, 2) AS link_km, - round(ref.ref_km::numeric, 2) AS ref_km, - round((100.0 * (lnk.link_km - ref.ref_km) - / nullif(ref.ref_km, 0))::numeric, 2) AS pct_diff -FROM lnk -LEFT JOIN ref USING (wsg) -ORDER BY lnk.wsg; -" - - res <- DBI::dbGetQuery(conn, sql) - - if (nrow(res) == 0L) { - stop("No WSGs present in both fresh.streams_access and fresh.streams_vw_bcfp - ", - "nothing to prove. Persist at least one WSG first.", call. = FALSE) - } - - res$known_divergence <- res$wsg %in% known_divergence - - cat("\nCoho accessible_km parity (link vs tunnel-free bcfp snapshot)\n") - print(res, row.names = FALSE) - - # ref_km NULL -> nullif(0) -> NA pct_diff: bcfp reports no accessible km for a - # WSG link DID build (link_km may be > 0). Either a data problem (missing - # snapshot rows) or a genuine 0-vs-nonzero divergence. Fail loud before the - # tolerance check, where an NA index would otherwise slip through as exit 0. - null_ref <- res[is.na(res$pct_diff), , drop = FALSE] - if (nrow(null_ref) > 0L) { - stop(sprintf("bcfp ref_km is 0/NULL for WSG(s) link built: %s - check ", - paste(null_ref$wsg, collapse = ", ")), - "streams_vw_bcfp populate or investigate as divergence.", call. = FALSE) - } - - over <- res[abs(res$pct_diff) > tolerance_pct, , drop = FALSE] - flagged_known <- over[over$known_divergence, , drop = FALSE] - if (nrow(flagged_known) > 0L) { - cat(sprintf("\nKnown divergence(s) over +/-%g%% (expected, bcfp-side):\n", - tolerance_pct)) - print(flagged_known[, c("wsg", "link_km", "ref_km", "pct_diff")], - row.names = FALSE) - } - - unexpected <- over[!over$known_divergence, , drop = FALSE] - if (nrow(unexpected) > 0L) { - stop(sprintf("%d UNEXPECTED WSG(s) exceed +/-%g%%: %s", - nrow(unexpected), tolerance_pct, - paste(unexpected$wsg, collapse = ", ")), call. = FALSE) - } - - n_ok <- sum(!res$known_divergence) - cat(sprintf("\nPASS: all %d non-divergence WSG(s) within +/-%g%%.\n", - n_ok, tolerance_pct)) -} - -main() diff --git a/data-raw/parity_crosssection.R b/data-raw/parity_crosssection.R index cc5a6c7f..f223e8dd 100644 --- a/data-raw/parity_crosssection.R +++ b/data-raw/parity_crosssection.R @@ -1,15 +1,23 @@ #!/usr/bin/env Rscript -# parity_crosssection.R — tunnel-free accessible + spawning + rearing parity -# across a WSG cross-section. The provincial-parity harness EXTENDED with the -# accessible column (#221) to prove the #223 access-segmentation fix converges -# accessible_km without regressing habitat. +# parity_crosssection.R — the single accessible + spawning + rearing parity +# validator/proof for the #221 (accessible_km roll-up) + #223 (access-segmentation +# fix) work. Supersedes the earlier accessible_km_proof_co.R (coho-only) and +# accessible_km_fix_validate.R (fix gate) — both folded in here. # -# link side = lnk_rollup_wsg() (#221): streams x streams_access x streams_habitat_ -# on the full PK, accessible = access_ IN (1,2). -# bcfp side = fresh.streams_vw_bcfp (tunnel-free snapshot), IN (1,2) predicate on -# access_ / spawning_ / rearing_ (bcfp codes these 0/1/2/3; -# IN (1,2) is the presence predicate — a bare `= 1` UNDER-counts). -# Note: bcfp has no rearing_cm / rearing_pk (chum/pink do not rear in freshwater). +# Two jobs, one script: +# 1. PARITY SWEEP — link vs bcfp for accessible/spawning/rearing per (WSG, species). +# link side = lnk_rollup_wsg() (#221): streams x streams_access x streams_habitat_ +# on the full PK, accessible = access_ IN (1,2). +# bcfp side = tunnel-free fresh.streams_vw_bcfp, IN (1,2) predicate (bcfp codes +# access/spawning/rearing 0/1/2/3; a bare `= 1` UNDER-counts). No rearing_cm/_pk +# (chum/pink don't rear in freshwater). +# 2. STRUCTURAL — the #223 mechanism proof on the canonical evidence segment +# (FINA blk 359209845, BT frontier 3834.78): streams break at the frontier, the +# reach above is BT-blocked, the accessible reach tops out at the frontier. +# +# Exits non-zero if any ACCESSIBLE pair breaches tol, any STRUCTURAL check fails, or +# any habitat pair breaches tol that is NOT a known parked departure. Habitat parked +# cases (fresh#190 BULK SK) and #189 residence species (link-absent) are allowlisted. # # Prereq: each WSG re-run through lnk_pipeline_run(mapping_code = TRUE) with the fix. # Usage: [LNK_LOAD=loadall] Rscript data-raw/parity_crosssection.R [WSG ...] @@ -33,11 +41,17 @@ wsgs <- if (length(args)) toupper(args) else c("FINA", "PARS", "PCEA", "L species <- c("bt", "st", "co", "ch", "cm", "pk", "sk", "wct") rear_sp <- c("bt", "ch", "co", "sk", "st", "wct") present_min <- 1.0 -tol_acc <- 1.0 # accessible: the fix should make this tight +tol_acc <- 1.0 # accessible: the #223 fix should make this tight tol_hab <- 5.0 # spawn / rear: legitimate methodology wiggle +hab_parked <- c("BULK:SK") # documented parked habitat departures (fresh#190) — not a #223 regression col_map <- c(accessible = "accessible_km", spawning = "spawning_km", rearing = "rearing_km") bc_prefix <- c(accessible = "acc_", spawning = "spawn_", rearing = "rear_") +# canonical #223 evidence segment +frontier_blk <- 359209845L +frontier_m <- 3834.78 +frontier_eps <- 1.0 + bcfp_rollup <- function(w) { cols <- unlist(lapply(species, function(sp) { r <- if (sp %in% rear_sp) { @@ -58,6 +72,7 @@ num <- function(x) { if (length(x) == 0L || is.na(x)) 0 else x } +# ---- 1. parity sweep ------------------------------------------------------ rows <- list() absent <- character(0) for (w in wsgs) { @@ -77,22 +92,24 @@ for (w in wsgs) { } pct <- 100 * (lkv - bcv) / bcv tol <- if (m == "accessible") tol_acc else tol_hab + parked <- m != "accessible" && paste(w, sp_up, sep = ":") %in% hab_parked rows[[length(rows) + 1L]] <- data.frame( wsg = w, sp = sp_up, metric = m, link = round(lkv, 1), bcfp = round(bcv, 1), - diff_pct = round(pct, 2), ok = abs(pct) <= tol) + diff_pct = round(pct, 2), ok = abs(pct) <= tol, parked = parked) } } } tab <- do.call(rbind, rows) cat("\n============ accessible + spawn + rear parity (link vs bcfp, tunnel-free) ============\n") -print(tab, row.names = FALSE) +print(tab[, c("wsg", "sp", "metric", "link", "bcfp", "diff_pct", "ok")], row.names = FALSE) cat("\n==== summary by metric (accessible tol", tol_acc, "%, habitat tol", tol_hab, "%) ====\n") for (m in c("accessible", "spawning", "rearing")) { sub <- tab[tab$metric == m, ] fails <- sub[!sub$ok, ] over <- if (nrow(fails)) { - paste0(" OVER: ", paste(sprintf("%s/%s %+.1f%%", fails$wsg, fails$sp, fails$diff_pct), collapse = "; ")) + paste0(" OVER: ", paste(sprintf("%s/%s %+.1f%%%s", fails$wsg, fails$sp, fails$diff_pct, + ifelse(fails$parked, " (parked)", "")), collapse = "; ")) } else { "" } @@ -104,4 +121,45 @@ if (length(absent)) { cat("\n link-absent (bcfp models species, link does not — #189 residence, excluded):\n") for (a in absent) cat(" ", a, "\n") } -cat(sprintf("\nOVERALL: %d/%d pairs within tolerance (%d over)\n", sum(tab$ok), nrow(tab), sum(!tab$ok))) + +# ---- 2. structural checks: canonical #223 evidence segment ---------------- +struct_fail <- character(0) +if ("FINA" %in% wsgs) { + cat("\n==== structural: FINA blk", frontier_blk, "BT frontier", frontier_m, "====\n") + # no fresh.streams segment straddles the frontier (breaks round to integer 3835, as bcfp) + span <- DBI::dbGetQuery(conn, sprintf( + "SELECT count(*)::int n FROM fresh.streams WHERE blue_line_key=%d AND watershed_group_code='FINA' + AND downstream_route_measure < %f AND upstream_route_measure > %f", + frontier_blk, frontier_m - frontier_eps, frontier_m + frontier_eps))$n + # the segment starting at the frontier is BT-blocked; accessible BT reach tops out there + seg <- DBI::dbGetQuery(conn, sprintf( + "SELECT a.access_bt FROM fresh.streams s JOIN fresh.streams_access a + ON s.id_segment=a.id_segment AND s.watershed_group_code=a.watershed_group_code + WHERE s.blue_line_key=%d AND s.watershed_group_code='FINA' + AND abs(s.downstream_route_measure - %f) < %f", frontier_blk, frontier_m, frontier_eps)) + maxupm <- DBI::dbGetQuery(conn, sprintf( + "SELECT coalesce(max(s.upstream_route_measure),0) m FROM fresh.streams s JOIN fresh.streams_access a + ON s.id_segment=a.id_segment AND s.watershed_group_code=a.watershed_group_code + WHERE s.blue_line_key=%d AND s.watershed_group_code='FINA' AND a.access_bt IN (1,2)", frontier_blk))$m + checks <- c( + "no segment straddles the frontier" = span == 0L, + "segment at frontier is BT-blocked" = nrow(seg) > 0L && isTRUE(all(seg$access_bt == 0L)), + "accessible BT reach tops at frontier" = maxupm <= frontier_m + frontier_eps) + for (nm in names(checks)) { + cat(sprintf(" %s: %s\n", ifelse(checks[[nm]], "pass", "FAIL"), nm)) + if (!checks[[nm]]) struct_fail <- c(struct_fail, nm) + } +} + +# ---- verdict -------------------------------------------------------------- +acc_fail <- tab[tab$metric == "accessible" & !tab$ok, ] +hab_fail <- tab[tab$metric != "accessible" & !tab$ok & !tab$parked, ] +cat(sprintf("\nOVERALL: %d/%d parity pairs within tolerance (%d over: %d accessible, %d habitat non-parked)", + sum(tab$ok), nrow(tab), sum(!tab$ok), nrow(acc_fail), nrow(hab_fail))) +n_fail <- nrow(acc_fail) + nrow(hab_fail) + length(struct_fail) +if (n_fail == 0L) { + cat(" — PASS\n") + quit(status = 0) +} +cat(sprintf(" — %d FAILURE(S)\n", n_fail)) +quit(status = 1) diff --git a/man/lnk_rollup_wsg.Rd b/man/lnk_rollup_wsg.Rd index 2bd05c27..a835802c 100644 --- a/man/lnk_rollup_wsg.Rd +++ b/man/lnk_rollup_wsg.Rd @@ -77,8 +77,9 @@ this for WSG totals; use those for per-crossing upstream summaries. \code{accessible_km} sums \verb{access IN (1, 2)} — link's per-species access model on \code{streams_access}, the number validated against the -tunnel-free bcfp reference in \code{data-raw/accessible_km_proof_co.R} -(coho, 19/20 WSGs within +/-5\%). It deliberately does \strong{not} use the +tunnel-free bcfp reference in \code{data-raw/parity_crosssection.R} +(accessible + spawning + rearing, 8 species x 11 WSGs). It +deliberately does \strong{not} use the \code{accessible} boolean on \verb{streams_habitat_}, which carries different (pre-gating) semantics and diverges from the access model (MORR coho: 3424 km vs the validated 3330 km). diff --git a/planning/active/task_plan.md b/planning/active/task_plan.md index 8fd165e6..85bc79e6 100644 --- a/planning/active/task_plan.md +++ b/planning/active/task_plan.md @@ -127,8 +127,10 @@ parity pass — inseparable for the proof. Merged 221→223 (PWF conflicts only; - [ ] Return to the accessible_km vignette to demonstrate bcfp equivalence. ## Validation -- `Rscript data-raw/accessible_km_fix_validate.R` exits non-zero if any WSG BT - `|pct_diff|` exceeds tolerance or segment 3835 is not blocked. +- `LNK_LOAD=loadall Rscript data-raw/parity_crosssection.R ` — the single + accessible+spawn+rear parity validator/proof (supersedes accessible_km_fix_validate.R + + accessible_km_proof_co.R, both folded in). Exits non-zero on any accessible-over-tol, + structural FINA-frontier failure, or non-parked habitat divergence. - `Rscript -e 'devtools::test()' 2>&1 | grep -E "(FAIL|ERROR|PASS)"` green. ## Out of scope From 51f07a422de58669fc23b278e3d398b0f6e790e4 Mon Sep 17 00:00:00 2001 From: almac2022 Date: Fri, 3 Jul 2026 20:57:51 -0700 Subject: [PATCH 16/16] Archive planning files for #221 + #223 (accessible_km parity) --- .../.gitkeep | 0 .../README.md | 15 +++++++++++++++ .../findings.md | 0 .../progress.md | 0 .../task_plan.md | 0 5 files changed, 15 insertions(+) create mode 100644 planning/archive/2026-07-issue-221-223-accessible-km-parity/.gitkeep create mode 100644 planning/archive/2026-07-issue-221-223-accessible-km-parity/README.md rename planning/{active => archive/2026-07-issue-221-223-accessible-km-parity}/findings.md (100%) rename planning/{active => archive/2026-07-issue-221-223-accessible-km-parity}/progress.md (100%) rename planning/{active => archive/2026-07-issue-221-223-accessible-km-parity}/task_plan.md (100%) diff --git a/planning/archive/2026-07-issue-221-223-accessible-km-parity/.gitkeep b/planning/archive/2026-07-issue-221-223-accessible-km-parity/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/planning/archive/2026-07-issue-221-223-accessible-km-parity/README.md b/planning/archive/2026-07-issue-221-223-accessible-km-parity/README.md new file mode 100644 index 00000000..48f74481 --- /dev/null +++ b/planning/archive/2026-07-issue-221-223-accessible-km-parity/README.md @@ -0,0 +1,15 @@ +## Outcome + +Combined **#221** (per-WSG `accessible_km` roll-up) + **#223** (access-segmentation-frontier fix), shipped as one PR. + +**#223 root cause:** `gradient_barriers_minimal` was fed the `fresh::frs_barriers_minimal()` downstream-most reduction as a *segmentation* source, so a stream segment straddled the accessibility frontier and its whole reach — including the blocked part above the barrier — was credited accessible. BT/ST `accessible_km` over-credited up to +40% (PCEA), +23.6% (FINA). **Fix** (one file, `lnk_pipeline_prepare.R`): union the RAW per-model gradient + falls positions into `gradient_barriers_minimal` so streams break at every frontier — mirroring the orphan path and matching bcfishpass. `frs_barriers_minimal` is now unused in link. + +**#221** added the `accessible_km` column to `lnk_compare_rollup` + a reusable `lnk_rollup_wsg()`. + +**Proof** (`research/parity_accessible_habitat_2026_07_03.md`, `data-raw/parity_crosssection.R`): across **11 WSGs × 8 species** (Peace/Fraser/Skeena/Columbia), `accessible_km` converges **44/44 within 0.05%**; spawning/rearing hold, the only 2 over-tolerance being the documented parked **BULK SK** (fresh#190 dual-rearing-lake topology). Zero regressions. Consolidated 3 accessible-km scripts into one. + +**Key learnings:** bcfp `spawning_`/`rearing_`/`access_` are coded **0/1/2/3** — use `IN (1,2)`, a bare `= 1` under-counts (cost me a false "+42% spawning divergence" scare). The fix is **habitat-neutral** (byte-identical spawn/rear pre/post). Segment count grows **2–3.5×** (inherent to bcfp-matching; intersects perf #205). + +**Follow-ups filed:** #224 (bcfp `dam_dnstr_ind` reservoir-inflow propagation), #225 (rename `gradient_barriers_minimal` → `gradient_barriers_break`), #226 (extend PARS vignette for `accessible_km`), #227 (`wsg_outlet` builder + single-WSG downstream-state guard). + +Closed by: branch `223-access-segmentation-frontier` — PR closes #221 + #223. diff --git a/planning/active/findings.md b/planning/archive/2026-07-issue-221-223-accessible-km-parity/findings.md similarity index 100% rename from planning/active/findings.md rename to planning/archive/2026-07-issue-221-223-accessible-km-parity/findings.md diff --git a/planning/active/progress.md b/planning/archive/2026-07-issue-221-223-accessible-km-parity/progress.md similarity index 100% rename from planning/active/progress.md rename to planning/archive/2026-07-issue-221-223-accessible-km-parity/progress.md diff --git a/planning/active/task_plan.md b/planning/archive/2026-07-issue-221-223-accessible-km-parity/task_plan.md similarity index 100% rename from planning/active/task_plan.md rename to planning/archive/2026-07-issue-221-223-accessible-km-parity/task_plan.md