fresh 0.50.1
Closes #289. No change to the package’s functions.
The tie sweep sends its measures exactly. - data-raw/point_snap_tie_sweep.R sends each measure at 17 significant digits (.frs_sql_exact()), and in each state checks that the database stores what it was sent. It used one format() call over the whole frame, which rounded the CSV measures to 11 decimals. - Re-run against the same migrations, CSV fwa_locatealong() misses are 36 before the tiebreak migration and 31 after, and five measures now resolve. 0.44.0’s “four of 23,540 measures now resolve” was measured on the rounded measures. - The head case is now part of the sweep: of 400 blue lines, none resolve their own top measure before the migration and 383 after (sweep_tops.csv). research/fwa_point_snap.md carries the numbers.
fresh 0.50.0
Closes #274.
Route measures reach the SQL exactly. - frs_point_locate() writes its measure at 17 significant digits. It wrote 15, which for a stream’s top can name a point past the last segment’s end, and then fwa_locatealong() returned no row. - frs_network(), frs_network_upstream(), frs_network_downstream(), frs_network_prune() and frs_waterbody_network() find the segment a measure falls on (downstream_route_measure <= m) with the measure at full precision too, so a measure at a segment’s own start no longer risks picking the segment before. - These functions now stop with <arg> must be a single finite number when a measure is NA, infinite or not length 1. Before, the value reached the database and failed there. - frs_wsg_drainage() still writes outlet measures at 15 digits. fwa_downstream() gives the same answer at either precision, and the bundled outlet table has at most 10.
fresh 0.49.0
Closes #286. Relates to wet#155.
frs_lake_fetch(), frs_wetland_fetch() and frs_stream_fetch() return the same sf on every call to the same database, and can simplify and clip in PostGIS. - Each query ends on ORDER BY the table’s unique key (waterbody_poly_id, linear_feature_id). A new order_by names another table’s key, or NULL for none. limit = n now returns the n lowest keys, not whichever rows came first. - simplify_m simplifies in the database: ST_Normalize(ST_SimplifyPreserveTopology()) for lakes and wetlands, and ST_SimplifyPreserveTopology() alone for streams, because ST_Normalize() would reverse a line’s downstream-to-upstream direction. Both drop M; area_ha and length_metre stay the stored values. frs_watershed_at_measure(simplify_m =) simplifies a subbasin once, after the difference. - aoi takes a polygon, a watershed group code or a blue line key and measure, through the same resolver frs_extract() uses, and filters with ST_Intersects(). bbox stays envelope overlap. The new arguments come after limit, so positional calls keep working. - Simplified streams were byte-identical across two connections and across GEOS 3.9.0 and 3.10.2 (BULK and MORR, 52,291 segments). Across those GEOS versions 46 of 1,754 simplified BULK lakes differed by 1–2 vertices, so record postgis_full_version() with a saved result. research/fwa_fetch_determinism.md has the detail.
Numbers reach the SQL exactly. - frs_watershed_at_measure() wrote a round blue line key passed as a double as 3.56e+08, which matches no fwapg function. Keys now go in as integers, and measures at full precision, so a measure at a segment’s own start is no longer rounded below it and returned no watershed. aoi = list(blk, measure) shares the same builder. - An sf aoi’s WKT is written at 15 digits. It followed getOption("digits"), which at the default 7 rounds BC Albers x to the metre, so this also changes what frs_extract(aoi = <sf>) sends. - area_ha_min in the lake and wetland fetchers is validated and written exactly.
fresh 0.48.0
Closes #284. Relates to link#401.
frs_network_features(result = "distinct") returns, per segment, the feature IDs upstream or downstream once each, so a caller can ask which kinds of feature lie there. - With feature_id_col naming a kind (a species code), each segment gets the set of kinds, sorted by the ID in its type’s order, in the same list-column shape as result = "ids". It runs the array walk’s own join, predicate, aoi filter and scoped downstream path, aggregating array_agg(DISTINCT … ORDER BY …) with no ordered subquery. result = "ids" and "exists" send the same SQL as 0.47.0. - Over every watershed group’s fish records it costs less than an "ids" walk over one group’s own: LFRA 65 → 37 to 39 s, BULK 21 → 8 s, SETN 47 → 44 to 46 s (m1, twice each). One existence walk per species cost 3 to 7 times more. Evidence is in link’s data-raw/logs/obs_province_401/.
fresh 0.47.0
Closes #281. Relates to link#394.
frs_network_features(result = "exists") returns the segments that have any feature downstream (or upstream), without building the arrays. - The result is one row per segment that has at least one feature, with <segment_id_col> and a logical has_features that is always TRUE. Segments with none are left out, as with the arrays, so a caller testing id %in% result$id gets the same answer by adding one argument. A segment whose only features have NULL ids is a row in both forms. - It runs the array walk’s own join on the same predicate, aoi filter and #272 scoped downstream path, and reduces it to SELECT DISTINCT segment ids. Nothing is aggregated into an array and the walk has no ORDER BY. result = "ids", the default, sends the same SQL as 0.46.0. - On link’s natural barrier views it is 1.00× (SALR) to 1.24× (SETN) faster end to end. The saving in seconds grows with the number of segments reached: in SETN, CH takes 41 s rather than 50 s. Most of the time is the join, which both forms run. A correlated EXISTS was 1.1× to 15× slower than the join, because it has to drive from the segments. Logs are in data-raw/logs/network_features_exists_281/, from data-raw/network_features_exists.R. research/fwa_downstream_walk.md, “Existence walks”, has the plans.
fresh 0.46.0
The connectivity passes no longer read or write other watershed groups’ rows in a shared habitat table. - frs_habitat(to_habitat = ) writes every watershed-group job into one habitat table, and the passes that run after classification joined it on id_segment alone. id_segment is unique only within a group — each working table numbers its segments from max(id_segment) + row_number() — so one group’s pass read and cleared another group’s rows. Every habitat join in frs_cluster() (all three directions), .frs_distance_filter() and .frs_connected_waterbody() now matches watershed_group_code as well, and every UPDATE is confined to the groups present in table. .frs_bucket_connected() already did (#240). - The worst of them cleared whole groups with no id collision involved. .frs_connected_waterbody()’s subtractive step named no group at all: it set spawning = FALSE for the species wherever id_segment was not among the ids this job qualified, so every group already in the table lost its spawning. With the shipped parameters SK and KO reach this path, which made it the live one. - frs_cluster(direction = "both") was also returning wrong answers for the group it was processing. Its downstream trace joined habitat with a LEFT JOIN on id_segment, so a colliding row elsewhere in the table duplicated the traced segment and the running sum of length_metre counted it twice. A cluster inside bridge_distance could therefore read as beyond it. The other group’s rows needed no flags set at all for this. - frs_cluster()’s table and habitat must now carry watershed_group_code. Anything reaching it already needs wscode_ltree, localcode_ltree, blue_line_key, watershed_key, downstream_route_measure, gradient, length_metre and linear_feature_id, so in practice this is a table from frs_extract() or frs_network_segment(), which carry it. With species = NULL it now resolves only the species present in table’s groups rather than every species in the table. - On-spawning protection in frs_cluster(direction = "both") is now the predicate label_connect IS NOT TRUE on the UPDATE rather than an id_segment NOT IN (…) list read from the whole habitat table, where another group’s row protected this group’s segment. It assumes (id_segment, watershed_group_code, species_code) is unique in the habitat table; no index enforces that. - The progress lines now report the job rather than the whole table. The before/after counts are aggregates with no join to carry the group, so they take a shared clause instead; a serial multi-watershed-group run was printing another group’s numbers as this job’s. Eight sites across five functions, frs_habitat_classify()’s included — it writes the shared table rather than reading it, which is why the first pass at this missed it. - The published examples no longer teach the join this release fixes. frs_habitat_classify()’s example built a view joining fresh.streams to fresh.streams_habitat on id_segment alone, five lines above its own note that both tables accumulate across groups — the defect #218 measured, shipped as the recommended recipe. With it, three example count(*) queries that totalled a shared habitat table as though it were one job’s result, and two prose claims that id_segment links the two tables. frs_feature_index()’s output carries no watershed_group_code at all, so its example cannot join on both; it now states the constraint, and widening that schema is part of #277. - Each pass has a test in which a second group’s rows share id_segment values with the first: it asserts that those rows are untouched, that they neither rescue nor condemn the first group’s, and that the pass still keeps and strips what it should. Every one of the 19 predicates was then restored, one at a time, against a baseline confirmed green and skip-free: all 19 are caught by a named test.
fresh 0.45.0
Closes #272. Relates to #265 and link#261.
frs_network_features(direction = "downstream", aoi = ) walks only the features the AOI can reach. - A feature can be downstream of a segment only when it is on the segment’s blue line, or when its watershed code is an ancestor of, or equal to, the segment’s. These are fwa_downstream()’s two criteria. The function now copies the AOI’s segments into an indexed, analyzed session temp table, admits only the features that meet either criterion against it, and walks those. The answer is unchanged: every walk measured returned the same features per segment. - Before, the join order depended on the tables. Against a large feature table the planner could probe the AOI’s segments once for every feature in the table, and with a province-wide segments table it could leave the segment-driven plan altogether. With fresh_default.streams and link’s barrier views, the v0.44.0 SQL did not finish in 10 minutes on any CH or BT walk in SALR, LKEL, CHWK or LSKE. The new form took 0.34 to 8.4 s (mean of two runs), temp table included (0.07 to 0.7 s). The anthropogenic view went from 0.85-4.5 s to 0.17-1.7 s. Logs are in data-raw/logs/network_features_272/, from data-raw/network_features_prefilter.R. link#261 measured these walks as most of link’s recompute. - New session requirement. A scoped downstream call needs one backend and a session that can create temp tables, as frs_point_snap() does. In a read-only transaction or on a hot standby it now stops with an error naming the cause. Under transaction pooling the call can fail, with the same error while building the table or at the walk, when a statement reaches a different backend from the one holding the temp table. aoi = NULL and direction = "upstream" are unchanged and create nothing. - Each segment’s feature_ids now ends its ORDER BY on the feature id in both directions. Ties are therefore ordered the same way on every run, where before they followed the plan. Within a tie the order can differ from bcfishpass.load_dnstr’s. - research/fwa_downstream_walk.md, “Prefiltering the feature side”, has the planner mechanism, the alternative that was rejected, and what is still open (upstream walks; aoi = NULL on a table already cut to one group).
fresh 0.44.0
Closes #266. Relates to fwapg#41 and link#383.
frs_point_snap() breaks an exact distance tie on position, not on the lowest segment id. - A tie is a point whose nearest point on two segments is the vertex they share — common, since any point in the wedge outside a bend is one. It now goes to the segment with the higher downstream_route_measure: the one starting at that vertex, which is the one a measure there falls on, because measures resolve [downstream, upstream). linear_feature_id breaks what is still tied. This is fwapg’s rule, so frs_point_snap() and fwa_indexpoint() now agree on which segment a tie belongs to, and therefore on its local code — which differs between the two whenever a tributary joins at the vertex. - Three ordering sites carry it, not the two the issue named. The lateral’s own ORDER BY ... LIMIT is the load-bearing one: it decides which candidates survive, and is the only one that matters at num_features = 1. - The key is the raw measure, carried out of the lateral as an internal measure_raw. The emitted measure is clamped to a whole metre, and two segments sharing a vertex on a whole metre clamp to the same value, which would fall through to linear_feature_id and reinstate the old rule. No new column reaches the caller, and EXPLAIN confirms the GiST KNN index-ordered scan survives the extra key. - Within one stream the rule is positional; a tie across two blue lines is only deterministic. Of 3,463 ties counted over 49,725 snapped positions, 440 are within one stream.
frs_migration_trial() applies pending migrations inside a transaction it rolls back. - frs_migration_trial(conn, files, env) applies the files in one transaction and defers DBI::dbRollback() to env’s exit, so a pending fwapg migration can be measured, or tests run against it, without migrating a database anyone else uses. A migration’s own BEGIN;/COMMIT; pair is stripped with line numbers preserved, and any other transaction control — or any call a rollback does not undo — is refused before anything runs. - The screen is a small SQL lexer: comments, string literals, quoted identifiers and dollar-quoted bodies are blanked to spaces with line breaks kept, so transaction control is looked for only where the server would execute it. A plpgsql body’s own BEGIN / END; is therefore not mistaken for one, which matters because fwapg’s db/schema.sql writes bare END; seven times. An unterminated region is a refusal rather than a guess. - env = globalenv() is refused: there the rollback would be deferred to the end of the session, holding the migration’s AccessExclusiveLocks unannounced. withr moves from Suggests to Imports.
What fwapg#41 changes on fresh, measured before and after. - data-raw/point_snap_tie_sweep.R runs fresh’s inputs through three states — base, the tiebreak migration alone, and both pending migrations — with each trial rolled back. Logs in data-raw/logs/indexpoint_266/; the report’s body is reproducible, its header is not. No database is migrated: that waits on fwapg#41 / smnorris/fwapg#201. - All 49,725 of fresh’s own snapped positions resolve in every state. fresh clamps its measure to a whole metre and no candidate segment has an integer upstream_route_measure, so the head case the migration fixes is not reachable through frs_point_snap() → frs_point_locate(). It is reachable when a caller passes a segment’s own upstream measure. - What does move: four of 23,540 measures from crossings.csv and wsg_outlet.csv now resolve (named exactly in sweep_changed.csv), and one position stops raising line_interpolate_point: 2nd arg isn't within [0,1]. Every change belongs to the tiebreak migration. What does not move: 293 of 970 positions error identically in all three states with more than one row returned by a subquery used as an expression, a pre-existing fwapg defect. - research/fwa_point_snap.md carries the numbers and the reasoning, including why a measure at a stream’s head could never be placed: the old fwa_locatealong() compares bounds rounded to 4 decimals, so round(us, 4) > round(us, 4) is false by construction.
Live tests that gate on the database without hiding a stale one. - tests/testthat/test-frs_fwapg_gate.R pins both changes. They skip, naming the migration, on a database that predates it; FRESH_FWAPG_TRIAL trials it so they run anyway; and FRESH_FWAPG_CURRENT=true says the database should already be current, in which case a stale one fails instead of skipping — including one test that reads committed state on its own direct connection, so neither an unreachable database nor a rolled-back trial can turn the suite green. - tests/testthat/helper-db.R gains frs_fwapg_conn() on FRESH_PG* with the docker/ defaults. These tests must not reach the shared instance PG* names. See CLAUDE.md, “Testing on alternate hosts”.
fresh 0.43.0
Closes #269. Relates to link#326.
frs_order_child() can apply bcfishpass’s width and gradient tests on its order-1 rearing exception. - Two new arguments, both NULL by default, so existing calls are unchanged. channel_width_max admits only a direct child whose width is known and at most the value. gradient_max admits only one whose gradient is at most the value. - bcfishpass’s exception sits inside the rest of its rearing test (smnorris/bcfishpass@7f444f1b, load_habitat_linear_bt.sql:88-97). A line with no width never qualifies, and neither does one steeper than the rearing maximum. Passed the species’ rearing width minimum and gradient maximum, the function applies both tests. - The docs list what the function still leaves to the caller or does differently: the group’s model (cw only), spawning and connectivity, bcfishpass’s waterbody and edge-type filters for CH, CO, ST and WCT (three forms), and the stream_order_max direct-child test. - A rules YAML channel_width_min_bypass block accepts width_known_below_min: true|false. It is validated as a logical scalar, and callers read it to pass the two bounds.
fresh 0.42.0
A river nearby is measured by discharge, channel width or stream order. Breaking change to frs_near_river(). - frs_near_river(conn, table, to, threshold, distance_max, measure = "discharge") reads mad_m3s, channel_width or stream_order. A segment smaller than the threshold by that measure is an origin, and one at least that size is a target. The rest of the walk is unchanged. - The output table records measure and threshold in place of discharge_min. A table written by 0.41.0 stops the call with a message to drop it. - A rule’s mapping takes exactly one of discharge_min, channel_width_min or stream_order_min, with distance_max. The predicate matches the measure, the threshold and the distance. - The measures see different rivers. fwapg’s discharge is missing on some main stems (none of BULK’s 1,118 river-polygon rows carries one), while channel width is modelled on every river-polygon row. Most lake flow lines carry an order but no width. - On BULK, each walk by width (20 m) and by order (5) equals, row for row, the discharge walk on a copy of the table whose mad_m3s holds that measure (data-raw/near_river_measure_check.R, logs in data-raw/logs/near_river_262/). The independent reference that checked the walk in 0.41.0 reads discharge only, so it does not score these. Each walk takes 13 to 16 s at 1 km on BULK’s 32,472 segments.
The near-river pair is a threshold, inherited like gradient, width and discharge. - parameters_habitat_thresholds.csv can carry ten columns: per stage, <stage>_river_measure (discharge, channel_width, stream_order, or model, the default, which reads width under cw and discharge under mad), the threshold of each measure, and <stage>_river_distance_max. frs_params() stores the pair as params[[sp]]$near_river, beside ranges. Half a pair or a bad value stops, and an unknown *_river_* column warns. - A spawn or rear rule that inherits thresholds and has a size test takes its stage’s pair. Its own mapping overrides the pair, and near_river: false opts it out. A rule with no size test takes no pair, because the pair would become its only size test. waterbody_type: R rules inherit like stream rules. - The compiler and the walk’s list of pairs use one resolver, so a pair is walked exactly when a rule reads it. frs_habitat_classify() collects pairs only from species with a params_fresh row, and builds predicates only for the size models its groups use. It now walks before it writes the habitat table, so a walk that fails leaves the group’s habitat rows in place. Its checks of the walk’s inputs, including each measure’s column and type, run before anything is written. - With the columns absent or NA, every rule compiles exactly as in 0.41.0. A test checks this against 0.41.0’s own output for fresh’s bundled rules and two of link’s bundles. The bundled CSV carries the columns as NA.
fresh 0.41.0
Closes #259.
A habitat rule can admit a small stream near a large river (near_river). - A spawn or rear rule can carry near_river: {discharge_min, distance_max}. A segment that fails the rule’s size test (channel width under cw, mad_m3s under mad) then passes when a segment carrying at least discharge_min m³/s lies within distance_max m downstream. The rule’s edge-type, gradient and waterbody tests still apply, and rules without the key compile exactly as before. See frs_habitat_predicates(). - New frs_near_river() measures the distance. frs_habitat_classify() runs it into <to>_near_river when any rule asks, one walk for each distinct pair of discharge and distance, so no rule’s answer depends on another’s, and keeps that table. frs_habitat_predicates() gains near_river_table. The walk reads mad_m3s, so a near_river rule needs it on the table under either model. - The distance runs from the segment’s downstream end to the river’s bank. A tributary’s connector across the river’s polygon is left out, and so is the jump from its mouth where FWA draws the connector as a side line. Any other gap in the path, such as a stretch of river outside the table, counts at its straight length. A row is walked only below the point where the walk enters it. - Only segments below a discharge are measured at it, so a river that large passes or fails on its own size test. - Where a tributary joins its parent follows FWA’s local codes, as in every downstream trace in fresh, so where a code places a confluence away from where the lines meet, the distance can differ from a nearest-point walk either way. - Two consequences to know. A thresholds: false rule with near_river has no gradient test unless it carries gradient:. Under mad, a species with no MAD range now qualifies through near_river where a river is near. - Limits: the walk sees only the table, one watershed group, so a river beyond the group’s outlet is not found. A segment with no local watershed code never walks. That covers most side channels: 238 of 358 edge-1100 rows in BULK, and 50,280 of 83,642 province-wide. - Checked on BULK at 3 m³/s against an independent route-measure walk on raw FWA (data-raw/near_river_check.R). Of 2,462 sampled segments, every residual falls in a named class: the bank rule, past the 20 km reach, where the two walks enter a parent line, and a main-stem row whose local code nests under a tributary’s. None is unexplained. The walk takes 16 s at 1 km and 73 s at 20 km on BULK’s 32,472 segments.
citation("fresh") cites the Zenodo concept DOI. Config/DOI in DESCRIPTION holds it, and it resolves to every archived release; CITATION.cff carries it too.
fresh 0.40.1
No change to the package’s functions. This release exists so the package has a citable, archived version.
-
Citation. The maintainer is the author, and New Graph Environment Ltd. the copyright holder and funder (
cph,fndinAuthors@R) and the publisher inCITATION.cff.citation("fresh")andknitr::write_bib()give the entry the stable keyfresh. -
CITATION.cffis regenerated from DESCRIPTION by theupdate-citation-cffworkflow and is kept out of the build tarball. - The build excludes a git worktree’s
.gitfile.
fresh 0.40.0
Closes #247.
frs_point_snap() snaps a whole dataset in one query and returns watershed_group_code. Breaking change. - It takes points: a data frame with coordinate columns (col_x, col_y, srid), an sf POINT object, or a schema-qualified table (col_geom, with col_id required). 1,000 points are one snap query, not 1,000. - It returns the input id (col_id, or id_point for the row number) with blue_line_key, downstream_route_measure, watershed_group_code, linear_feature_id, wscode_ltree, localcode_ltree, gnis_name and distance_to_stream, plus candidate_rank when num_features > 1. to = writes the result to a table, ready for frs_candidates_pick(). - col_blk names a per-row blue_line_key hint column. A point with no hint snaps to any stream. - The default tolerance is now 100 m (was 5000 m), the distance bcfishobs accepts for A/B matches. - Every snap now uses the lateral nearest-neighbour search that link::lnk_points_snap() uses, and the fwa_indexpoint() path is gone. The measure is a whole metre, as in bcfishpass: rounded down, but never below the segment’s start rounded up. Placeholder (999.*) and unmapped segments are never candidates, and subsurface-flow (1425) segments are excluded by default (exclude_edge_types). Unlike fwa_indexpoint(), edge type 6010 (connectors where a stream leaves and re-enters a watershed group) is a candidate. - To migrate, frs_point_snap(conn, x = lon, y = lat) becomes frs_point_snap(conn, data.frame(x = lon, y = lat)); add tolerance = 5000 to keep the old reach. The old call errors with this hint. - Parity with lnk_points_snap() on all 19,905 PSCIS crossings at 150 m: 18,180 identical picks, 22 equidistant ties (same stream, at most 1 m apart), 11 where link picked a 999.* placeholder that fresh excludes, and no other differences (data-raw/point_snap_parity_check.R).
frs_feature_find(points = <sf>) works. - It always failed, because it passed the sf to the single-point snap. It now snaps through frs_point_snap(), and col_id, label, label_col, label_map, append and the stream scoping to table apply as they do for points_table. - Both paths write feature_id as text. A label_map now works on an integer or logical label_col (map logicals with "TRUE" / "FALSE" or "t" / "f" keys). A call that fails its checks or the snap no longer drops an existing to.
frs_watershed_split() snaps its points in one query. Its measures are now whole metres. It no longer snaps to subsurface-flow, placeholder or unmapped segments, and it can now snap to edge-type 6010 connectors.
fresh 0.39.0
New frs_channel_width() gives every segment a channel width, first-order streams included. - It writes a width predicted from upstream area and mean annual precipitation, which are joined first with frs_col_join(). - Models: "poisson2021" (the model behind fwapg’s MODELLED widths), "hall2007" (the VCA bankfull regression flooded uses), or a custom power law. - By default it fills only NULL widths. Point to at a new column for an independent estimate instead. - value assigns a constant, labelled ASSIGNED, to rows with no inputs. - It is not wired into frs_network_segment() / frs_habitat(), so bcfishpass parity is unchanged. - Why first-order streams had no width: fwapg’s regression covers them, and one stream_order > 1 filter drops them. Matching fwapg’s stored MODELLED values depends on taking the largest upstream area per watershed-code pair; the docs give the join. See research/fwapg_channel_width.md. Placeholder and unmapped segments have no inputs (#246).
frs_break() works again. - It errored on every call: it passed point-break arguments that frs_break_find() dropped when point breaks moved to frs_feature_find(). frs_break() now does attribute breaks only. - frs_habitat_access() failed whenever break_sources was given. It now locates them with frs_feature_find().
Habitat-pipeline vignette now includes gradient barriers. - Its example code, and the scripts that build its data, had the same stale call and skipped the watershed codes frs_classify(breaks =) needs. They now go through frs_habitat_access(). - The cached data had falls-only access. Regenerated: access barriers 4 to 278, accessible 638.0 to 499.0 km of 964.7 km.
Documentation. - The gate direction is corrected: segments upstream of a blocking break are inaccessible. - frs_habitat_predicates() docs now match its input, and its example runs without a database. - Previously undocumented arguments are documented. R CMD check is clean.
fresh 0.38.0
Closes #237.
A wetland rule’s wetland_ha_min now gates rearing, not only wetland_rearing. - The compiler for the main spawn / rear predicates read only lake_ha_min, so a waterbody_type: W rule admitted wetlands of every size. The floor now comes from the key of the rule’s type (lake_ha_min on L, wetland_ha_min on W) through one internal reader. The lake / wetland bucket predicates and the waterbody-connected spawning pass use the same reader. - Rearing moves for every bundle with a wetland floor: fresh’s bundled rules and link’s default* (BT, CH, CO, RB, ST, WCT). The bcfishpass bundle has no W rules and is unchanged. On link’s fresh_default, persisted rearing loses at least 40 segments (2.1 km) for NATR BT, 60 (3.1 km) for PARS BT and 2 for BULK CO. It is “at least” because cluster_rearing reruns on the narrower set. The change is small because the stream rule already admits most stream edges in wetlands (data-raw/logs/rear_wetland_floor_237/). - The floor holds per rule, not per predicate. Other rear rules still admit segments in smaller wetlands, chiefly the edge_types_explicit: [1050, 1150], thresholds: false wetland-flow rule. For NATR BT, about 307 segments (26.4 km) of rearing sit on wetland-flow lines in wetlands under 1 ha. That is an upper bound until cluster_rearing reruns. See link#311. - The rules loader now rejects an empty, null, NA or NaN lake_ha_min or wetland_ha_min. Before, an empty value meant no floor and NA compiled to invalid SQL. - For waterbody-connected spawning on a species whose first lake / wetland rear rule is W, the floor is now its wetland_ha_min rather than the 200 ha default. No bundled species takes that path.
fresh 0.37.0
Closes #240.
Lake and wetland rearing buckets are sized by polygon area, and can require connected spawning. - lake_rearing / wetland_rearing test polygon membership and the rule’s lake_ha_min / wetland_ha_min only, under both cw and mad. There is no channel-width or discharge test on the line through the polygon, which sized the inflow rather than the polygon, and no cw fallback to FALSE for a species with no rear width range. Every bundle’s buckets change on upgrade. On NATR, BT lake bucket km go from 310 to 521 and wetland km from 684 to 1,287 (data-raw/logs/bucket_connected_240/). - A species’ first rear waterbody_type: L / W rule may carry requires_connected: spawning and connected_distance_max (m). After classification, a polygon then keeps the flag only where same-species spawning is on one of its lines, downstream of an outlet, or upstream of it within that distance. The polygon is the unit, and rearing is unaffected. See frs_habitat(), “Lake and wetland rearing”. - On a rear rule, requires_connected now errors at rules load unless it is on the first L / W rule, set to spawning, with a finite distance > 0. Before, it was accepted and read by nothing. - The connectivity pass creates a GiST index on the working table’s geom if one is missing and ANALYZEs, which keeps it to a few seconds per species per watershed group.
fresh 0.36.3
- README: wet joins the ecosystem table as an open source of the mean annual discharge the
madhabitat rules read. Documentation only.
fresh 0.36.2
Closes #229.
- Live integration tests now skip when the database can’t be reached, instead of erroring. A new test helper,
skip_if_no_conn()intests/testthat/helper-db.R, attempts a real connection and skips with the error text on failure.test-frs_network_features-live.Ruses it in place of checking that a credential env var is set. Test-only change.
fresh 0.36.1
Closes #223.
- The bundled-rules test (
frs_params bundled rules has expected species blocks) now checks rule content instead of rule count and position. It had failed since v0.34.0, when link’slnk_rules_build()added a CO lake rearing rule. It now finds thethresholds: falsecarve-out by predicate and checks R/W/L waterbody coverage, so future rule resyncs won’t break it.
fresh 0.36.0
Closes #213.
frs_db_conn() reads the standard libpq env vars. Connection parameters now resolve as explicit args → PGHOST / PGPORT / PGDATABASE / PGUSER / PGPASSWORD (and anything else libpq reads, such as PGSERVICE or ~/.pgpass) → legacy PG_*_SHARE → libpq defaults. Arguments default to NULL, and it no longer stop()s when variables are unset. - PG_*_SHARE is a deprecated fallback. It is used only when none of the standard variables are set, and shows a message once per session. - The two groups are never mixed, so a PG_DB_SHARE name is never paired with a PGHOST host.
Behaviour change. On a machine that sets both groups, a bare frs_db_conn() now connects to the PG* target instead of the PG_*_SHARE one. If PG* points at a local fwapg and PG_*_SHARE at the bcfishpass tunnel, code reading bcfishpass.* through the default connection now errors with “relation does not exist”. That includes frs_params(conn) and some data-raw/ scripts. Point PG* at the tunnel or pass arguments explicitly. - frs_habitat() parallel workers inherit password = "" and let libpq read the worker’s PGPASSWORD. Pass password when connecting through the legacy fallback.
The test suite gates DB tests on .frs_db_available() and skips bcfishpass.* tests when that schema is absent.
fresh 0.35.0
Closes #220.
Per-watershed-group channel width vs MAD model. frs_habitat() and frs_habitat_classify() take params_method, the bcfishpass parameters_habitat_method.csv table (watershed_group_code, model). Groups set to mad classify on mad_m3s using the CSV *_mad_min/*_mad_max thresholds in place of channel width. This covers the CSV-ranges path, rules-path inheritance, and lake/wetland rearing. - frs_habitat_predicates() gains model = "cw" | "mad". - The bundled table is all cw, so default outputs are unchanged. Groups missing from params_method use cw. - A table spanning both models is classified per row on watershed_group_code. - A mad group on a table without mad_m3s stops before any writes.
Under mad, fresh follows bcfishpass: - Species with no MAD thresholds (BT, GR, KO, RB) get no stream habitat from rules that inherit thresholds. - Rule-level channel_width (the river-polygon bypass) is ignored. - SK/KO lake rearing is based on polygon membership.
Divergences from bcfishpass: - The minimum is inclusive (BETWEEN) where bcfishpass uses a strict >. - fresh does not implement bcfishpass’s stream_order >= 8 spawning bypass. - frs_habitat_partition() / frs_habitat_species() remain cw-only.
Fix: infinite thresholds. A blank *_max threshold (Inf from frs_params()) now renders as valid SQL. Previously it produced a bare Inf token.
fresh 0.34.0
Closes #114.
Streams carry mad_m3s. frs_network_segment() and frs_habitat() now join mad_m3s (mean annual discharge) from whse_basemapping.fwa_stream_networks_discharge, next to channel_width.
New mad rule predicate. The rules YAML accepts mad: [min, max], which compiles to s.mad_m3s BETWEEN min AND max. The validator rejects an empty value, non-numeric entries, the wrong number of values, non-finite bounds, and a reversed range. - MAD applies only where a rule asks for it. CSV *_mad_min/*_mad_max thresholds are still parsed but never applied or inherited, because the choice between channel width and MAD is made per watershed group. The bundled method table sets every group to cw, so existing outputs are unchanged. - The per-group switch is #220. - Segments with no modelled discharge (NULL mad_m3s) fail a mad rule. Coverage is uneven: the local fwapg has discharge for 123 groups, and BULK and LDEN are absent entirely.
Fix: saving to_streams in frs_habitat(). It now inserts by column name, and adds any columns missing from a table persisted by an older run. Previously the save was a positional INSERT ... SELECT * that ran after the per-group DELETE. On an older table, the new column would have made the insert fail and lost that group’s rows. A type mismatch on a shared column now stops the run before anything is deleted.
fresh 0.33.0
Closes #214. Rebuilds frs_wsg_drainage() on a per-group outlet point and the measure-aware whse_basemapping.fwa_downstream(), replacing the single-ltree ancestry test introduced in #211. That test was wrong, and the fact that its backing table — public.wsg_outlet, materialized ad-hoc and never committed (link#227) — kept going missing was masking it. A lone ltree cannot order two groups sharing a stem: all 14 Fraser groups carry wscode 100 and five Skeena groups carry 400, so frs_wsg_drainage(conn, "UFRA") silently omitted LFRA, the group the entire basin drains through. Province-wide, 62 of 246 groups resolved to an outlet shallower than their own polygon code, only 167 distinct outlets existed for 246 groups, and equal outlets fell through to an alphabetical wsg ASC tiebreak — which is a correctness hazard, not just wasted work, because downstream-first ordering is what makes cross-WSG barrier state settle in a single pass.
Outlets now come from fwa_stream_networks_sp, selected as max stream order first, then shallowest wscode_ltree, then lowest measure. The order filter is what removes the polygon slivers that defeated the old derivation: MORR (Morice) clips a 2-segment, 0 km, order-1 piece of the Bulkley-coded line 400.431358 alongside 1,236 segments and 275 km of the order-8 Morice line, and selecting on ltree depth alone picked the sliver — making the Bulkley appear to drain through the Morice, exactly inverting the hydrology. FWA carried the “Bulkley” name down through the confluence, so the larger flow is filed as the tributary; the fixtures now pin that case by name.
The 246-row table ships as inst/extdata/wsg_outlet.csv with a documented generator at data-raw/wsg_outlet.R, and is read by the new exported frs_wsg_outlets(). Outlets are passed to the database as a VALUES list, so no table is required in the target database — only the fwa_downstream() routine. That removes the public.wsg_outlet dependency outright rather than repairing it. Ordering is now derived from topology (count of groups downstream) instead of ltree depth. Validated on 12 hydrology fixtures plus province-wide structural invariants: 0 cycles, 0 self-edges, 0 transitivity violations across 3,537 checks, 62 coastal terminal groups, and every non-terminal group reaching one.
Breaking: the table argument is replaced by outlets, a data frame. Closures get tighter and more correct — frs_wsg_drainage(conn, c("PARS","BULK")) returns 9 groups where it previously returned 15, dropping LKEL (a separate tributary), MSKE/USKE (upstream of the confluence), LBTN (a sibling) and MORR (upstream).
fresh 0.32.0
Closes #211. Adds frs_wsg_drainage() — a pure FWA-topology primitive that returns the drainage closure of a focal set of watershed groups (focal + every WSG they flow through), ordered downstream-first by outlet ltree depth.
- Promotes the inline closure query from
NewGraphEnvironment/link@v0.40.5data-raw/study_area_wsgs.Rinto a reusable, tested function. Pure FWA topology — no species / bundle / overrides knowledge — so other fresh consumers (vignettes, ad-hoc R sessions, future provincial drivers) can use it without going through link. - Closure predicate
f.outlet <@ w.outletagainstpublic.wsg_outlet(ltree-based FWA WSG outlet table). Ordernlevel(outlet) ASC, wsg ASC— most downstream WSGs first, alphabetical within a depth. Running per-WSG work in this order persists downstream barriers before upstream WSGs read them, which is what link’s study-area runner relies on for cross-WSG access flags to settle within a single pass. - Signature
frs_wsg_drainage(conn, watershed_group_code, table = "public.wsg_outlet")follows fresh vocabulary (watershed_group_code, matchesfrs_wsg_species+frs_stream_fetch). Table identifier validated via.frs_validate_identifier; focal codes upper-cased internally and quoted viaDBI::dbQuoteLiteral(no injection surface). Unmatched focal codes warn rather than silently drop; all-unmatched errors. - First consumer:
link::lnk_wsg_resolve(NewGraphEnvironment/link#207) — the link wrapper composes this primitive with the bundle’s species-presence filter (#157) to drivedata-raw/study_area_wsgs.R(and replaces the inline query there). - New
@family wsgfamily.frs_wsg_speciesretag deferred to a separate concern. - 11 tests / 14 expectations: 6 arg-validation (no DB), 5 live-DB (gated on
PG_DB_SHARE) covering the 15-WSG PARS+BULK regression baseline, focal-order invariance, case-folding, unmatched-focal warning, and all-unmatched error. Live-validated against fwapg.
fresh 0.31.0
Closes #207. Adds frs_candidates_pick() — fourth primitive in the point-handling family (alongside frs_point_snap, frs_network_features, frs_point_match). Given a candidates table where multiple rows can share the same key value, optionally compute a per-row score from a caller-supplied SQL expression, optionally filter via a caller-supplied WHERE clause, then keep one row per key via DISTINCT ON (col_key) ORDER BY ....
- Generic over any “score + filter + dedup per key” workflow where column-to-column comparisons disambiguate matches: stream-name matching, watershed-group agreement, species-code overlap, assessment-date proximity, channel-width × stream-order compatibility, etc.
- Closes the BULK 5-diff gap from fresh#206 at the dedup-step level. Live validation on BULK PSCIS-to-stream selection (using bcfp’s pre-computed
pscis_streams_150mas the scored-candidates input): 102 / 102 ref picks byte-identical, 0 missing. The 4 “extras” are bcfp’s downstreamsuspect_matchrouting filter that lives caller-side, not in this primitive. - Parameter naming follows the
table_<role>/col_<role>/exp_<role>conventions (link/CLAUDE.md):table_in,table_to,col_key,exp_score,exp_filter,order_by. - Composition:
frs_point_snap(num_features = N)→frs_candidates_pick(exp_score, exp_filter, order_by)→frs_point_match(distance_max, tiebreak)reproduces the bcfp PSCIS-build pipeline byte-identically. First consumer: link#154 (lnk_pipeline_crossingsPSCIS↔︎modelled auto-snap), which will rewire to this three-step composition. - SQL composition: optional
WITH scored AS (SELECT *, (<exp_score>) AS score FROM <table_in>)CTE;SELECT DISTINCT ON (<col_key>) * FROM (scored | table_in); optionalWHERE <exp_filter>;ORDER BY <col_key>, <caller's order_by clauses>.col_keyprepended to ORDER BY to satisfy PostgreSQL’s DISTINCT-ON requirement. DROP + CREATE split into twoDBI::dbExecutecalls (RPostgres can’t run multi-statement SQL). - 25 mocked tests covering input validation, identifier sanitization, reserved-column collision (when
exp_scoreset), SQL composition (full path +exp_score = NULLvariant +exp_filter = NULLvariant), and the existing-score-column-allowed-when-exp_score-null edge case.
fresh 0.30.0
Closes #206. Adds frs_point_match() — a third primitive in the point-handling family alongside frs_point_snap (point↔︎stream) and frs_network_features (segment↔︎feature dnstr/upstr). Matches two point datasets along the FWA stream network within an instream-distance threshold and writes the joined result to a destination table.
- Generic over any pair of FWA-snapped point datasets (PSCIS↔︎modelled crossings, observations↔︎habitat confirmations, field-assessed↔︎user-added crossing dedup, etc.). All point inputs must already carry
blue_line_keyanddownstream_route_measure(the FWA convention) — typically viafrs_point_snapupstream. - Algorithm mirrors bcfp’s
02_pscis_streams_150m.sql(atsmnorris/bcfishpass@v0.7.14-125-g6e9cf1c, tunnelbcfishpass.log.model_run_id=121rebuilt 2026-05-05) — same-blue_line_keyjoin +ABS(drm_a - drm_b) < distance_max+DISTINCT ON (table_a_id, blue_line_key) ORDER BY distance_instream ASC NULLS LAST. LEFT JOIN preservestable_arows with no match (theirtable_b_id_colends up NULL). - Function parameters follow the
table_*convention from link/CLAUDE.md:table_a/table_b/table_tofor the three tables,table_a_id_col/table_b_id_colfor the ID columns. - Network-position columns (
blue_line_key,downstream_route_measure) hard-coded to the FWA convention. Per-side overrides (à lafrs_network_featuresv0.29.0) can be added if a real divergence appears. - Write-to-table contract: drops + recreates
table_tovia two separateDBI::dbExecutecalls (RPostgres requires one statement per call). Returnsconninvisibly. Different fromfrs_point_snap(returns sf) andfrs_network_features(returns tibble) because the result here is a derived dataset not a query result, and bcfp’s analog also writes table→table. - Live parity on ADMS PSCIS↔︎modelled at 100m instream: 60 / 60 (stream_crossing_id, modelled_crossing_id) pairs byte-identical to
bcfishpass.pscis.modelled_crossing_id. 0 in ours-not-ref, 0 in ref-not-ours. - Live parity on BULK PSCIS↔︎modelled (xref-excluded snap-only subset): 77 / 78 ref pairs identical (98.7% on ref, 5 in ours-not-ref). The 4–5 BULK edge-case divergences are documented in the function’s
@details— bcfp considers multi-stream candidates within 150m planar before settling on (PSCIS, stream), whilefrs_point_matchassumes the caller has already snapped each PSCIS to one stream. Thetiebreak = "planar"parameter closes the b-side dedup gap (from 6 → 5 diffs); the remaining 5 are addressed by #207 (frs_candidates_pick) — a sibling primitive that scores + picks per-key from a multi-candidate table. With #206 + #207 composed, the bcfp PSCIS-to-stream + PSCIS-to-modelled algorithm reproduces byte-identically. -
tiebreakparameter (c("instream", "planar")) controls the b-side dedup metric. Default"instream"(ABS(drm_a - drm_b)) requires no geometry and is self-consistent with the threshold filter."planar"(ST_Distance(a.geom, b.geom)) mirrors bcfp’s02_pscis_streams_150m.sqlline 190 tiebreak; requiresgeomcolumn on both tables. - 21 mocked tests covering validation (required args, scalar positive numeric
distance_max, identifier sanitization, same-name guard) and SQL composition (DROP + CREATE order, DISTINCT ON, same-blue_line_keyjoin predicate,distance_maxliteral, LEFT JOIN, ASC NULLS LAST tiebreak, ID-column carry-through). - First consumer: link#154 (
lnk_pipeline_crossings: missing PSCIS↔︎modelled 100m-instream auto-snap layer) which wires this primitive into link’s per-WSG crossings build.
fresh 0.29.0
Closes #204. Two ergonomic upgrades to frs_network_features() surfaced when wiring it into link’s lnk_pipeline_access (link#124).
- Per-side wscode/localcode column overrides. New args
segments_wscode_col,segments_localcode_col,features_wscode_col,features_localcode_col(all default"wscode_ltree"/"localcode_ltree"). The original “unified” intent in #204 didn’t survive contact withbcfishpass.observations(unsuffixedwscode/localcode) joined againstbcfishpass.streams(_ltree) — segments and features can have different column conventions on the same query. Passfeatures_wscode_col = "wscode"+features_localcode_col = "localcode"for that case. -
feature_idsis now an R list-column of character vectors rather than apq__textcolumn of array-literal strings ("{a,b,c}"). Callers canlengths(),lapply(),%in%directly. Empty / NULL arrays parse tocharacter(0). Internal helper.frs_parse_pg_array()does the parse — limited to unquoted-element arrays (the common case for FWA-snapped IDs); arrays containing commas or quotes inside elements would need a fuller parser per PostgreSQL arrays I/O. - Live parity on ADMS PSCIS barriers: still 1031 / 1031 byte-identical to bcfp.
- Live test added covers
bcfishpass.observationswith the per-side override — confirms column-resolution succeeds andfeature_ids[[1]]is a character vector ofobservation_keystrings, not a{...}literal.
fresh 0.28.0
Closes #201. Adds frs_network_features() — a direction-agnostic primitive that returns per-segment arrays of features at a relative position on the FWA stream network. For each row in a segments table, the function array_aggs feature IDs from a features table that lie either downstream of, or upstream of, that segment. Generic over any FWA-snapped point dataset (barriers, observations, water-quality stations, fish surveys, sediment samples — anything keyed by (blue_line_key, downstream_route_measure, wscode_ltree, localcode_ltree)).
- Sibling to
frs_network_downstream/frs_network_upstream(which are point→segments). New shape: segments→features (per-segment arrays). -
direction = c("downstream", "upstream")is required (no default).match.arg()enforces. -
aoidefaults toNULL(process all segments). Accepts WSG codes today; polygon / ltree forward-compat — will route through.frs_resolve_aoi()when generalised. - Output is a 2-column tibble.
feature_idsistext[]; segments with zero matches are absent from the output (INNER-JOIN semantics, mirroringbcfishpass.load_dnstr). Callers wanting all-segments-with-NULL can LEFT-JOIN the result back to their segments table. - SQL pattern mirrors
bcfishpass.load_dnstrexactly: subquery-INNER-JOIN feeding outer GROUP BY, with the canonicalORDER BY segment_id, wscode DESC, localcode DESC, drm DESCtriple at the subquery level for byte-identical element ordering. - Live parity test against bcfp’s
streams_dnstr_barriersfor ADMS PSCIS barriers: 1031 / 1031 segments byte-identical (mod sort). - First consumer: link’s
lnk_pipeline_access(link#124, in flight) — composes calls across (source × species) into the bcfp-shapestreams_accesstable.
fresh 0.27.6
Set SSD-friendly planner-cost defaults in docker/docker-compose.yml: random_page_cost=1.1, effective_io_concurrency=200, temp_buffers=64MB. PostgreSQL ships with values calibrated for spinning rust (4.0 / 1 / 8MB), which biased the planner away from index scans on segment-keyed lookups (WHERE blue_line_key = … AND drm <= … against streams_breaks is the hot path in link’s pipeline). Documents the new settings in docker/tuning.md and notes that the M1/cypher docker-compose override file (tracked separately) needs the same flags — compose merges override command: lists by replacing the base, not appending.
fresh 0.27.5
channel_width_min_bypass block schema change: replace single stream_order key with stream_order_min + stream_order_max to express child-order ranges. The single key was modeling only stream_order = 1 (bcfp parity), but frs_order_child always supported a range — child_order_min / child_order_max parameters. The new pair maps directly. Validator rejects the legacy stream_order key with unknown keys to flag stale rules.yaml files for regeneration.
Unblocks link’s dimensions.csv::rear_stream_order_child_min / rear_stream_order_child_max columns. No behaviour change for frs_order_child itself; this is a YAML schema rename only.
fresh 0.27.4
Allow optional distance_max key inside channel_width_min_bypass block in rules YAML — link uses it to cap the bypass to the lower N metres of each direct-trib BLK (i.e. frs_order_child(distance_max = N)). Validator accepts a positive numeric scalar; rejects zero / negative / non-numeric values.
fresh 0.27.3
Restore stream_order_max predicate in frs_order_child() — the previous patch (0.27.2) removed the s.stream_order = s.stream_order_max filter because the column doesn’t exist on fresh.streams. That was the wrong fix: the predicate is load-bearing for direct-child semantics. Without it, multi-order BLKs like a named creek that grows from order-1 headwaters to order-3 mouth (e.g. Divan Creek, BLK 356353593 in HORS) get their order-1 headwater segments credited as direct trib mouths of large rivers — they are not.
Fix: derive stream_order_max per BLK on the fly via MAX(stream_order) OVER (PARTITION BY blue_line_key) in a CTE, then apply s.stream_order = s.stream_order_max to bound the bypass to mouth-side reaches only. Same answer as bcfishpass.streams.stream_order_max (which is a stored column there). Verified against HORS BT — eliminates ~95 km of spurious credit on multi-order BLK headwaters.
fresh 0.27.2
Fix frs_order_child() SQL referencing nonexistent column s.stream_order_max. fresh.streams has stream_order and stream_order_parent from FWA but no stream_order_max — the 0.27.0 SQL failed at execution against any real database (only the SQL-shape unit tests passed, and they asserted the broken predicate). Drop the broken predicate; default both child_order_min and child_order_max to 1L when neither is set, which matches bcfishpass’s hardcoded stream_order = 1 predicate exactly. Caller-passed bounds still apply unchanged.
Adds a regression test that scans the emitted SQL across every parameter combination for stream_order_max and fails if it reappears.
fresh 0.27.1
Patch follow-up to 0.27.0. .frs_load_rules validator now accepts the channel_width_min_bypass predicate that link emits to drive frs_order_child() post-classify (link#96 wiring). Validates that the field is a named mapping with stream_order and stream_order_parent_min integer scalars. Without this patch, link’s emitted rules.yaml fails fast at frs_params() with unknown predicates: channel_width_min_bypass before any classification runs.
fresh 0.27.0
Closes #158. New frs_order_child(): post-classification UPDATE that credits direct order-1 (or other) tributaries of order-N+ rivers with <label> = TRUE. Captures the bcfishpass rearing bypass currently hard-coded in model/02_habitat_linear/sql/load_habitat_linear_<sp>.sql for BT/CH/CO/ST/WCT — predicate cw.channel_width >= rear_channel_width_min OR (s.stream_order_parent >= 5 AND s.stream_order = 1).
- Parametric:
parent_order_min(default5Lmatches bcfp),child_order_min/max,distance_max. Generic on label column (rearing/lake_rearing/wetland_rearing). - Idempotent + additive:
<label> IS NOT TRUEguard. Never adds rearing above an inaccessible barrier:accessible = TRUEguard. - No existing-caller behaviour change. Wiring into link’s pipeline is a follow-up PR (link side:
lnk_pipeline_classifycallsfrs_order_childpost-classify per-species, gated ondimensions.csv::rear_stream_order_bypass).
fresh 0.26.0
Closes #191. .frs_connected_waterbody Phase 2 (upstream spawn) cluster + lake-adjacency gate is now opt-out via a lake_adjacent parameter on the rule. Default TRUE keeps bcfishpass-parity behaviour byte-identical for any caller not setting the knob; passing lake_adjacent: no in <sp>.spawn_connected.lake_adjacent runs the relaxed Phase 2 that credits any spawn-eligible segment upstream of and accessible from a qualifying rearing waterbody. Used by callers that need to credit upstream tributary reaches not in lake-adjacent clusters (e.g. NewGraph default-bundle SK methodology — link#87). .frs_load_rules validator updated to accept the new key.
Cross-host testing pattern (m4 ↔︎ m1 over Tailscale, with Sys.setenv() overrides for PG_*_SHARE to point fresh tests at a host’s local Docker fwapg when the bcfp tunnel is unavailable) documented in CLAUDE.md “Testing on alternate hosts.”
fresh 0.25.0
Two bcfishpass-parity fixes surfaced during link’s WSG-coverage expansion (link’s MORR + KISP runs, 2026-04-30).
frs_cluster phase-1 + confluence-boost interaction (#186). .frs_cluster_both previously excluded on-spawning rearing segments from the clustering CTE. Removing those segments could shift cluster_minimums into the confluence-boost zone (DRM < confluence_m) and validate clusters that bridge_gradient should deny. bcfishpass’s path-2 keeps on-spawning segments in clusters, so its cluster_min stays high and the confluence boost doesn’t fire. Phase-1 protection now lives only in the final UPDATE step — on-spawning segments retain label_cluster = TRUE regardless of cluster outcome, but the cluster boundaries used for phase-2 / phase-3 testing are unchanged. Reproduction case: MORR ST cluster 502 (Nado Creek + tributary 360704379, 12.58% gradient between trib confluence and downstream spawning) — bcfp denies the trib’s rearing-eligible segments; fresh now matches.
.frs_trace_downstream averaged FWA gradient (#187). .frs_trace_downstream previously used whse_basemapping.fwa_downstreamtrace, an iterator returning FWA-original linear_feature_id rows with feature-averaged gradients. Localized barriers on a sub-piece of a long FWA feature (e.g. a 7 m, 84% lake-outlet drop inside an otherwise flat 3 km feature) are invisible to that approach. Switched to a FWA_Downstream predicate join against the broken streams table arg — same pattern .frs_cluster_both phase-3 already uses. Localized gradients produced by frs_network_segment() are now visible to the trace. New required columns in origins_sql: wscode_ltree, localcode_ltree (.frs_connected_waterbody is the only caller; updated). Reproduction case: KISP SK at Kitwancool Lake — bcfp’s model/02_habitat_linear/sql/load_habitat_linear_sk.sql correctly stops at the lake-outlet drop; fresh now matches.
Both fixes tighten link’s bcfishpass-config parity rollup. No API changes for end users; .frs_trace_downstream is an internal helper. NEWS bump from 0.24.1 → 0.25.0 because behaviour changes are observable in frs_habitat_classify / frs_cluster / frs_habitat outputs for any caller running connectivity or cluster-aware classifications.
fresh 0.24.1
Refresh bundled inst/extdata/parameters_habitat_rules.yaml to match link’s current default-bundle output. The bundled sample had drifted to pre-in_waterbody / pre-area_only shape (last synced 2026-04-12) while link’s bundles evolved through three #69 phases. Sync recovers the canonical sample for any frs_habitat() caller using fresh’s bundled defaults.
Reframe vignettes/habitat-pipeline.Rmd as a primitives walkthrough. Header note up front directs production users to frs_habitat() and link’s rule-based pipeline; vignette body unchanged — primitives are still useful for debugging, custom pipelines, and understanding what frs_habitat() does internally.
No behaviour change to any exported function.
fresh 0.24.0
Add area_only: true flag to rule grammar — decouples bucket-flag derivation from the main rear predicate (#182).
Today, a waterbody_type: L or W rule placed in a species’ rear: list does two things at once: triggers fresh to derive a lake_rearing / wetland_rearing predicate (the per-segment flag that drives lake_rearing_ha / wetland_rearing_ha area rollups), AND OR’s into the main rear predicate (the per-segment rearing flag, which drives linear rearing_km). The two effects are coupled, so a user can’t say “credit the polygon area but exclude polygon-mainlines from linear km.”
This release adds area_only: true on a rule. When set, fresh uses the rule’s predicate to derive the corresponding bucket flag (lake_rearing or wetland_rearing) but does not include the rule in the OR-chain that builds the main rear predicate. Stream-edge rules (with in_waterbody: false from #180) decide what counts as linear; L/W rules with area_only: true decide what polygons contribute area without double-counting the polygon-mainline as linear.
-
area_only: trueon awaterbody_type: L|Wrule → rule contributes tolake_rear/wetland_rearpredicate derivation only, excluded from mainrearpredicate. -
area_only: falseor absent → today’s behaviour (rule contributes to both). Backward compatible. - Validator: requires
waterbody_type: LorW(no bucket flag to drive on stream-edge rules orwaterbody_type: R); rejects non-logical / NA / length>1 values. - 13 new tests across
test-frs_params.R(validator) andtest-frs_habitat_predicates.R(decoupling). 167 PASS in those files (was 154). Full suite green.
Coordinates with link#69 phase 2 — lnk_rules_build() will emit area_only: true on L/W polygon rule blocks driven by per-species rear_lake_area_only / rear_wetland_area_only columns in dimensions.csv. Combined with the polygon-rule edge_types_explicit: [1000, 1100] filter (mainlines only), this expresses the use-case-2 model: linear excludes polygon-mainlines, area still rolls up.
fresh 0.23.1
Hotfix on top of 0.23.0 — register in_waterbody with the rules-YAML validator so emitted rules pass loading.
.frs_load_rules() validates each rule entry’s keys against an allowlist of known predicates (valid_predicates in R/frs_params.R). 0.23.0 added the predicate at the SQL emission layer (.frs_rule_to_sql()) but missed adding it to the allowlist, so a rules YAML carrying in_waterbody: was rejected by .frs_load_rules() before SQL emission ever ran. This release adds in_waterbody to valid_predicates.
- 1 new test:
.frs_load_rulesaccepts a YAML within_waterbody: true|falseon edge-type and waterbody-type rules (113 PASS intest-frs_params.R, was 112). - No behaviour change beyond unblocking the predicate that 0.23.0 introduced.
fresh 0.23.0
Add in_waterbody boolean predicate to the rule grammar (#180).
The rule grammar in parameters_habitat_rules.yaml already had positive predicates for selecting segments inside polygon waterbodies (waterbody_type: R/L/W) but no symmetric way to restrict a rule to segments outside any waterbody. That left the stream-edge rule families silent on polygon membership, so a stream rule like edge_types_explicit: [1000, 1100, 2000, 2300] matched both true streams and the same-coded centerlines that thread through wetland/lake/river polygons — overlapping with the polygon rules on those segments and double-classifying.
This release adds in_waterbody: false | true as the natural complement to waterbody_type:. Stream-edge rules can now express “this classification only applies outside polygon footprints,” which is the semantic complement of the existing polygon rules. Together the two predicates partition the network into stream-edge classifications and polygon-classifications with no overlap.
-
in_waterbody: false→ addss.waterbody_key IS NULLto the rule’s AND chain. -
in_waterbody: true→ addss.waterbody_key IS NOT NULL. -
absent → no constraint (pre-
in_waterbodybehaviour, backward compatible). - Composes with
waterbody_type: <letter>— the positive predicate already impliesIS NOT NULL, so the two together are redundant rather than contradictory. - Validation: non-logical / NA / length>1 values raise an error at
.frs_rule_to_sql()time. - 5 new tests under
test-frs_params.R(112 in that file, was 107). Full suite green.
Coordinates with link#69 — lnk_rules_build() will emit in_waterbody: false on stream-edge rule blocks once this lands.
fresh 0.22.0
frs_habitat_overlay() simplified — drop format and long_value_col parameters; accept only the canonical source-table shape (#177).
-
Canonical shape: one row per (segment × species), with join keys in
by, the species code inspecies_col(default"species_code"), and one indicator column per habitat type. Indicator coercion accepts integer 1, text'true'/'t'/'1'(case + whitespace insensitive), boolean. -
Dropped paths (breaking, pre-1.0):
format = "wide"per-species-suffix layout (spawning_sk,rearing_sk) andformat = "long"(habitat_typerows +habitat_indindicator). Neither had current production consumers — the wide-suffix layout was scoped for direct reads ofbcfishpass.streams_habitat_known(never integrated); the long format was link’s read of bcfishpass’s pre-2026-04-26 CSV (bcfishpass moved to a different shape on 2026-04-26). -
New parameter:
species_col(default"species_code"). Was added in PR #176’s first attempt as an additive bolt-on; this release lands it as the only path. -
Non-canonical sources: transform first via a SQL view, R pivot, or upstream adapter (e.g., link’s forthcoming
lnk_ingest_bcfishpass()), then call overlay against the canonical-shape view. Shape-translation lives with the consumer; fresh stays a thin SQL adapter. -
Bridge mode (
bridge = NULL) unchanged — orthogonal to source shape. - Tests: dropped wide-suffix and long-format paths; canonical-shape integration tests exercise integer + text + boolean indicators, additive guard, custom
species_col, customby, and bridge mode.
Coordinated link release (0.12.0) updates the call site in lnk_pipeline_classify.
fresh 0.21.0
frs_habitat_overlay() rename + 3-way bridge join. Pre-1.0 cleanup driven by review of v0.20.0; no deprecation alias.
-
Param renames (breaking):
-
table→to(matches fresh’s “destination” convention) -
known→from(matches “source” convention; also detaches from the “known habitat” provenance framing — function is the abstract overlay mechanism)
-
-
New
bridge = NULLparameter for 3-way joins. When supplied, the SQL becomesto ← bridge ← fromwith range containment:bridge.downstream_route_measure >= from.downstream_route_measure AND bridge.upstream_route_measure <= from.upstream_route_measure. Letstotables keyed byid_segment(e.g.fresh.streams_habitat) overlay from sources keyed by(blue_line_key, drm)ranges. -
Range columns auto-stripped from
byin bridge mode —downstream_route_measure/upstream_route_measureare handled by the range predicates, so they don’t need to be (and shouldn’t be) in the equalitybyclause. -
frs_habitat_classify()’sknown = NULLshortcut dropped (also breaking). Convenience hid configurability now that overlay has format + bridge knobs. Caller pattern is now explicitly two-step:frs_habitat_classify()thenfrs_habitat_overlay(). - Reads as a sentence:
frs_habitat_overlay(from = X, to = Y, bridge = Z)— “overlay flags from X to Y via this bridge.” Domain-agnostic — bridge isn’t required to befresh.streams; any segments table providingid_segment+ range columns works (lake centerlines, wetland shorelines, future cottonwood-pinned segmentations).
Surfaced in link#55 when fresh.streams_habitat (keyed by id_segment only) needed overlay from user_habitat_classification (keyed by (blue_line_key, drm) with range [drm, urm]).
Tests: 52 (was 43 in v0.20.0) — added bridge schema validation, SQL shape verification, range-containment integration test on a 3-segment fixture.
fresh 0.20.0
frs_habitat_overlay() accepts long-format known-habitat tables in addition to the original wide-format. Backward-compatible: the new format arg defaults to "wide".
- New args:
format = c("wide", "long")andlong_value_col = "habitat_ind". - Long-format expects: join keys (per
by) +species_code+habitat_type+ a boolean / text indicator column. Each row is one (segment × species × habitat_type) tuple. Matches link’suser_habitat_classification.csvshape, which is the bcfishpassuser_habitat_classification.csvsource format (before bcfishpass pivots it to wide forstreams_habitat_known). - Indicator accepts boolean OR text —
'TRUE'/'t'/'true'all qualify. Non-matching values (e.g.'FALSE', NULL) skip. - Up-front validation: long-format known table must have all required columns (join keys +
species_code+habitat_type+ indicator). Errors clearly if missing. - Wide-format path unchanged. 11 new tests covering long-format unit + integration; previous wide tests all still pass (42 total on
frs_habitat_overlay).
Surfaced in link#55 — link’s user_habitat_classification table is loaded long-format by lnk_pipeline_prepare. Forcing a pivot in link to satisfy the wide-format assumption was special-case overhead. Now fresh handles either shape so callers use whichever they have.
fresh 0.19.0
Decompose frs_habitat_classify() into composable pieces; rename frs_habitat_known() → frs_habitat_overlay(). Pre-1.0, breaking changes accepted to set the right shape before external use.
- New exported
frs_habitat_predicates(sp_params)— pure-R helper that takes a single species’ params and returns a named list of SQL boolean predicates (spawn,rear,lake_rear,wetland_rear) ready to embed inCASE WHEN <pred> THEN TRUE ELSE FALSE END. Extracted fromfrs_habitat_classify()’s per-species loop. Testable without a DB; 33 unit tests cover both rules-YAML and CSV-ranges paths, edge_type filters, and the lake/wetland W-rule gating. -
frs_habitat_classify()shrinks from 551 → ~447 lines and gains aknown = NULLparameter — when supplied, callsfrs_habitat_overlay()automatically as a post-step. Rule-based classification + observation overlay can now be a single call. -
Breaking:
frs_habitat_known()renamed tofrs_habitat_overlay(). The mechanism is overlay (additive OR-in); “known” presupposed provenance the function doesn’t enforce. No deprecation alias — pre-1.0, no external users.
fresh 0.18.0
- New
frs_habitat_overlay()— stitches known-habitat boolean flags from a wide-format lookup table INTO an existingstreams_habitatclassified byfrs_habitat_classify(). Mirrors the bcfishpass blend of model output (habitat_linear_<sp>) with manual / observation-based knowns (streams_habitat_known) into the publishedstreams_habitat_linearinteger encoding. Purely additive (FALSE → TRUEonly). Per-species columns{habitat}_{species_lower}matched against the species code instreams_habitat; missing per-species columns skip with a verbose message rather than erroring. Compound join key parameterisable viaby(defaultc("blue_line_key", "downstream_route_measure")). Surfaced as a need in link#55 — link’s pipeline loadeduser_habitat_classification.csvfor break-points + barrier overrides but never propagated its flags intofresh.streams_habitat. After this lands, link’slnk_pipeline_classifycan callfrs_habitat_overlay()as a post-step.
fresh 0.17.1
- Params validator accepts
wetland_ha_minpredicate on rear rules withwaterbody_type: W— mirrors the existinglake_ha_min/waterbody_type: Lrule. The classifier in 0.17.0 already readwetland_ha_minfrom rear rules to filter the fwa_wetlands_poly join; the validator predicate allowlist hadn’t been extended so rules YAML with the key was rejected. Surfaced in link#51 whenlnk_rules_build()started emittingwaterbody_type: Wrules with the threshold.
fresh 0.17.0
-
frs_habitat_classify()now honours thewaterbody_type: L/waterbody_type: Wentries under a species’rear:rules in the rules YAML. Previously thelake_rearing/wetland_rearingbooleans were set whenever a segment fell in the species’ rear channel-width window and matched a lake/wetlandwaterbody_key— regardless of whether the species had a lake/wetland rear rule declared. Now the flag isFALSEunless the species has the matching rule, and the rule’s optionallake_ha_min/wetland_ha_minthreshold filters the polygon join to exclude waterbodies below that area (#165). Surfaced in link#51 when every species in link’s bcfishpass and default bundles produced bit-identicallake_rearing_hatotals despite the bundles declaring different rear rules.
fresh 0.16.0
-
frs_habitat_classify()output schema gains awetland_rearingboolean column, mirroringlake_rearingbut joined towhse_basemapping.fwa_wetlands_polyonwaterbody_key(#164). Additive schema change — existing callers are unaffected;lake_rearingsemantics unchanged. Prerequisite for link’s compound rearing rollup (link#51).
fresh 0.15.0
- Remove unused
frs_fish_habitat()andfrs_fish_obs(). Both were BC-specific fetchers hard-coded to bcfishpass/bcfishobs table names and had no callers in fresh or link. If BC-specific fetchers become useful later they belong in link where the domain context lives (#162) - Exported function count: 41 → 39.
- Add missing
@param barrier_overridesdocstring onfrs_habitat_classify()— the parameter was already in the signature but absent from roxygen so the rendered help didn’t describe link’s main integration point. - Docs role-clarity refresh: README + CLAUDE.md ecosystem tables updated to describe link’s full scope; pipeline wording split into fish-habitat (link → fresh) and land-cover-change (fresh → flooded → drift); CLAUDE.md version + architecture file list brought current.
fresh 0.14.0
- Add
frs_barriers_minimal()— reduce barrier points to the downstream-most per flow path viafwa_upstream()self-join. Extracts the bcfishpass “non-minimal removal” pattern from link’s compare pipeline into a reusable fresh function. Typical reduction ~27,000 raw gradient barriers → ~700 minimal on a full watershed group (#160)
fresh 0.13.8
Three-phase cluster connectivity for rearing.
- Phase 1: on-spawning segments (both rearing AND spawning) excluded from clustering — always valid. Prevents over-removal of rearing on spawning streams (#153)
- Phase 3:
FWA_Downstream()on the broken streams table (mainstem only) replacesfwa_downstreamtrace()on raw FWA. Finds spawning downstream of rearing clusters with path gradient + distance constraints (#153) - BULK CH rearing +6.0% → +2.6%, BT rearing +1.3% → -2.2%. All species within 5% across 4 WSGs
fresh 0.13.7
Apply bridge gradient along downstream path in frs_cluster.
-
frs_cluster()downstream check now appliesbridge_gradientandbridge_distancesegment-by-segment along thefwa_downstreamtracepath. Upstream check remains boolean —FWA_Upstreamreturns tributaries that interleave with mainstem inrow_number()ordering, making path gradient unreliable on branching networks (#153)
fresh 0.13.6
Support spawn_connected rules for waterbody-adjacent spawning.
- Parse
spawn_connectedblock in rules YAML — permissive spawn thresholds for segments in the downstream trace from waterbody outlets. Own validation separate from spawn/rear rules (#154) - Additive step in
.frs_connected_waterbody(): accessible segments in the trace meetingspawn_connectedthresholds getspawning = TRUEeven if they failed standard classification. BULK SK spawning -9.6% to -0.7% vs bcfishpass (#154)
fresh 0.13.5
Fix lake outlet ordering and extract reusable downstream trace.
- Fix multi-BLK lake outlet selection: use
wscode_ltreenetwork topology instead ofdownstream_route_measure(meaningless across BLKs). BULK SK spawning -22.6% to +0.1% vs bcfishpass (#147) - Fix cumulative distance partitioning: partition by
waterbody_keyinstead ofblue_line_keyso distance accumulates per lake, not per BLK (#147) - Extract
.frs_trace_downstream()— reusable downstream trace with distance cap and gradient stop (#147) - Rename
.frs_connected_spawning()to.frs_connected_waterbody()— parameterized for any waterbody type (#147) -
waterbody_type: Lnow includes reservoirs (fwa_manmade_waterbodies_poly) via shared.frs_waterbody_tables()helper — the FWA table split is digitization origin, not ecology (#147)
fresh 0.13.4
Index input tables for standalone frs_habitat_classify performance.
-
frs_habitat_classify()now indexes input tables before the access gating loop — 35x speedup when called directly (bypassingfrs_habitat) (#150) -
.frs_index_working()is now idempotent withIF NOT EXISTS— safe to call multiple times on the same table (#150)
fresh 0.13.3
Two-phase connected spawning and multi-label breaks.
- Two-phase connected spawning for lake-rearing species (SK, KO) matching bcfishpass v0.5.0: downstream trace from lake outlets (3km cap, gradient stop) + upstream cluster with lake polygon proximity (
ST_DWithin). Auto-dispatched when rearing rules includewaterbody_type: L(#147) - Multiple labels per break position preserved — a gradient_15 and a falls at the same measure both survive in
streams_breaks(#145)
fresh 0.13.2
Preserve multiple labels per break position.
- Multiple labels at the same
(blue_line_key, downstream_route_measure)now survive instreams_breaks— a gradient_15 and a falls at the same measure are both preserved. Enables per-barrier overrides, access reporting, and habitat reporting by barrier type (#145)
fresh 0.13.1
Persist gradient barriers and downstream-only distance fix.
-
to_barriersparameter onfrs_habitat()— persist gradient barriers table with ltree enrichment for link’slnk_barrier_overrides()(#143) -
connected_distance_maxdistance filter now downstream-only with correctfwa_upstreamdirection (#133) - Fix
aoi+wsginteraction — character aoi ANDed with WSG filter instead of replacing it (#141)
fresh 0.13.0
Distance filter, aoi fix, and measure rounding.
-
connected_distance_maxdistance filter now downstream-only — upstream spawning has no distance cap, matching bcfishpass v0.5.0 SK spawning logic. Uses DRM difference (same-BLK) and ST_Distance Euclidean (cross-BLK) (#133) - Fix
aoireplacingwsgfilter instead of being additive —frs_habitat(wsg = "ADMS", aoi = "edge_type != 6010")now correctly scopes to ADMS, not province-wide (#141) -
measure_precisionparameter onfrs_network_segment()andfrs_habitat()— controls break measure rounding. Default0(integer, matching bcfishpass). Breaks table rounded and deduped before splitting (#135) - Auto-skip gradient/channel_width inheritance on
waterbody_type: L/Wrules (#131)
fresh 0.12.9
Post-cluster distance filter, measure rounding, and lake threshold auto-skip.
-
connected_distance_maxnow caps habitat EXTENT from connected segments (not just search distance). Post-cluster filter removes individual segments beyond the cap. SK spawning 112 km → ~74 km (#133) -
measure_precisionparameter onfrs_network_segment()andfrs_habitat()— controls decimal places for break measure rounding. Default0(integer, matching bcfishpass). Breaks table rounded and deduped before splitting (#135) - Auto-skip gradient/channel_width inheritance on
waterbody_type: L/Wrules — lake/wetland flow lines are routing lines (#131)
fresh 0.12.8
Lake/wetland threshold auto-skip and connected distance cap.
- Auto-skip gradient/channel_width CSV inheritance on
waterbody_type: Landwaterbody_type: Wrules — lake/wetland flow lines are routing lines, channel dimensions are meaningless (#131) -
connected_distance_maxpredicate in rules YAML — caps network distance from connected habitat whenrequires_connectedis present. SK/KO spawning capped at 3km from rearing lake (#133)
fresh 0.12.7
Replace observations with barrier_overrides.
-
barrier_overridesparameter replacesobservationsonfrs_habitat()andfrs_habitat_classify()— accepts a pre-computed table of(blue_line_key, downstream_route_measure, species_code)from link. Fresh skips matched barriers without counting, thresholds, or date filters (#129) - Remove observation counting SQL and
observation_*columns fromparameters_fresh.csv— fish passage interpretation belongs in link, not the network engine
fresh 0.12.6
Multi-class gradient barrier detection.
-
frs_break_find(classes =)— single-pass multi-class gradient detection matching bcfishpass v0.5.0. Tags every vertex with its gradient class, groups consecutive same-class vertices into islands, places one barrier at each class transition. Catches transitions WITHIN steep sections that boolean above/below missed (#127) -
frs_break_find(blk_filter =)— restrict to main flow lines (blue_line_key = watershed_key). Default TRUE (matches bcfishpass) -
frs_habitat()now callsfrs_break_find()once with all gradient classes instead of looping per threshold. Simpler, faster, more barriers detected.
fresh 0.12.5
Observation-based access override.
-
observationsparameter onfrs_habitat()andfrs_habitat_classify()— fish observations upstream of gradient/falls barriers override access gating per species. Thresholds fromparameters_fresh.csv:observation_threshold,observation_date_min,observation_buffer_m,observation_species(#69) - Defaults match bcfishpass v0.5.0: BT >= 1 obs (all salmonids count), CH/CO/SK/PK/CM/ST >= 5 obs (species-specific), since 1990, 20m buffer
- ADMS: BT accessible +103%, CH/CO +10% with
bcfishobs.observations
fresh 0.12.4
Post-segmentation gradient control.
-
gradient_recomputeparameter onfrs_network_segment()andfrs_habitat()—TRUE(default) recomputes gradient from DEM vertices after splitting;FALSEinherits parent segment gradient, matching bcfishpass v0.5.0 behavior (#124) -
frs_col_generate(exclude =)— skip named columns from regeneration (generic, reusable)
fresh 0.12.3
SK/KO spawning requires connected rearing lake.
-
requires_connectedpredicate in rules YAML — spawning segments must be spatially connected to rearing viafrs_cluster(). No rearing lake = no spawning (#120) - New
.frs_run_connectivity()orchestrates both rearing cluster checks (cluster_rearing) and spawning connectivity checks (requires_connected+cluster_spawning) after classification - SK/KO:
cluster_spawning = TRUEwith 3km bridge distance. KO added toparameters_fresh.csv - ADMS sub-basin: SK spawning 15 → 0 (correct — no lakes >= 200 ha)
fresh 0.12.2
Fix short barrier detection.
-
frs_break_find()gainsmin_lengthparameter (default0) — keeps all gradient islands regardless of length. A 30m waterfall at 20% gradient is a real barrier but was silently dropped by the old 100m minimum. Passmin_length = 100to restore pre-0.12.2 behavior (#118) - ADMS sub-basin at 15%: 137 barriers (was 98, +39 recovered)
fresh 0.12.1
Per-rule threshold overrides in habitat rules YAML.
- Rules can now specify
gradient: [min, max]andchannel_width: [min, max]that override CSV inheritance per rule. Missing fields still inherit from CSV whenthresholds: true(#116) - Bundled YAML updated: all
waterbody_type: Rrules now havechannel_width: [0, 9999]— skips channel_width_min on river polygons (matching bcfishpass v0.5.0 pattern where river polygon widths are unreliable)
fresh 0.12.0
Habitat eligibility rules format (Phase 1).
- YAML-based habitat rules for multi-rule species — each species gets a list of rules per habitat type joined by OR, each rule is AND of predicates. Replaces the single-rule-per-CSV-row limitation (#113)
- Bundled
inst/extdata/parameters_habitat_rules.yamlwith NGE defaults for 11 species (synced fromlink/inst/extdata/parameters_habitat_dimensions.csv) -
frs_params(rules_yaml =)loads rules YAML (default = bundled,NULL= skip rules for backward compat) -
frs_habitat(rules =)pass-through (NULL= bundled,FALSE= disable, string path = custom file) -
thresholds: falseper rule — wetland-flow carve-out pattern where a rule bypasses CSV gradient/channel_width inheritance -
Default behavior change: SK rearing now lake-only (area >= 200 ha); PK/CM rearing = 0 (explicit
rear: []); CO/BT/CH/ST/WCT/RB rearing expanded to include river polygons + wetland-flow segments. Passrules = FALSEto restore pre-0.12.0 behavior. - Phase 2 MAD support deferred to #114
fresh 0.11.1
Gradient barrier label format precision fix.
- New
gradient_NNNNlabel format — 4-digit zero-padded basis points (threshold × 10000) preserves precision for fractional thresholds.0.05→gradient_0500,0.0549→gradient_0549,0.15→gradient_1500. Resolution: 1 basis point (#110) - Fixes silent precision loss in 0.11.0 where
as.integer(thr * 100)collapsed0.05and0.0549both togradient_5, causing distinct biological thresholds to share a single break - New
.frs_validate_gradient_thresholds()errors on out-of-range, excess precision, NA, or label-collision inputs (catches the bug class going forward) - Parser
.frs_access_label_filter()accepts BOTH new format and legacygradient_Nfor backward compat with user-supplied labels viafrs_break_find(label = "gradient_15")
fresh 0.11.0
Sub-segment gradient resolution via auto-derived breaks.
-
breaks_gradientparameter onfrs_habitat()— generates gradient breaks at biologically meaningful thresholds derived fromspawn_gradient_max+rear_gradient_maxinparameters_habitat_thresholds.csv. Three modes:NULL(default auto-derive), numeric vector (explicit override),numeric(0)(disable, 0.10.0 behavior) (#101) - Default behavior change:
frs_habitat()now produces ~30% more segments on typical sub-basins (ADMS test: 312 → 409). The added breaks are at biologically meaningful gradient thresholds and givefrs_cluster()the resolution it needs to detect within-segment steep sections that would otherwise be hidden by averaging - Pass
breaks_gradient = numeric(0)to restore 0.10.0 behavior (only species access thresholds)
fresh 0.10.0
Network cluster connectivity analysis.
-
frs_cluster()— generic cluster connectivity validation. Groups adjacent segments sharing one label, checks if another label exists upstream, downstream, or both on the network. Disconnected clusters set to FALSE. UsesST_ClusterDBSCANfor spatial clustering andfwa_downstreamtrace()for downstream trace with gradient bridge and distance cap (#107) - Per-species cluster config in
parameters_fresh.csv—cluster_rearing,cluster_direction,cluster_bridge_gradient,cluster_bridge_distance,cluster_confluence_m. Anadromous species opt in; resident species do not -
direction = "both"evaluates upstream and downstream independently, keeps clusters valid in either direction
fresh 0.9.0
Flexible AOI, configurable access gating, and feature indexing.
-
frs_habitat()accepts any AOI — sf polygons, WHERE clauses, ltree filters — not just WSG codes. Addspeciesandlabelparams for custom runs (#96) -
gateparameter onfrs_habitat_classify()— setFALSEto classify raw habitat potential without access restrictions (#98) -
blocking_labelsparameter — configurable which labels restrict access. Default"blocked". Setc("blocked", "potential")for conservative analysis. Only"blocked"andgradient_Nblock by default; everything else passes through -
frs_feature_find()— locate any point features on the network (crossings, observations, stations) withcol_idfor feature identity (#92) -
frs_feature_index()— index upstream/downstream relationships between segments and features as ID arrays (#93) -
frs_break_find()slimmed to gradient-only. Point/table modes moved tofrs_feature_find() - Province-wide run completed: 229 WSGs, 5.98M segments
- Classify growth penalty fixed: constant-time DELETE by
watershed_group_code(#91)
fresh 0.8.0
Unified pipeline, domain-agnostic network segmentation, and major performance improvements.
New functions
-
frs_network_segment()— domain-agnostic network segmentation. Extracts, enriches, breaks at any point sources, assignsid_segment. One table, one copy of geometry. -
frs_habitat_classify()— long-format habitat classification. One row per segment x species withaccessible,spawning,rearing,lake_rearing. Species-specific accessibility via break label filtering. -
frs_feature_find()— locate any point features on the network (crossings, observations, stations). Replacesfrs_break_find()points/table modes. -
frs_feature_index()— index upstream/downstream relationships between segments and features. Stores feature ID arrays per segment.
Pipeline changes
-
frs_habitat()rewritten to wrapfrs_network_segment()+frs_habitat_classify(). Auto-generates gradient barriers per WSG from species parameters. -
to_streams+to_habitatparams replaceto_prefixfor persistent output tables. - mirai replaces furrr for parallel execution — lighter daemons, foundation for crew.aws.batch.
-
frs_break_find()slimmed to gradient-only (island detection). Point/table modes moved tofrs_feature_find().
Performance
- Island-based gradient barriers: 85% fewer barriers than interval method (#86)
- Accessibility recycled by threshold group: 5 queries → 2 for species sharing thresholds (#89)
- Classify growth penalty fixed: constant time DELETE by
watershed_group_code(#91) - Breaks table indexed with ltree GIST for cross-BLK queries: 15x speedup
- Province-wide: 229 WSGs, 5.98M segments completed
fresh 0.7.0
Local Docker fwapg instance and generic network-referenced classification via break_sources.
- Replace
fallsparameter withbreak_sourceslist across pipeline (frs_habitat(),frs_habitat_partition(),frs_habitat_access()) — accepts any number of point tables withlabel,label_col, andlabel_mapfor flexible classification (#70) - Add
labelandsourcecolumns to breaks table for provenance tracking - Ship
inst/extdata/falls.csv(3,294 barrier falls) andinst/extdata/crossings.csv(533k crossings) withdata-raw/refresh scripts - Add Docker-based local fwapg: containerized PostGIS + loader with GDAL/psql/bcdata (#63)
- Fix Phase 2 sequential mode to reuse
conninstead of callingfrs_db_conn()
fresh 0.6.0
Parallelized multi-WSG habitat pipeline — 2.5x speedup over sequential on BULK.
- Add
frs_habitat()— orchestrator: run the full habitat pipeline across watershed groups withfurrrparallelism (#61) - Add
frs_habitat_partition()— generic partition prep (extract, enrich, pre-compute breaks). Accepts any AOI, not just WSG codes - Add
frs_habitat_access()— gradient + falls barrier computation at a threshold, deduplicated across species sharing the sameaccess_gradient_max - Add
frs_habitat_species()— classify one species with pre-computed access and habitat breaks - Both Phase 1 (partition prep across WSGs) and Phase 2 (species classification) parallelize via
furrr::future_map()whenworkers > 1 - Benchmark scripts and logs in
scripts/habitat/
fresh 0.5.0
Watershed-group habitat pipeline — run the full pipeline across all species in a WSG.
- Add
whereparameter tofrs_extract()for SQL predicate filtering, ANDed withaoiwhen both provided (#60) - Add
frs_wsg_species()to look up species presence and bcfishpass view names per watershed group from bundledwsg_species_presence.csv - Add
data-raw/pipeline_wsg.R— runs full habitat pipeline per species per WSG with timing (baseline: 20 min for BULK, 7 species, 32K segments)
fresh 0.4.1
- Add
frs_categorize()for priority-ordered boolean-to-category collapse — mapping codes for QGIS, reporting, gq style registry (#57) - Refine habitat pipeline vignette: code below each narrative section with function links, 2x2 scenario grid, access barriers on habitat map
fresh 0.4.0
Coho habitat pipeline — classify habitat on the database at any scale. New vignette proves the workflow on the Byman-Ailport subbasin of the Neexdzii Kwa.
- Add
frs_col_join()for generic lookup table enrichment — channel width, MAD, upstream area, any key (#51) - Add
frs_edge_types()lookup helper with bundled FWA edge type CSV (41 codes from GeoBC User Guide) - Add
toparam tofrs_network()— write results to DB working tables instead of pulling to R, scales to any number of watershed groups (#53) - Add
whereparam tofrs_classify()— scope classification by SQL predicate for edge-type-aware and accessibility-filtered habitat modelling - Add
whereandappendparams tofrs_break_find()table mode — filter points table and combine multiple break sources (gradient + falls) in one breaks table (#38) - Add
exclude_edge_typesparam tofrs_point_snap()— only exclude subsurface flow (1425) by default, not wetland connectors (1410) (#52) - Bundle bcfishpass habitat parameter CSVs and fresh-specific access gradient thresholds (#54)
- Add coho habitat pipeline vignette: extract → enrich → break (gradient + falls) → classify (accessible, spawning, rearing, lake rearing) → aggregate → scenario comparison
fresh 0.3.1
- Add
fromandextra_whereparams to waterbody specs infrs_network()for filtering waterbodies to those connected to habitat streams (#49) - Network traversal table configurable via
.frs_opt("tbl_network")(#44)
fresh 0.3.0
Server-side habitat model pipeline — replaces ~34 bcfishpass SQL scripts with 4 composable functions. See the function reference for details.
- Add
frs_extract()for staging read-only data to writable working schema (#36) - Add
frs_break()family (find,validate,apply, wrapper) for network geometry splitting viaST_LocateBetweenandfwa_slopealongintervalgradient sampling (#38) - Add
frs_classify()for labeling features by attribute ranges, break accessibility (viafwa_upstream), and manual overrides — pipeable for multi-label classification (#39) - Add
frs_aggregate()for network-directed feature summarization from points (#40) - Add
frs_col_generate()to convert gradient/measures/length to PostgreSQL generated columns — auto-recompute after geometry changes (#45) - Add
.frs_opt()for configurable column names viaoptions()— foundation for spyda compatibility (#44) - All write functions return
conninvisibly for consistent|>chaining
fresh 0.2.0
CRAN release: 2020-05-29
-
Breaking: All DB-using functions now take
connas the first required parameter instead of...connection args. Create a connection once withconn <- frs_db_conn()and pass it to all calls. Enables piping:conn |> frs_break() |> frs_classify()(#35)
fresh 0.1.0
CRAN release: 2019-10-21
- Multi-blue-line-key support for
frs_watershed_at_measure()andfrs_network()viaupstream_blkparam (#20) - Add
frs_watershed_at_measure()for watershed polygon delineation with subbasin subtraction - Add
frs_network()unified multi-table traversal function replacing per-type fetch functions - Add
frs_default_cols()with sensible column defaults for streams, lakes, crossings, fish obs, and falls - Add
upstream_measureparam for network subtraction between two points on the same stream - Add
frs_waterbody_network()for upstream/downstream lake and wetland queries via waterbody key bridge - Add
wscode_col,localcode_col, andextra_whereparams for custom table schemas - Add
frs_check_upstream()validation for cross-BLK network connectivity - Add
blue_line_keyandstream_order_minparams tofrs_point_snap()for targeted snapping via KNN (#16, #17, #7, #18) - Add stream filtering guards: exclude placeholder streams (999 wscode) and unmapped tributaries (NULL localcode) from network queries;
include_allto bypass. Subsurface flow (edge_type 1410/1425) kept in network results (real connectivity) but excluded from KNN snap candidates (#15) - Add
frs_clip()for clipping sf results to an AOI polygon, withclipparam onfrs_network()for inline use (#12) - Add
frs_watershed_split()for programmatic sub-basin delineation from break points — snap, delineate, subtract with stableblk/drmidentifiers (#31) - Security hardening: quote string values in SQL, validate table/column identifiers, clear error on missing PG env vars, gitignore credential files (#19)
- Input type validation on all numeric params
- Add subbasin query vignette with tmap v4 composition
- Fix ref CTE to always query stream network, not target table
Initial release. Stream network-aware spatial operations via direct SQL against fwapg and bcfishpass. See the function reference for details.
