Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: link
Title: Stream Network Habitat Interpretation (Experimental)
Version: 0.21.0
Version: 0.22.0
Authors@R: c(
person("Allan", "Irvine", , "airvine@newgraphenvironment.com",
role = c("aut", "cre"),
Expand Down Expand Up @@ -31,7 +31,7 @@ Suggests:
bcdata,
digest,
dplyr,
fresh (>= 0.26.0),
fresh (>= 0.27.5),
lintr,
mockery,
sf,
Expand Down
16 changes: 16 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
# link 0.22.0

Wires `fresh::frs_order_child` into the pipeline as link methodology — small streams plugging directly into large rivers can be credited as rearing despite low/missing FWA channel-width estimates. Closes [fresh#158](https://github.com/NewGraphEnvironment/fresh/issues/158) on the link side.

- Four new per-species columns in `dimensions.csv` (both bundles), all opt-in via `rear_stream_order_bypass: yes/no`:
- `rear_stream_order_parent_min` — min order at the trib BLK's mouth confluence (default 5, matches bcfp)
- `rear_stream_order_child_min` — lower bound on segment's own stream_order (default 1)
- `rear_stream_order_child_max` — upper bound on segment's own stream_order (default 1)
- `rear_stream_order_distance_max` — cap (m) on distance from trib mouth (empty = no cap)
- `lnk_rules_build` emits the values into a `channel_width_min_bypass:` block on the rear stream-edge rule. `lnk_pipeline_classify` reads the block and calls `frs_order_child` per species post-classification, gated on `rear_stream_order_bypass`.
- Both bundles ship `bypass: no` for all species — infrastructure is parametric and tested but disabled by default. Re-enable per species via `dimensions.csv`. The 4-WSG regression (HARR / HORS / LFRA / BABL) is byte-identical to the pre-#96 baseline with bypass=off, confirming the wiring is purely additive when disabled.
- Updates `inst/extdata/configs/dimensions_columns.csv` xref doc with all four new columns and refreshes the `rear_stream_order_bypass` entry (was stale — said "currently inert").
- Bumps fresh dep to `>= 0.27.5` for the renamed bypass YAML schema (`stream_order` → `stream_order_min` + `stream_order_max`).

Related: [link#23](https://github.com/NewGraphEnvironment/link/issues/23) (CH spawning misread, closed not-a-bug). PWF for the wire-up at `planning/active/`.

# link 0.21.0

Closes [#87](https://github.com/NewGraphEnvironment/link/pull/94). Default-bundle SK upstream-spawn now credits any spawn-eligible segment upstream of and accessible from a qualifying rearing waterbody, dropping bcfishpass's restrictive cluster + lake-adjacency gate. bcfishpass-bundle SK keeps the gate (parity preserved).
Expand Down
37 changes: 37 additions & 0 deletions R/lnk_pipeline_classify.R
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,43 @@ lnk_pipeline_classify <- function(conn, aoi, cfg, loaded, schema,
verbose = FALSE)
}

# fresh#158 stream-order bypass: post-classification, credit direct
# tributaries of large-order rivers as rearing even when channel
# width is below threshold. Mirrors bcfp's hard-coded
# `stream_order_parent >= 5 AND stream_order = 1` predicate in
# load_habitat_linear_<sp>.sql for BT/CH/CO/ST/WCT.
#
# Per-species opt-in driven by `dimensions.csv::rear_stream_order_bypass`,
# which `lnk_rules_build` propagates into rules.yaml as
# `rear[].channel_width_min_bypass = list(stream_order, stream_order_parent_min)`.
# We detect that field's presence on any rear rule and call
# `frs_order_child` with the embedded parent_order threshold.
for (sp in species) {
rear_rules <- params[[sp]][["rules"]][["rear"]]
bypass <- NULL
for (rr in rear_rules) {
if (!is.null(rr[["channel_width_min_bypass"]])) {
bypass <- rr[["channel_width_min_bypass"]]
break
}
}
if (!is.null(bypass)) {
pom <- bypass[["stream_order_parent_min"]] %||% 5L
cs_min <- bypass[["stream_order_min"]]
cs_max <- bypass[["stream_order_max"]]
dmax <- bypass[["distance_max"]]
fresh::frs_order_child(conn,
table = "fresh.streams",
habitat = "fresh.streams_habitat",
species = sp,
parent_order_min = pom,
child_order_min = cs_min,
child_order_max = cs_max,
distance_max = dmax,
verbose = FALSE)
}
}

invisible(conn)
}

Expand Down
47 changes: 44 additions & 3 deletions R/lnk_rules_build.R
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,22 @@ lnk_rules_build <- function(csv,
tolower(trimws(dimensions$rear_stream_order_bypass)) == "yes"
}

# Optional: per-species `stream_order_parent_min` for the bypass.
# Default 5L matches bcfishpass's hard-coded predicate; the column lets
# callers tune the threshold without editing the rules YAML by hand.
has_sopm <- "rear_stream_order_parent_min" %in% names(dimensions)

# Optional: per-species child-order range for the bypass. Both default to
# 1L (matches bcfp). `_min` and `_max` map to fresh's frs_order_child
# `child_order_min` and `child_order_max` arguments.
has_csmin <- "rear_stream_order_child_min" %in% names(dimensions)
has_csmax <- "rear_stream_order_child_max" %in% names(dimensions)

# Optional: per-species `distance_max` for the bypass — caps the bypass
# to the lower N metres of each direct-trib BLK (segment's
# downstream_route_measure <= distance_max). Empty → no cap (whole BLK).
has_sodm <- "rear_stream_order_distance_max" %in% names(dimensions)

# Optional: requires_connected columns (value is the habitat type, not yes/no)
has_spawn_rc <- "spawn_requires_connected" %in% names(dimensions)
has_rear_rc <- "rear_requires_connected" %in% names(dimensions)
Expand Down Expand Up @@ -266,10 +282,35 @@ lnk_rules_build <- function(csv,
if (!is.na(rlhm)) lake_rule$lake_ha_min <- rlhm
rear_rules[[1]] <- add_rc(lake_rule, rear_rc, rear_cdm)
} else {
# Stream order bypass: first-order streams with parent order >= 5
# bypass rearing channel_width_min
# Stream order bypass: first-order streams with parent order
# >= stream_order_parent_min bypass rearing channel_width_min.
# Threshold defaults to 5L (bcfishpass parity); per-species
# `rear_stream_order_parent_min` column overrides when present
# and numeric.
soe_bypass <- if (has_soe && d$rear_stream_order_bypass) {
list(stream_order = 1L, stream_order_parent_min = 5L)
# Helper: read a positive integer column with default fallback
read_pos_int <- function(raw, default) {
if (is.null(raw) || is.na(raw) ||
nchar(trimws(as.character(raw))) == 0L) return(default)
n <- suppressWarnings(as.integer(raw))
if (is.na(n) || n < 1L) return(default)
n
}
pom <- read_pos_int(if (has_sopm) d$rear_stream_order_parent_min else NULL, 5L)
cs_min <- read_pos_int(if (has_csmin) d$rear_stream_order_child_min else NULL, 1L)
cs_max <- read_pos_int(if (has_csmax) d$rear_stream_order_child_max else NULL, 1L)
out <- list(stream_order_min = cs_min,
stream_order_max = cs_max,
stream_order_parent_min = pom)
if (has_sodm) {
raw_dm <- d$rear_stream_order_distance_max
if (!is.null(raw_dm) && !is.na(raw_dm) &&
nchar(trimws(as.character(raw_dm))) > 0) {
dm <- suppressWarnings(as.numeric(raw_dm))
if (!is.na(dm) && dm > 0) out$distance_max <- dm
}
}
out
} else {
NULL
}
Expand Down
101 changes: 75 additions & 26 deletions data-raw/maps/_lnk_map_compare.R
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,10 @@ lnk_map_compare <- function(wsg, species, habitat,
function() {
c <- conn_local(); on.exit(DBI::dbDisconnect(c))
sf::st_read(c, query = sprintf("
SELECT linear_feature_id, blue_line_key, edge_type, geom
SELECT linear_feature_id, blue_line_key,
downstream_route_measure, edge_type,
stream_order, gradient,
gnis_name, geom
FROM whse_basemapping.fwa_stream_networks_sp
WHERE watershed_group_code = '%s'", wsg))
})
Expand Down Expand Up @@ -201,19 +204,23 @@ lnk_map_compare <- function(wsg, species, habitat,
" | ", round(length_metre), "m"))
}

axis_combined <- dplyr::bind_rows(
bcfp_axis |> dplyr::filter(category == "bcfp_only") |> mk_label("bcfp-only"),
link_axis |> dplyr::filter(category == "link_only") |> mk_label("link-only"),
link_axis |> dplyr::filter(category == "both") |> mk_label("both"))
# Split axis layers per category so each is independently toggleable
# in the layers control (link_only / bcfp_only / both).
axis_bcfp_only <- bcfp_axis |>
dplyr::filter(category == "bcfp_only") |> mk_label("bcfp-only")
axis_link_only <- link_axis |>
dplyr::filter(category == "link_only") |> mk_label("link-only")
axis_both <- link_axis |>
dplyr::filter(category == "both") |> mk_label("both")

other_label <- if (habitat == "rearing") "spawning" else "rearing"
other_combined <- dplyr::bind_rows(
bcfp_other |> mk_label(paste("bcfp", other_label)),
link_other |> mk_label(paste("link", other_label)))
other_bcfp <- bcfp_other |> mk_label(paste("bcfp", other_label))
other_link <- link_other |> mk_label(paste("link", other_label))

# ---- bbox: full WSG (let user zoom) ------------------------------------

fb <- sf::st_bbox(dplyr::bind_rows(bcfp_axis, link_axis, bcfp_other, link_other))
fb <- sf::st_bbox(dplyr::bind_rows(
axis_bcfp_only, axis_link_only, axis_both, other_bcfp, other_link))
pad <- 0.02
dx <- fb["xmax"] - fb["xmin"]; dy <- fb["ymax"] - fb["ymin"]
view_bbox <- sf::st_bbox(c(
Expand All @@ -223,10 +230,11 @@ lnk_map_compare <- function(wsg, species, habitat,

# ---- mapgl --------------------------------------------------------------

pal_values <- c("bcfp_only", "link_only", "both")
pal_colors <- c("#1f78b4", "#e31a1c", "#6a3d9a")
pal_labels <- c("bcfp only (link missing)", "link only (extra)", "both")
other_color <- "#33a02c"
col_bcfp_only <- "#1f78b4"
col_link_only <- "#e31a1c"
col_both <- "#6a3d9a"
col_other_bcfp <- "#33a02c" # green (bcfp side of "other" habitat context)
col_other_link <- "#b15928" # brown (link side)

legend_title <- sprintf("%s %s %s — link vs bcfishpass",
wsg, species, habitat)
Expand All @@ -237,21 +245,56 @@ lnk_map_compare <- function(wsg, species, habitat,
fill_color = "#a6cee3", fill_opacity = 0.4, popup = "gnis_name")

if (underlay) {
streams_under <- streams_under |>
dplyr::mutate(under_label = paste0(
"fwa | lfid ", linear_feature_id,
" | blkey ", blue_line_key,
" | DRM ", round(downstream_route_measure %||% NA),
" | edge ", edge_type,
" | order ", stream_order,
" | grad ", formatC(gradient, format = "f", digits = 4),
ifelse(is.na(gnis_name) | gnis_name == "", "", paste0(" | ", gnis_name))))
m <- m |> mapgl::add_line_layer(
id = "streams_all", source = streams_under,
line_color = "#cccccc", line_width = 0.4, line_opacity = 0.6)
line_color = "#cccccc", line_width = 0.4, line_opacity = 0.6,
popup = "under_label")
}

m <- m |>
mapgl::add_line_layer(
id = other_label, source = other_combined,
line_color = other_color, line_width = 1.5, line_opacity = 0.85,
popup = "label") |>
mapgl::add_line_layer(
id = habitat, source = axis_combined,
line_color = mapgl::match_expr("category",
values = pal_values, stops = pal_colors, default = "#999999"),
line_width = 3, line_opacity = 0.9, popup = "label")
# Spawn / rear context layers (the OTHER habitat) — split per side so
# the user can isolate "where does bcfp see spawn?" independently of
# link's spawn picture.
if (nrow(other_bcfp) > 0) {
m <- m |> mapgl::add_line_layer(
id = sprintf("%s_bcfp", other_label), source = other_bcfp,
line_color = col_other_bcfp, line_width = 1.5, line_opacity = 0.85,
popup = "label")
}
if (nrow(other_link) > 0) {
m <- m |> mapgl::add_line_layer(
id = sprintf("%s_link", other_label), source = other_link,
line_color = col_other_link, line_width = 1.5, line_opacity = 0.85,
popup = "label")
}

# Axis (target habitat) layers — three independent toggles.
if (nrow(axis_both) > 0) {
m <- m |> mapgl::add_line_layer(
id = sprintf("%s_both", habitat), source = axis_both,
line_color = col_both, line_width = 3, line_opacity = 0.9,
popup = "label")
}
if (nrow(axis_bcfp_only) > 0) {
m <- m |> mapgl::add_line_layer(
id = sprintf("%s_bcfp_only", habitat), source = axis_bcfp_only,
line_color = col_bcfp_only, line_width = 3, line_opacity = 0.9,
popup = "label")
}
if (nrow(axis_link_only) > 0) {
m <- m |> mapgl::add_line_layer(
id = sprintf("%s_link_only", habitat), source = axis_link_only,
line_color = col_link_only, line_width = 3, line_opacity = 0.9,
popup = "label")
}

for (lyr in extra_layers) {
m <- m |> mapgl::add_line_layer(
Expand All @@ -265,8 +308,14 @@ lnk_map_compare <- function(wsg, species, habitat,
m <- m |>
mapgl::add_legend(
legend_title,
values = c(pal_labels, paste(other_label, "(either)")),
colors = c(pal_colors, other_color),
values = c(
"bcfp only (link missing)",
"link only (extra)",
"both",
sprintf("%s (bcfp)", other_label),
sprintf("%s (link)", other_label)),
colors = c(col_bcfp_only, col_link_only, col_both,
col_other_bcfp, col_other_link),
type = "categorical", position = "top-right") |>
mapgl::add_layers_control(collapsible = TRUE, position = "top-left")

Expand Down
18 changes: 9 additions & 9 deletions inst/extdata/configs/bcfishpass/dimensions.csv
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"species","spawn_lake","spawn_stream","spawn_stream_in_waterbody","rear_lake","rear_lake_only","rear_lake_area_only","rear_no_fw","rear_stream","rear_stream_in_waterbody","rear_wetland","rear_wetland_polygon","rear_wetland_area_only","rear_all_edges","river_skip_cw_min","rear_stream_order_bypass","spawn_requires_connected","spawn_connected_distance_max","spawn_connected_direction","spawn_connected_gradient_max","spawn_connected_cw_min","spawn_connected_edge_types","spawn_connected_lake_adjacent","rear_requires_connected","rear_connected_distance_max","notes"
"BT","no","yes","no","no","no","no","no","yes","no","no","no","no","yes","yes","no","",,"",,,,"",,,"bcfishpass: rear_all_edges=yes means no edge_type filter for rearing."
"CH","no","yes","no","no","no","no","no","yes","no","no","no","no","no","yes","no","",,"",,,,"",,,"bcfishpass: stream order bypass handled in compare script Step 7b."
"CM","no","yes","no","no","no","no","yes","no","no","no","no","no","no","yes","no","",,"",,,,"",,,"bcfishpass: no freshwater rearing."
"CO","no","yes","no","no","no","no","no","yes","yes","yes","no","no","no","yes","no","",,"",,,,"",,,"bcfishpass: wetland-flow carve-out (1050/1150 no thresholds); polygon=no excludes the W-polygon rule."
"PK","no","yes","no","no","no","no","yes","no","no","no","no","no","no","yes","no","",,"",,,,"",,,"bcfishpass: no freshwater rearing."
"SK","no","yes","no","yes","yes","no","no","no","no","no","no","no","no","yes","no","rearing",3000,"downstream",0.05,0,,"yes",,,"bcfishpass: lake-only rearing. Spawning within 3km ds of rearing lake."
"ST","no","yes","no","no","no","no","no","yes","yes","no","no","no","no","yes","no","",,"",,,,"",,,"bcfishpass: stream order bypass handled in compare script Step 7b."
"WCT","no","yes","no","no","no","no","no","yes","no","no","no","no","no","yes","no","",,"",,,,"",,,"bcfishpass: stream order bypass handled in compare script Step 7b."
"species","spawn_lake","spawn_stream","spawn_stream_in_waterbody","rear_lake","rear_lake_only","rear_lake_area_only","rear_no_fw","rear_stream","rear_stream_in_waterbody","rear_wetland","rear_wetland_polygon","rear_wetland_area_only","rear_all_edges","river_skip_cw_min","rear_stream_order_bypass","rear_stream_order_parent_min","rear_stream_order_child_min","rear_stream_order_child_max","rear_stream_order_distance_max","spawn_requires_connected","spawn_connected_distance_max","spawn_connected_direction","spawn_connected_gradient_max","spawn_connected_cw_min","spawn_connected_edge_types","spawn_connected_lake_adjacent","rear_requires_connected","rear_connected_distance_max","notes"
"BT","no","yes","no","no","no","no","no","yes","no","no","no","no","yes","yes","no","","","","","",,"",,,,"",,,"bcfishpass: rear_all_edges=yes means no edge_type filter for rearing."
"CH","no","yes","no","no","no","no","no","yes","no","no","no","no","no","yes","no","","","","","",,"",,,,"",,,"bcfishpass: stream order bypass handled in compare script Step 7b."
"CM","no","yes","no","no","no","no","yes","no","no","no","no","no","no","yes","no",,,,,"",,"",,,,"",,,"bcfishpass: no freshwater rearing."
"CO","no","yes","no","no","no","no","no","yes","yes","yes","no","no","no","yes","no","","","","","",,"",,,,"",,,"bcfishpass: wetland-flow carve-out (1050/1150 no thresholds); polygon=no excludes the W-polygon rule."
"PK","no","yes","no","no","no","no","yes","no","no","no","no","no","no","yes","no",,,,,"",,"",,,,"",,,"bcfishpass: no freshwater rearing."
"SK","no","yes","no","yes","yes","no","no","no","no","no","no","no","no","yes","no",,,,,"rearing",3000,"downstream",0.05,0,,"yes",,,"bcfishpass: lake-only rearing. Spawning within 3km ds of rearing lake."
"ST","no","yes","no","no","no","no","no","yes","yes","no","no","no","no","yes","no","","","","","",,"",,,,"",,,"bcfishpass: stream order bypass handled in compare script Step 7b."
"WCT","no","yes","no","no","no","no","no","yes","no","no","no","no","no","yes","no","","","","","",,"",,,,"",,,"bcfishpass: stream order bypass handled in compare script Step 7b."
Loading