Skip to content

frs_habitat_overlay: relax bridge mode from strict containment to range overlap #178

@NewGraphEnvironment

Description

@NewGraphEnvironment

Problem

frs_habitat_overlay() with bridge set requires that each bridge segment's [downstream_route_measure, upstream_route_measure] range be fully contained inside the source row's [drm, urm] range to flip:

bridge.downstream_route_measure >= from.downstream_route_measure
AND bridge.upstream_route_measure   <= from.upstream_route_measure

When a bridge segment partially overlaps a source range (e.g., bridge [80, 130] vs source [100, 200]), the overlay skips it. Real-world consequence: link's bcfishpass-bundle SK BABL spawning catches only ~85 km of bcfishpass's published 132 km of known habitat — the missing ~47 km is in segments whose ranges fall outside their corresponding user_habitat_classification.csv rows because of how link's segmentation breaks at neighboring features (gradient barriers, crossings, etc.) inside or across the CSV ranges.

Why this came up

bcfishpass's streams_habitat_known builder marks any 100 m segment that overlaps a CSV range as known habitat. Link's overlay is stricter — strict containment — so partial overlaps are dropped silently.

This drove a -33% diff_pct on SK BABL spawning when the link bcfishpass-bundle compared against bcfishpass.streams_habitat_linear (model + known). The bundle's overlay step is now disabled in link 0.13.1 (pipeline.apply_habitat_overlay: false) because the bcfishpass bundle's job is to reproduce bcfishpass's RULE classification (habitat_linear_<sp>), not the post-overlay blend. So the parity gap is closed for the bundle. But the overlay's range-containment behaviour still under-counts when used in any consumer that wants the model+known view — the default link bundle, hypothetical reporting consumers, etc.

Proposed solution

Switch the bridge SQL from strict containment to range overlap (any non-empty intersection):

bridge.upstream_route_measure   >= from.downstream_route_measure
AND bridge.downstream_route_measure <= from.upstream_route_measure

Matches bcfishpass's "any 100 m segment that overlaps" behavior. Partial overlaps now flip.

Side effects to think through

  • Overhang: a long bridge segment that only overlaps a small portion of the CSV range gets fully flipped. For fine-grained bcfishpass-style segmentation (100 m) this is a tiny over-count; for coarse link segments it could matter. Worth a paragraph in the docstring.
  • Multiple matches per bridge segment: a segment might overlap multiple CSV rows; current SQL does INSERT ... ON CONFLICT DO NOTHING for direct mode and an UPDATE for bridge mode — confirm the UPDATE-multiple-times-with-same-result is idempotent (it is — additive guard makes it so).
  • Tests: add an integration test fixture with a partial-overlap range; assert the segment flips.

Versions

  • fresh: 0.22.0 (current main) → target 0.23.0 with the relaxation
  • link: would pick up via Suggests: bump; default bundle gets back its overlay accuracy

Related

  • link 0.13.1 disabled the overlay on the bcfishpass bundle to close the parity gap (apply_habitat_overlay: false); this fresh fix is what would let the default bundle get full known-habitat coverage
  • link research/default_vs_bcfishpass.md §6 / §7 traces the BABL SK gap quantitatively (60 km of csv_only segments that link doesn't catch)

Relates to NewGraphEnvironment/sred-2025-2026#24

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions