Skip to content

frs_network_features: per-segment feature arrays for any FWA-snapped point dataset #201

@NewGraphEnvironment

Description

@NewGraphEnvironment

Problem

A recurring pattern across stream-network analysis: for each segment in a network table, aggregate the set of point features that lie at a particular relative position (downstream of, or upstream of, that segment) and surface them as a per-segment array. Examples:

  • Barriers downstream of habitat segments — does this stretch of stream have any access blockers between it and the river mouth? (bcfishpass.streams_dnstr_barriers)
  • Fish observations upstream of a barrier — has any observed species made it past this point? (bcfishpass.streams_upstr_observations)
  • Water-quality stations downstream of a sample site — what monitoring data integrates flow from this segment?
  • Sediment sample points along a project reach — which surveys overlap our area of interest?
  • Crossings, diversions, withdrawals, weather stations — any point feature snapped to FWA's (blue_line_key, downstream_route_measure, wscode_ltree, localcode_ltree) keying.

bcfishpass solves the barriers-and-observations slice via a Postgres UDF (bcfishpass.load_dnstr_chunked), but the SQL pattern is dataset-agnostic. fresh has the right home for the primitive — sibling to its existing frs_network_* family — but it doesn't exist yet, so consumers either reach into bcfp's UDF (DB-side, not in the R API) or hand-roll the SQL each time.

Proposed Solution

A new exported function in fresh:

frs_network_features(
  conn,
  segments,                              # schema-qualified segments table
  features,                              # schema-qualified features table
  segment_id_col = "id_segment",
  feature_id_col,                        # required (no default)
  direction,                             # c("downstream", "upstream"); required
  aoi = NULL,                            # forward-compat: WSG today, polygon/ltree later via .frs_resolve_aoi
  include_equivalents = FALSE
)

For each row of segments, return the array of feature_id_col values from features that lie in the requested direction relative to the segment. Empty (array_agg over zero matching rows returns NULL — propagate that, don't synthesize empty arrays).

The SQL pattern, paraphrased:

SELECT
  a.<segment_id_col>,
  array_agg(b.<feature_id_col> ORDER BY b.wscode_ltree DESC, b.localcode_ltree DESC, b.downstream_route_measure DESC)
    FILTER (WHERE b.<feature_id_col> IS NOT NULL) AS feature_ids
FROM <segments> a
LEFT JOIN <features> b ON
  whse_basemapping.fwa_<direction>(
    a.blue_line_key, a.downstream_route_measure, a.wscode_ltree, a.localcode_ltree,
    b.blue_line_key, b.downstream_route_measure, b.wscode_ltree, b.localcode_ltree,
    <include_equivalents>, 1
  )
WHERE <aoi-resolved-WHERE-on-a>
GROUP BY a.<segment_id_col>

The direction arg flips between whse_basemapping.fwa_downstream and whse_basemapping.fwa_upstream. Otherwise the SQL is structurally identical.

Naming + family fit

frs_network_features slots into the existing frs_network_* family (siblings: frs_network_downstream, frs_network_upstream, frs_network_segment, frs_network_prune). Sibling-not-replacement: those primitives are point→segments; this one is segments→features.

aoi matches frs_habitat's convention (WSG today, polygon/ltree later). MVP only validates WSG codes; polygon/ltree support arrives when .frs_resolve_aoi generalises.

Test plan

Test Bar
Synthetic 5-segment + 3-feature fixture, direction = "downstream" Per-segment array contents exact-match expected
Same fixture, direction = "upstream" Inverted relationship matches expected
Live parity: frs_network_features(bcfishpass.streams, bcfishpass.barriers_pscis, ..., direction = "downstream", aoi = "ADMS") Output array contents match bcfishpass.streams_dnstr_barriers.barriers_pscis_dnstr filtered to ADMS, mod array element ordering
Edge: empty features table Every segment row → NULL array
Edge: missing required column on either table Clear error before SQL runs
aoi = NULL Processes all segments in the table
include_equivalents = TRUE Same-position features count as relative-direction matches

Acceptance

  • Public exported function with roxygen + a working \dontrun{} example.
  • Synthetic-fixture tests pass.
  • ADMS live-parity test against bcfp tunnel matches byte-identical (mod sort).
  • Default direction not provided — caller must pass explicitly.
  • Documentation calls out the abstract framing: function works for any FWA-snapped point dataset, not specifically barriers.

Out of scope

The bcfp-shape composition streams_access (joins multiple frs_network_features calls across [source × species] into a wide table with per-species access integer codes) is the consumer's job, not this primitive's. Likely consumers:

  • link lnk_pipeline_access (link#124, in flight) — composes for bcfp parity.
  • Future water-quality / sediment / fish-survey roll-ups.

Each consumer composes the primitive; the primitive itself stays minimal and dataset-agnostic.

Related

  • bcfishpass.load_dnstr_chunked (bcfishpass/db/migrations/archive/v0.7.6/load_dnstr_chunked.sql) — the existing canonical SQL pattern this function ports to R.
  • Existing frs_network_downstream / frs_network_upstream — sibling primitives (point→segments).
  • link#124 — first consumer (bcfp accessibility-label parity).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions