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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ conn <- lnk_db_conn() # reads PG_DB_SHARE, PG_HOST_SHARE, etc.
- `lnk_score(conn, crossings, method)` — `method = "severity"` for biological impact classification (high/moderate/low). `method = "rank"` for weighted multi-criteria prioritization. Threshold-driven, NULL-safe, column-agnostic.

### Rules family
- `lnk_rules_build(csv, to, edge_types)` — transforms a species habitat dimensions CSV into the rules YAML format consumed by `frs_habitat()`. Two CSVs: NGE defaults (`parameters_habitat_dimensions.csv`) and bcfishpass comparison (`parameters_habitat_dimensions_bcfishpass.csv`).
- `lnk_rules_build(csv, to, edge_types)` — transforms a species habitat dimensions CSV into the rules YAML format consumed by `frs_habitat()`. Two CSVs: newgraph defaults (`inst/extdata/parameters_habitat_dimensions.csv`) and bcfishpass comparison variant (`inst/extdata/configs/bcfishpass/dimensions.csv`).

### Barrier overrides
- `lnk_barrier_overrides(conn, barriers, observations, habitat, params, to)` — processes fish observations and habitat confirmations into a barrier skip list for fresh. Counts observations upstream of each barrier via `fwa_upstream()` SQL, applies per-species thresholds, unions with habitat confirmations. Output: `(blue_line_key, downstream_route_measure, species_code)` table that fresh skips during access gating.
Expand Down
5 changes: 3 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: link
Title: Crossing Connectivity Interpretation
Version: 0.1.0
Version: 0.2.0
Authors@R:
person("Allan", "Irvine", , "airvine@newgraphenvironment.com",
role = c("aut", "cre"),
Expand All @@ -21,7 +21,8 @@ Depends:
Imports:
DBI,
RPostgres,
utils
utils,
yaml
Remotes:
NewGraphEnvironment/fresh
Suggests:
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Generated by roxygen2: do not edit by hand

S3method(print,lnk_config)
export(lnk_aggregate)
export(lnk_barrier_overrides)
export(lnk_config)
export(lnk_db_conn)
export(lnk_load)
export(lnk_match)
Expand Down
7 changes: 7 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# link 0.2.0

Config bundles for pipeline variants.

- Add `lnk_config(name_or_path)` — load a config bundle (rules YAML, dimensions CSV, parameters_fresh, overrides, pipeline knobs) as one list object. Bundles live at `inst/extdata/configs/<name>/` with a `config.yaml` manifest, or any directory containing `config.yaml` for custom variants ([#37](https://github.com/NewGraphEnvironment/link/issues/37))
- Relocate bcfishpass config files into `inst/extdata/configs/bcfishpass/` (rules.yaml, dimensions.csv, parameters_fresh.csv, overrides/). All R scripts and data-raw/ references updated.

# link 0.0.0.9000

Initial release. Crossing connectivity interpretation layer — scores,
Expand Down
6 changes: 3 additions & 3 deletions R/lnk_barrier_overrides.R
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
#' @param params Data frame with per-species parameters. Must have columns:
#' `species_code`, `observation_threshold`, `observation_date_min`,
#' `observation_buffer_m`, `observation_species`. See
#' `parameters_fresh_bcfishpass.csv` for format.
#' `configs/bcfishpass/parameters_fresh.csv` for format.
#' @param cols_index Character vector. Column names to index on the
#' barriers table for `fwa_upstream()` performance. Indexes are created
#' `IF NOT EXISTS`. Default `c("blue_line_key", "wscode_ltree",
Expand All @@ -45,8 +45,8 @@
#' \dontrun{
#' conn <- lnk_db_conn()
#'
#' params <- read.csv(system.file("extdata",
#' "parameters_fresh_bcfishpass.csv", package = "link"))
#' params <- read.csv(system.file("extdata", "configs", "bcfishpass",
#' "parameters_fresh.csv", package = "link"))
#'
#' lnk_barrier_overrides(conn,
#' barriers = "fresh.streams_breaks",
Expand Down
191 changes: 191 additions & 0 deletions R/lnk_config.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#' Load a Pipeline Config Bundle
#'
#' Reads a config bundle manifest (`config.yaml`) and returns a single
#' list object containing everything a pipeline needs to classify
#' habitat for a given interpretation variant — rules YAML, parameters,
#' overrides, observation exclusions, habitat confirmations, and
#' pipeline knobs (break order, cluster params, spawn_connected rules).
#'
#' A config bundle is a directory under `inst/extdata/configs/<name>/`
#' (for bundled variants) or an arbitrary directory path (for custom
#' variants) containing `config.yaml` plus the files the manifest
#' references. All file paths in the manifest are resolved relative to
#' the bundle directory.
#'
#' The returned list is the single object passed around the pipeline
#' (e.g. into `_targets.R`), so pipeline variants become a config
#' authoring exercise, not a code fork.
#'
#' @param name_or_path Character. Either a bundled config name
#' (`"bcfishpass"`, `"default"`) or an absolute path to a config
#' directory. Bundled names resolve to `system.file("extdata",
#' "configs", name, package = "link")`.
#'
#' @return An `lnk_config` S3 list with these slots:
#'
#' - `name` — config name (from `name_or_path` or the manifest)
#' - `dir` — absolute path to the config directory
#' - `rules_yaml` — absolute path to the rules YAML (consumed by
#' `fresh::frs_habitat_classify()`)
#' - `dimensions_csv` — absolute path to the dimensions CSV (source
#' of `rules_yaml` via `lnk_rules_build()`)
#' - `parameters_fresh` — data frame of per-species fresh overrides
#' - `habitat_classification` — data frame of expert-confirmed
#' habitat endpoints (or `NULL` if the manifest does not reference
#' one)
#' - `observation_exclusions` — data frame of observation IDs to
#' skip (or `NULL`)
#' - `wsg_species` — data frame of species per watershed group (or
#' `NULL`)
#' - `overrides` — named list of data frames, one per override CSV
#' listed in the manifest
#' - `pipeline` — named list of pipeline knobs from the manifest
#' (`break_order`, `cluster`, `spawn_connected`)
#'
#' @export
#'
#' @examples
#' # Load the bundled bcfishpass variant
#' cfg <- lnk_config("bcfishpass")
#'
#' # Inspect
#' cfg$name
#' cfg$dir
#' file.exists(cfg$rules_yaml)
#' head(cfg$parameters_fresh)
#' names(cfg$overrides)
#' cfg$pipeline$break_order
#'
#' \dontrun{
#' # Custom config: point at any directory containing config.yaml
#' my_cfg <- lnk_config("/path/to/my/variant")
#'
#' # Feed into the pipeline
#' fresh::frs_habitat_classify(conn, ...,
#' rules = cfg$rules_yaml,
#' params = cfg$parameters_fresh)
#' }
lnk_config <- function(name_or_path) {
if (!is.character(name_or_path) || length(name_or_path) != 1L) {
stop("name_or_path must be a single string", call. = FALSE)
}

dir <- .lnk_config_resolve_dir(name_or_path)

manifest_path <- file.path(dir, "config.yaml")
if (!file.exists(manifest_path)) {
stop("config.yaml not found in ", dir, call. = FALSE)
}
manifest <- yaml::read_yaml(manifest_path)

required_top <- c("name", "files")
missing_top <- setdiff(required_top, names(manifest))
if (length(missing_top) > 0L) {
stop("config.yaml missing required keys: ",
paste(missing_top, collapse = ", "), call. = FALSE)
}

files <- manifest$files %||% list()
required_files <- c("rules_yaml", "dimensions_csv", "parameters_fresh")
missing_files <- setdiff(required_files, names(files))
if (length(missing_files) > 0L) {
stop("config.yaml `files:` missing required entries: ",
paste(missing_files, collapse = ", "), call. = FALSE)
}

resolve <- function(rel) {
if (is.null(rel)) return(NULL)
file.path(dir, rel)
}

resolve_required <- function(key) {
p <- resolve(files[[key]])
if (!file.exists(p)) {
stop("config.yaml `files$", key, "` references missing file: ", p,
call. = FALSE)
}
p
}

read_csv_optional <- function(key) {
rel <- files[[key]]
if (is.null(rel)) return(NULL)
p <- resolve(rel)
if (!file.exists(p)) {
stop("config.yaml `files$", key, "` references missing file: ", p,
call. = FALSE)
}
utils::read.csv(p, stringsAsFactors = FALSE)
}

overrides <- lapply(manifest$overrides %||% list(), function(rel) {
p <- resolve(rel)
if (!file.exists(p)) {
stop("config.yaml `overrides:` references missing file: ", p,
call. = FALSE)
}
utils::read.csv(p, stringsAsFactors = FALSE)
})

out <- list(
name = manifest$name,
dir = dir,
rules_yaml = resolve_required("rules_yaml"),
dimensions_csv = resolve_required("dimensions_csv"),
parameters_fresh = utils::read.csv(resolve_required("parameters_fresh"),
stringsAsFactors = FALSE),
habitat_classification = read_csv_optional("habitat_classification"),
observation_exclusions = read_csv_optional("observation_exclusions"),
wsg_species = read_csv_optional("wsg_species"),
overrides = overrides,
pipeline = manifest$pipeline %||% list()
)
class(out) <- c("lnk_config", "list")
out
}

#' @export
print.lnk_config <- function(x, ...) {
cat("<lnk_config> ", x$name, "\n", sep = "")
cat(" dir: ", x$dir, "\n", sep = "")
cat(" rules: ", basename(x$rules_yaml), "\n", sep = "")
cat(" dimensions: ", basename(x$dimensions_csv), "\n", sep = "")
cat(" parameters_fresh: ", nrow(x$parameters_fresh), " rows\n", sep = "")
if (length(x$overrides) > 0L) {
cat(" overrides: ", paste(names(x$overrides), collapse = ", "),
"\n", sep = "")
}
if (length(x$pipeline) > 0L) {
cat(" pipeline: ", paste(names(x$pipeline), collapse = ", "),
"\n", sep = "")
}
invisible(x)
}

.lnk_config_resolve_dir <- function(name_or_path) {
# Heuristic: inputs containing a path separator are treated as
# filesystem paths; bare identifiers are looked up as bundled names
# first. Without this, a `bcfishpass/` directory in the current
# working directory would silently shadow the bundled config.
looks_like_path <- grepl("[/\\\\]", name_or_path)

if (looks_like_path) {
if (!dir.exists(name_or_path)) {
stop("No config directory found at path: ", name_or_path,
call. = FALSE)
}
return(normalizePath(name_or_path, mustWork = TRUE))
}

bundled <- system.file("extdata", "configs", name_or_path, package = "link")
if (nzchar(bundled) && dir.exists(bundled)) {
return(normalizePath(bundled, mustWork = TRUE))
}

stop("No config bundle found for name: ", name_or_path,
"\n Bundled configs are in: ",
system.file("extdata", "configs", package = "link"),
"\n To load a custom config, pass an absolute or relative path",
" (must contain '/').",
call. = FALSE)
}
7 changes: 4 additions & 3 deletions R/lnk_rules_build.R
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@
#' to = "inst/extdata/parameters_habitat_rules.yaml"
#' )
#'
#' # bcfishpass v0.5.0 comparison
#' # bcfishpass comparison variant
#' lnk_rules_build(
#' csv = system.file("extdata", "parameters_habitat_dimensions_bcfishpass.csv", package = "link"),
#' to = "inst/extdata/parameters_habitat_rules_bcfishpass.yaml",
#' csv = system.file("extdata", "configs", "bcfishpass", "dimensions.csv",
#' package = "link"),
#' to = "inst/extdata/configs/bcfishpass/rules.yaml",
#' edge_types = "explicit"
#' )
#' }
Expand Down
4 changes: 4 additions & 0 deletions R/utils.R
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Internal utilities — not exported
# These are the building blocks every lnk_* function uses.

#' Null-coalescing operator
#' @noRd
`%||%` <- function(x, y) if (is.null(x)) y else x # nolint: object_name_linter.

#' Execute SQL statement with error context
#' @noRd
.lnk_db_execute <- function(conn, sql) {
Expand Down
5 changes: 5 additions & 0 deletions _pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ template:
bootstrap: 5

reference:
- title: Configs
desc: Load a pipeline config bundle (rules, parameters, overrides)
contents:
- lnk_config

- title: Thresholds
desc: Configurable scoring defaults
contents:
Expand Down
Loading