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).
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:
bcfishpass.streams_dnstr_barriers)bcfishpass.streams_upstr_observations)(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 existingfrs_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:
For each row of
segments, return the array offeature_id_colvalues fromfeaturesthat lie in the requested direction relative to the segment. Empty (array_aggover zero matching rows returns NULL — propagate that, don't synthesize empty arrays).The SQL pattern, paraphrased:
The
directionarg flips betweenwhse_basemapping.fwa_downstreamandwhse_basemapping.fwa_upstream. Otherwise the SQL is structurally identical.Naming + family fit
frs_network_featuresslots into the existingfrs_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.aoimatchesfrs_habitat's convention (WSG today, polygon/ltree later). MVP only validates WSG codes; polygon/ltree support arrives when.frs_resolve_aoigeneralises.Test plan
direction = "downstream"direction = "upstream"frs_network_features(bcfishpass.streams, bcfishpass.barriers_pscis, ..., direction = "downstream", aoi = "ADMS")bcfishpass.streams_dnstr_barriers.barriers_pscis_dnstrfiltered to ADMS, mod array element orderingaoi = NULLinclude_equivalents = TRUEAcceptance
\dontrun{}example.directionnot provided — caller must pass explicitly.Out of scope
The bcfp-shape composition
streams_access(joins multiplefrs_network_featurescalls across [source × species] into a wide table with per-species access integer codes) is the consumer's job, not this primitive's. Likely consumers:lnk_pipeline_access(link#124, in flight) — composes for bcfp parity.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.frs_network_downstream/frs_network_upstream— sibling primitives (point→segments).