WEPPcloud

← Back to usersum index

Roads NoDb Inslope Integration Specification

Status: Implemented (Phase 2 Step-2)
Last Updated: 2026-03-27
Scope: Inslope_bd and Inslope_rd road designs for the first WEPPcloud Roads NoDb integration. Canonical cross-route output_scope contract: docs/schemas/output-scope-contract.md.

Goal

Implement a repeatable pipeline that:

  1. Converts road linework into monotonic segments.
  2. Preserves original feature properties and adds segment metadata.
  3. Tags each segment with channel and receiving-hillslope Topaz IDs near its low point.
  4. Runs road-segment WEPP hillslopes and injects their effects into watershed routing via pass-file combination.

This phase targets only inslope bare-ditch and inslope rocked-ditch designs.

WEPPcloud User Story (Phase 1)

  1. User runs WEPP normally (existing WEPPcloud workflow).
  2. User enables Roads from the Mods dropdown; this instantiates a Roads controller for the run.
  3. User uploads a roads GeoJSON with road segment paths.
  4. User reviews/edits Roads run settings (surface/traffic defaults, soil texture, rock fragment percent, climate years, optional overrides).
  5. User clicks Run WEPPcloud Roads.
  6. System executes Roads pipeline:
    • monotonic segmentation and lowpoint attribution,
    • segment-to-hillslope/channel mapping,
    • single-OFE road segment WEPP runs,
    • pass-file combination against mapped hillslopes,
    • watershed rerun with combined pass files.
  7. User sees Roads run status and Roads-vs-baseline diagnostics/reports.

Roads NoDb Controller Integration (Phase 1 In Scope)

Roads is in-scope as a first-class NoDb controller for this phase.

Implementation targets:

  • Controller: wepppy/nodb/mods/roads/roads.py
  • Module package: wepppy/nodb/mods/roads/
  • WEPPcloud routes: wepppy/weppcloud/routes/nodb_api/roads_bp.py
  • UI controls/report templates:
    • wepppy/weppcloud/templates/controls/roads_pure.htm
    • wepppy/weppcloud/templates/reports/roads/*.htm

Controller type:

  • class Roads(NoDbBase) with run-scoped persisted state (roads.nodb).

Run precondition:

  • Baseline WEPP hillslope and watershed outputs exist for the run (wepp/output/H*.pass.dat, wepp/runs/pw0.run).

Activation and backend guard:

  • Roads is enabled/disabled through the canonical project mod endpoint (/tasks/set_mod).
  • Roads requires WBT delineation backend (watershed.delineation_backend_is_wbt == True).
  • Attempting to enable Roads on non-WBT runs must return an explicit error (same pattern as existing backend-gated modules).
  • Roads execution and preflight completion are WEPP-dependent; no Roads run is considered complete before WEPP completion.

Roads Controller State Contract

Minimum persisted state fields:

  • enabled: bool
  • uploaded_geojson_relpath: str | null
  • uploaded_geojson_sha256: str | null
  • discovered_attribute_catalog: dict | null (top-level feature-property field catalog for current upload)
  • roads_params: dict
  • last_prepare_summary: dict | null
  • last_run_summary: dict | null
  • status: one of idle|prepared|running|completed|failed
  • errors: list[str]
  • timestamps: dict

Roads artifacts (run-relative):

  • wepp/roads/segments/*
  • wepp/roads/runs/*
  • wepp/roads/output/*
  • wepp/roads/output/interchange/* (regenerated report resources)

Roads(NoDbBase) Implementation Blueprint

Implemented class contract (wepppy/nodb/mods/roads/roads.py):

  • class Roads(NoDbBase)
  • filename = "roads.nodb"
  • Persist all state in a run-local controller file (<wd>/roads.nodb) using standard NoDb locking semantics.

Controller responsibilities:

  • maintain roads enablement/config state for the run.
  • validate and stage uploaded roads GeoJSON.
  • execute deterministic segment preparation (monotonic_segments + lowpoint/channel/hillslope attribution).
  • orchestrate segment WEPP runs and watershed-injection rerun.
  • expose status and summarized diagnostics for UI/report endpoints.

Suggested public controller surface (phase 1):

  • set_enabled(enabled: bool) -> None
  • set_params(payload: dict) -> dict (normalized params)
  • set_uploaded_geojson(src_path: str) -> dict (saved path + checksum metadata)
  • prepare_segments() -> dict (writes wepp/roads/segments/*, updates last_prepare_summary)
  • run_roads_wepp() -> dict (writes wepp/roads/{runs,output}/*, updates last_run_summary)
  • query_status() -> dict
  • query_summary() -> dict

State machine contract:

  • idle -> prepared -> running -> completed
  • any execution error transitions to failed and appends diagnostic messages in errors.
  • successful parameter or upload changes after prepared/completed clear stale run summaries and return to idle.

Collaborators and dependencies:

  • Segment utility: wepppy/nodb/mods/roads/monotonic_segments.py
  • WEPP run context: wepppy.nodb.core.wepp.Wepp
  • Topaz translator: wepppy.wepp.out.top_summary.WeppTopTranslator
  • Watershed rerun builder: wepp_runner.wepp_runner.make_watershed_omni_contrasts_run
  • RQ workers (new): wepppy/rq/roads_rq.py with separate prepare/run entrypoints.

UI and Registration Touchpoints

Roads integration must include all of the following registration updates:

  • Add Roads to project mod display map:
    • wepppy/weppcloud/routes/nodb_api/project_bp.py (MOD_DISPLAY_NAMES).
  • Enforce Roads WBT backend guard in project mod toggle path:
    • wepppy/weppcloud/routes/nodb_api/project_bp.py (set_project_mod_state).
  • Add Roads to header Mods dropdown options:
    • wepppy/weppcloud/templates/header/_run_header_fixed.htm (header_mod_options).
  • Add Roads control metadata in run-0 mod registry, ordered immediately after Debris Flow:
    • wepppy/weppcloud/routes/run_0/run_0_bp.py (MOD_UI_DEFINITIONS).
  • Add Roads nav item and content section in run page template, ordered after Debris Flow:
    • wepppy/weppcloud/routes/run_0/templates/runs0_pure.htm.
  • Register Roads TOC/preflight emoji mapping:
    • wepppy/weppcloud/routes/run_0/run_0_bp.py (TOC_TASK_ANCHOR_TO_TASK, anchor #roads).
  • Register Roads preflight checklist selector mapping:
    • wepppy/weppcloud/static/js/preflight.js (getSelectorForKey).
  • Register Roads blueprint exports/imports:
    • wepppy/weppcloud/routes/nodb_api/__init__.py
    • wepppy/weppcloud/routes/__init__.py

Roads API/UI Workflow Contract

Expected route family (patterned after existing NoDb api/tasks/query/report routes):

  • POST /runs/<runid>/<config>/tasks/set_mod with payload {"mod":"roads","enabled":true|false} (canonical mod enable path)
  • POST /runs/<runid>/<config>/tasks/roads/upload_geojson
  • POST /runs/<runid>/<config>/tasks/roads/set_params
  • GET /runs/<runid>/<config>/api/roads/config
  • POST /runs/<runid>/<config>/api/roads/config
  • GET /runs/<runid>/<config>/api/roads/status
  • GET /runs/<runid>/<config>/api/roads/results
  • POST /runs/<runid>/<config>/tasks/roads/prepare_segments
  • POST /runs/<runid>/<config>/tasks/roads/run
  • GET /runs/<runid>/<config>/query/roads
  • GET /runs/<runid>/<config>/query/roads/summary
  • GET /runs/<runid>/<config>/report/roads/summary
  • GET /runs/<runid>/<config>/report/roads/results

UI control requirement:

  • Add a Run WEPPcloud Roads action in Roads controls once baseline WEPP exists and a roads GeoJSON is uploaded.
  • Roads control appears in the run page immediately after Debris Flow in both TOC and content stack.

Execution mode:

  • Queue-backed task execution (RQ) for prepare_segments and run to avoid request blocking.
  • RQ payload contract:
    • run_roads_prepare_rq(runid: str) executes only prepare stage.
    • run_roads_rq(runid: str) executes full run stage from latest prepared segments.
    • both jobs persist status transitions in roads.nodb.

Roads Report Resource Regeneration Contract (Implemented)

At the end of run_roads_wepp(), Roads executes _regenerate_roads_report_resources() and stores the result under last_run_summary["roads_report_resources"].

Behavioral contract:

  • Regeneration is roads-scoped only:
    • writes under wepp/roads/output/interchange/*,
    • never mutates baseline wepp/output/*.
  • Regeneration refreshes query-engine catalog registration for roads outputs.
  • Regeneration fails explicitly when required resources are missing (no silent fallback wrappers).
  • Output scope is explicit and fixed in this payload:
    • output_scope = "roads".

Persisted roads_report_resources fields:

  • status ("ready" on success)
  • output_scope ("roads")
  • roads_output_relpath
  • interchange_relpath
  • required_relpaths
  • missing_relpaths
  • roads_segment_loss_summary_relpath (null for single-storm runs)
  • generated_at

Required resource sets:

  • Non-single-storm:
    • H.pass.parquet
    • H.wat.parquet
    • loss_pw0.out.parquet
    • loss_pw0.hill.parquet
    • loss_pw0.chn.parquet
    • ebe_pw0.parquet
    • totalwatsed3.parquet
    • README.md
    • roads_segment_loss_summary.parquet
    • optional chnwb.parquet only when source chnwb.txt(.gz) exists.
  • Single-storm:
    • H.pass.parquet
    • ebe_pw0.parquet
    • README.md

Run Results link gating:

  • report/roads/results shows only report links whose required roads resources are present.
  • Non-single-storm-only links (for example return periods, yearly/avg water balance, streamflow, watershed loss summary) are hidden when required resources are absent.

Road segment loss summary artifact:

  • File: wepp/roads/output/interchange/roads_segment_loss_summary.parquet.
  • Built from roads.segment.pass.manifest.json + loss_pw0.hill.parquet.
  • Join precedence:
    • first target_hillslope_wepp_id,
    • fallback segment_run_id.
  • Includes diagnostics columns:
    • loss_match_key
    • loss_row_missing.
  • No on-disk CSV is written; CSV is served on demand via download conversion (?as_csv=1).

Roads API Payload and Validation Contract

upload_geojson request contract:

  • Accept multipart upload (file) with .geojson extension.
  • Require GeoJSON FeatureCollection with LineString/MultiLineString geometries.
  • Reject non-line geometries and invalid JSON with explicit error responses.

upload_geojson validation rules:

  • max upload size (phase-1 default): 50 MB.
  • reject empty feature sets.
  • require readable CRS context (input_crs param + optional GeoJSON crs.properties.name).
  • prepare stage uses configured input_crs for coordinate transforms during segmentation/attribution.
  • persist source checksum (sha256) and normalized ingest summary.

set_params request contract:

  • Accept JSON object only.
  • validate enums/ranges for:
    • soil_texture_default, surface_default, traffic_default,
    • rfg_pct_default, road_width_m_default,
    • optional attribute_field_map values:
      • design
      • surface
      • traffic
    • attribute discovery limit params:
      • attribute_discovery_profile_feature_limit
      • attribute_discovery_value_preview_limit
      • attribute_discovery_value_max_chars.

Attribute Discovery and Mapping Contract

  • Upload-time discovery scans top-level feature.properties keys and persists:
    • field_names
    • field_profiles (non_empty_count, distinct_non_empty_count, sample_values)
    • profile scope metadata (field_count, total_feature_count, profiled_feature_count, profile_truncated)
    • discovery-limit metadata (discovery_limits).
  • On each upload:
    • stale mapping selections are reset,
    • best-effort remap discovery is attempted by exact field-name match.
  • Mapping is used in both prepare and run stages:
    • prepare-stage design eligibility respects configured attribute_field_map.design,
    • run-stage design/surface/traffic resolution respects configured mapping fields.
  • Surface and traffic fallback values are explicit params:
    • surface_default must resolve to gravel or paved,
    • traffic_default must resolve to high, low, or none.
  • Surface and traffic resolution order depends on mapping state:
    • when mapped field is set: mapped field -> configured default value,
    • when mapped field is unset: legacy keys -> configured default value.
  • Missing custom mapped fields warn and fallback to configured defaults (no hard fail). Warning summaries are persisted in prepare/run summaries.
  • Discovery and mapping scope is top-level feature.properties only for this phase (nested paths out of scope).

Common API error messages (current behavior):

  • "Roads module is not enabled for this run."
  • "Roads requires WBT delineation backend."
  • "Provide multipart \file` for Roads upload."`
  • "Roads upload must be a .geojson file."
  • "Roads GeoJSON must be a FeatureCollection."
  • "Roads GeoJSON supports only LineString or MultiLineString geometries."
  • "Roads upload exceeds max_upload_mb limit (... MB)."
  • parameter-validation messages from set_params (for example numeric/range/enum validation failures).

TaskEnum and Preflight Contract

TaskEnum updates (wepppy/nodb/redis_prep.py):

  • add run_roads = "run_roads".
  • label: Run Roads.
  • emoji: 🚗.

Run page TOC/preflight mapping updates:

  • add #roads -> TaskEnum.run_roads in TOC_TASK_ANCHOR_TO_TASK (run_0_bp.py).
  • add selector mapping "roads": 'a[href="#roads"]' in weppcloud/static/js/preflight.js.

Preflight service checklist updates (services/preflight2/internal/checklist/checklist.go):

  • add checklist key roads (default false).
  • compute roads completion as WEPP-dependent:
    • check["roads"] = safeGT(prep["timestamps:run_roads"], runWepp).
    • if runWepp is missing, roads remains false.

Lock UI mapping update:

  • "roads.nodb" is mapped in preflight.js to the Roads run lock indicator.

Feasibility Assessment

What Already Works

  • wepppy.nodb.mods.roads.monotonic_segments already:
    • splits non-monotonic road paths by DEM profile.
    • defaults to 0.5 m tolerance.
    • preserves source properties.
    • assigns unique segment_id.
    • emits segment low-point point features.
    • sets topaz_id_chn_lowpoint and topaz_id_hill_lowpoint for eligible inslope designs.
  • Existing watershed runs consume H*.pass.dat entries from pw0.run, so swapping selected hillslope pass files is operationally feasible.
  • wepp_runner.make_watershed_omni_contrasts_run(...) already supports per-hillslope pass path substitution.
  • wepppyo3 already has high-performance hillslope pass parsing (hillslope_pass_to_columns), which is a good base for a pass combiner.

What Changed In Implementation

  • Segment eligibility/attribution is implemented for both:
    • Inslope_bd
    • Inslope_rd
  • Receiving hillslope attribution is implemented as topaz_id_hill_lowpoint with suffix invariant (1|2|3) enforced by utility behavior/tests.
  • Segment execution is implemented under wepp/roads/segments/ and wepp/roads/{runs,output}/.
  • Pass combination is implemented through wepppyo3.wepp_interchange.combine_hillslope_pass_files(..., strategy="phase1").

Technical Risk and Mitigation

  • Risk: pass-file fields like hydrograph-shape terms (tcs, oalpha) are not strictly additive.
  • Mitigation: phase 1 treats pass combination as a calibrated approximation and validates against targeted full reruns where available.
  • Risk: ambiguous lowpoint neighbors (multiple channel/hillslope candidates).
  • Mitigation: deterministic neighbor-order and tie-break rules (defined below).

Legacy WEPP:Road Alignment

The legacy WEPP:Road implementation (/workdir/fswepp2/api/wepproad.py) uses a 3-OFE hillslope (road/fill/buffer), not MOFE.
This Roads NoDb phase intentionally simplifies inslope runs to a single OFE hillslope representation, with road OFE parameterization borrowed from legacy road templates.

Implication: this is an approximation relative to full WEPP:Road 3-OFE physics; acceptable for phase 1 if validated against known scenarios.

Single-OFE Parameterization (Greenfield, Legacy-Derived)

This section defines the required parameterization contract for Inslope_bd and Inslope_rd in WEPPcloud Roads.

Source rationale:

  • wepproad.py design/surface/traffic logic (/workdir/fswepp2/api/wepproad.py)
  • legacy soil template semantics and comments (/workdir/fswepp2/api/db/wepproad/soils/*.sol)
  • legacy UI label shows inveg as Insloped, vegetated or rocked ditch (parity outputs under /workdir/fswepp2/parity-runs/wepproad/)

Design Mapping

  • Inslope_bd -> legacy inbare semantics.
  • Inslope_rd -> legacy inveg semantics (vegetated or rocked ditch bucket in legacy WEPP:Road).

Only these two designs are in scope for phase 1.

Surface Mapping

Target surface domain:

  • gravel
  • paved

Default normalization from incoming road properties (case-insensitive):

  • Dirt, Gravel, Graveled, Native, Unpaved -> gravel
  • Paved, Asphalt, Concrete -> paved

If no recognized surface value exists, use controller default (phase-1 default: gravel) and log a warning.

This legacy lookup applies when attribute_field_map.surface is unset. When a surface mapping is set, runtime order is mapped field -> surface_default.

Traffic Mapping

Target traffic domain:

  • high
  • low
  • none

Resolution order:

  1. explicit per-segment TRAFFIC property (if present/valid),
  2. mapped from CONDITION (phase-1 default mapping):
    • Impassable -> none
    • Year round -> high
    • New2011 -> low
  3. controller default (phase-1 default: low).

This legacy order applies when attribute_field_map.traffic is unset. When a traffic mapping is set, runtime order is mapped field -> traffic_default.

Soil Template Selection

Use legacy naming convention:

  • 3{surf_code}{texture}{tau_c}.sol

Where:

  • surf_code = "g" for gravel, "p" for paved
  • texture in {"clay","silt","sand","loam"}
  • tau_c selection:
    • default 2
    • Inslope_rd (inveg) -> 10
    • Inslope_bd (inbare) with paved surface -> 1

Resulting design/surface combinations:

  • Inslope_bd + gravel -> tau_c = 2
  • Inslope_bd + paved -> tau_c = 1
  • Inslope_rd + gravel -> tau_c = 10
  • Inslope_rd + paved -> tau_c = 10

Soil Parameter Adjustments

Apply legacy WEPP:Road adjustments to selected template:

  • rfg_pct handling:
    • write segment/default ubr value.
    • set urr_ref = 65 for gravel; 95 for paved.
    • set ufr_ref = (ubr + 65) / 2 for both gravel and paved.
  • traffic effect on detachability/interrill:
    • for traffic != high (low or none), divide first-layer Ki and Kr by 4.

Management File Selection

Use legacy management files:

  • base: 3inslope.man for both Inslope_bd and Inslope_rd
  • if traffic == none: 3inslopen.man

(traffic == low uses 3inslope.man; only soil Ki/Kr is reduced as above.)

Legacy Parameterization Matrix (Phase 1)

The following matrix is the greenfield single-OFE mapping contract to legacy WEPP:Road semantics.

Design Surface Traffic Soil Template Rule tau_c Management Ki/Kr Factor (layer 1) urr_ref
Inslope_bd gravel high 3g{texture}2.sol 2 3inslope.man 1.0 65
Inslope_bd gravel low 3g{texture}2.sol 2 3inslope.man 0.25 65
Inslope_bd gravel none 3g{texture}2.sol 2 3inslopen.man 0.25 65
Inslope_bd paved high 3p{texture}1.sol 1 3inslope.man 1.0 95
Inslope_bd paved low 3p{texture}1.sol 1 3inslope.man 0.25 95
Inslope_bd paved none 3p{texture}1.sol 1 3inslopen.man 0.25 95
Inslope_rd gravel high 3g{texture}10.sol 10 3inslope.man 1.0 65
Inslope_rd gravel low 3g{texture}10.sol 10 3inslope.man 0.25 65
Inslope_rd gravel none 3g{texture}10.sol 10 3inslopen.man 0.25 65
Inslope_rd paved high 3p{texture}10.sol 10 3inslope.man 1.0 95
Inslope_rd paved low 3p{texture}10.sol 10 3inslope.man 0.25 95
Inslope_rd paved none 3p{texture}10.sol 10 3inslopen.man 0.25 95

Single-OFE Slope File Contract

Road segment slope file generation is greenfield single-OFE:

  • one OFE only (n_ofe = 1)
  • profile width:
    • per-segment WIDTH_M override when available, else controller default (phase-1 default: 4.0 m)
  • profile length:
    • segment geometry length in meters
  • slope:
    • segment grade from oriented high-to-low endpoints over segment length,
    • clamped to safe bounds (0.1% to 40%) using legacy WEPP:Road road-slope validation bounds.

The segment geometry must be oriented downslope before slope-file emission.

Run-Level Parameter Defaults

Controller-level defaults (editable in Roads UI/API):

  • soil_texture_default: one of clay|silt|sand|loam
  • rfg_pct_default: 20
  • surface_default: gravel
  • traffic_default: low
  • road_width_m_default: 4.0
  • trace_max_steps: 20000
  • input_years: inherit baseline WEPP climate years unless explicitly overridden
  • wepp_bin: inherit baseline run setting unless explicitly overridden

Per-segment property overrides are supported for:

  • design
  • surface
  • traffic
  • soil texture
  • rock fragment percent
  • road width

WEPP Run Assembly Contract

For each selected segment, Roads must generate a standalone WEPP hillslope contributor run with:

  • climate: inherit baseline run climate station/files and simulation year span unless explicitly overridden in Roads params.
  • slope file: generated from monotonic, downslope-oriented segment profile (see monotonicity requirement below).
  • soil file: selected and adjusted using the legacy-derived rules in this specification.
  • management file: selected from 3inslope.man or 3inslopen.man.
  • run metadata:
    • deterministic run key based on segment_id,
    • preserved provenance fields (segment_id, source feature ID if available, topaz_id_chn_lowpoint, topaz_id_hill_lowpoint).

Contributor variants:

  • channel-associated segments: one-OFE road contributor (phase-1 behavior retained).
  • non-channel routed segments: two-OFE routed contributor (road OFE + buffer OFE), where:
    • road OFE uses the existing inslope road parameterization,
    • buffer OFE uses trace-derived path_length_m and slope statistics,
    • routed management transform strips fill scenario content and remaps yearly FOREST itype from 3 to 2 so WEPP management cardinality stays aligned with two-scenario output.

Required output for each successful segment run:

  • segment pass file under wepp/roads/output/.
  • per-segment execution record in last_run_summary with status and diagnostics.

Segment Utility Requirements

Module: wepppy/nodb/mods/roads/monotonic_segments.py

Design Filter

Define inslope eligibility with case-insensitive comparison:

  • design.lower() in {"inslope_bd", "inslope_rd"}

Only eligible designs receive channel/hillslope lowpoint attribution. All segments retain topaz_id_chn_lowpoint and topaz_id_hill_lowpoint properties; default value is null.

Output Feature Contract

Each output segment feature must preserve all source properties and include:

  • segment_id: globally unique string per output file (roads-seg-######).
  • topaz_id_chn_lowpoint: int | null.
  • topaz_id_hill_lowpoint: int | null.
  • _roads_low_point_x: low point x in source CRS.
  • _roads_low_point_y: low point y in source CRS.
  • _roads_low_point_elevation_m: sampled DEM elevation.
  • _roads_lowpoint_decision: prepare-stage classification label.
  • _roads_routing_eligibility: one of channel_associated, non_channel_routable, non_routable, design_not_eligible, missing_channel_lookup_rasters.
  • _roads_non_channel_routable: bool flag for run-stage trace eligibility.
  • _roads_lowpoint_row, _roads_lowpoint_col: lowpoint seed cell used for run-stage trace calls.
  • _roads_lowpoint_topaz_id, _roads_lowpoint_topaz_suffix, _roads_lowpoint_is_hillslope_pixel: lowpoint subwta diagnostics.

Companion low-point point features must carry the same properties as their segment, plus:

  • _roads_feature_type = "segment_low_point"

Channel Lowpoint Rule

For eligible designs:

  1. Compute lowpoint raster cell from segment lowpoint coordinate.
  2. Search [self + 8 neighbors] for channel mask cells (netful > 0).
  3. If found, set topaz_id_chn_lowpoint to that cell’s subwta Topaz ID.
  4. If none found, set null.

Determinism: preserve fixed offset order so repeated runs produce identical IDs.

Non-Channel Routable Rule (Step-2)

For eligible designs where channel-neighbor search fails:

  1. Inspect subwta at the low-point cell.
  2. If low-point subwta suffix is 1, 2, or 3, mark segment as non-channel routable:
    • _roads_lowpoint_decision = "non_channel_hillslope_routable"
    • _roads_routing_eligibility = "non_channel_routable"
    • _roads_non_channel_routable = true
  3. Otherwise mark non-routable:
    • _roads_lowpoint_decision = "no_channel_pixel_near_lowpoint"
    • _roads_routing_eligibility = "non_routable"
    • _roads_non_channel_routable = false

Hillslope Lowpoint Rule

For eligible designs with non-null topaz_id_chn_lowpoint:

  1. Let chn = topaz_id_chn_lowpoint (must end in 4).
  2. Candidate receiving hillslopes are:
    • chn - 3 (suffix 1, center)
    • chn - 2 (suffix 2, right)
    • chn - 1 (suffix 3, left)
  3. Sample subwta in a local neighborhood around the segment lowpoint cell.
  4. Select the first candidate present by deterministic priority:
    • nearest cell distance to lowpoint center.
    • then candidate priority center (chn-3), right (chn-2), left (chn-1).
    • then numeric ascending as final tie-break.
  5. Set topaz_id_hill_lowpoint to selected candidate; else null.

Required invariant:

  • If topaz_id_hill_lowpoint is not null, it must end in 1, 2, or 3.

wepp/roads Layout and Execution Contract

Run-root-relative directories:

  • wepp/roads/segments/
  • wepp/roads/runs/
  • wepp/roads/output/

Primary artifacts:

  • wepp/roads/segments/roads.inslope.monotonic.geojson
  • wepp/roads/segments/roads.inslope.low_points.geojson
  • wepp/roads/segments/roads.inslope.summary.json
  • wepp/roads/segments/roads.segment.pass.manifest.json
  • wepp/roads/runs/*.run (segment and watershed runs)
  • wepp/roads/output/H<segment_or_target>.pass.dat
  • wepp/roads/output/interchange/*
    • includes roads_segment_loss_summary.parquet and regenerated report resources
  • wepp/roads/roads.log

Layout rationale:

  • Roads is an in-run mod, not a child-run clone; storing outputs under wepp/roads/* aligns with existing mod layouts (for example wepp/ag_fields/*) and avoids unnecessary _pups/* coupling.
  • Keeping Roads pass files in wepp/roads/output/ allows a single watershed pw0.run in wepp/roads/runs/ to reference one directory (../output/) for both unchanged and roads-adjusted hillslopes.

Segment Selection for WEPP Runs

Operational filter for inslope step-2:

  • DESIGN in {Inslope_bd, Inslope_rd}
  • channel-associated execution path:
    • topaz_id_chn_lowpoint is not null,
    • topaz_id_hill_lowpoint is not null.
  • non-channel routed execution path:
    • _roads_non_channel_routable == true (prepare-stage metadata),
    • run-stage trace reaches channel,
    • traced receiving hillslope resolves to subwta suffix 1|2|3,
    • traced receiving hillslope maps through top2wepp.

Non-selected segments are recorded but not executed.

Road Slope File Monotonicity Requirement

Road segment WEPP slope files must be monotonic in elevation for each simulated segment.

Required behavior:

  • Before writing each segment .slp, verify the sampled profile is monotonic (within tolerance) in the direction used for WEPP routing.
  • If the segment geometry direction is opposite of the desired downslope routing direction, reverse coordinates before writing the slope file.
  • If a segment cannot be represented as monotonic after segmentation/tolerance rules, skip execution and report it in diagnostics.

Rationale:

  • WEPP hillslope routing assumes a consistent flow direction along profile length; non-monotonic segment slope files can produce unstable or nonphysical runoff routing behavior.

Hillslope Mapping Requirement

Each executed segment must map to a watershed hillslope WEPP ID using the run translator (WeppTopTranslator.top2wepp).

Contract:

  • segment -> topaz_id_hill_lowpoint -> wepp_id
  • if no mapping exists, segment is skipped and reported in summary diagnostics.

Watershed-Routing Injection Strategy

Phase 1 Strategy (Pass Combination + Watershed Rerun)

  1. Copy baseline wepp/runs to wepp/roads/runs.
  2. Generate and run segment-level WEPP hillslope runs under wepp/roads/.
  3. For each target hillslope WEPP ID, combine:
    • baseline hillslope pass (H<wepp_id>.pass.dat)
    • all mapped road-segment pass files
  4. Stage wepp/roads/output/ with one pass file per watershed hillslope:
    • for untouched hillslopes, stage baseline pass from wepp/output/H<wepp_id>.pass.dat (symlink preferred, copy acceptable when symlink is unavailable),
    • for roads-targeted hillslopes, write combined pass as wepp/roads/output/H<wepp_id>.pass.dat.
  5. Build watershed run in wepp/roads/runs/pw0.run using make_watershed_omni_contrasts_run with per-hillslope pass paths rooted at ../output/H<wepp_id>.
  6. Run watershed from wepp/roads/runs/pw0.run.

This preserves routing dynamics in WEPP watershed execution while injecting road effects.

Pass Combiner Specification (wepppyo3)

Preferred module location: wepppyo3/wepp_interchange.

Recommended API direction:

  • combine_hillslope_pass_files(base_pass, road_passes, out_pass, *, strategy="phase1")

Minimum behavior:

  1. Parse pass files to structured columns (existing parser).
  2. Align rows by simulation day key:
    • primary: year + julian
    • fallback: sim_day_index
  3. Resolve day-level event kind by precedence across contributors:
    • EVENT > SUBEVENT > NO EVENT
  4. Additive volume/mass fields (sum across base + roads):
    • runvol, sbrunv, drrunv, gwbfv, gwdsv, tdet, tdep.
  5. Depth fields (runoff, sbrunf, drainq) must be explicitly handled:
    • if contributing area metadata is available, recompute from combined volumes.
    • otherwise, sum same-unit depth terms when all contributors are directly comparable.
    • if neither rule can be applied, set deterministic fallback (0) and emit diagnostic metadata.
  6. Concentration fields:
    • derive per-class mass mass_i = sedcon_i * runvol.
    • sum masses.
    • recompute sedcon_i = total_mass_i / total_runvol when total_runvol > 0, else 0.
  7. Hydrograph-shape fields (dur, tcs, oalpha) and peak (peakro) in phase 1:
    • dur: max(dur_i) across aligned source events.
    • tcs: max(tcs_i) across aligned source events.
    • peakro: combine with SCS-triangular superposition (WEPP-like wshscs behavior), not simple sum.
    • oalpha: back-calculate from combined fields:
      • if runvol_comb > 0: oalpha_comb = max(tcs_comb / 24, peakro_comb * 3600 * tcs_comb / runvol_comb).
      • else: oalpha_comb = 0.
  8. Day-kind handling:
    • resolved EVENT: apply full merge behavior above.
    • resolved SUBEVENT: combine subsurface/tile/baseflow terms; set dur, tcs, oalpha, peakro to 0.
    • resolved NO EVENT: keep day as NO EVENT with zero flow fields.
  9. Serialize a valid WEPP hillslope pass file.

Notes:

  • This is an approximation. A later phase can introduce a physics-aware hydrograph merge.
  • Build combiner tests against parser round-trip and synthetic multi-source events.
  • oalpha is intentionally not capped to 1.0 in phase 1 to mirror current WEPP pass-writing behavior in wshpas.for (the upper-bound clamp is commented out there).
  • Rationale for phase-1 hydrograph-shape rules is based on WEPP internals in /workdir/wepp-forest:
    • hillslope pass writes tcs/oalpha (wshpas.for) and watershed reads htcs/halpha (wshred.for);
    • channel duration merge is max (wshcqi.for);
    • channel tc follows longest/max path logic (wshtc.for);
    • channel alpha merge uses max contributor alpha (wshpek.for);
    • multi-contributor peak merge uses SCS-triangular superposition (wshscs.for, called by wshpek.for);
    • left/right/top channel contributors are treated as upstream inflow in aggregation logic (wshcqi.for, wshchr.for).

Validation and Acceptance Criteria

Utility Tests

  • Existing monotonic split tests remain green.
  • Coverage includes tests for:
    • Inslope_rd channel attribution parity with Inslope_bd.
    • topaz_id_hill_lowpoint assignment and suffix invariant.
    • deterministic tie-break behavior.
    • null behavior when no nearby channel/hillslope exists.

Integration Checks

  • Segment pipeline writes expected artifacts under wepp/roads/segments/.
  • Segment-to-hillslope mapping summary includes counts:
    • eligible
    • mapped
    • skipped (no channel, no hillslope, no translator map)
  • Watershed rerun succeeds using combined pass files.
  • Compare baseline vs roads-injected watershed outputs (mass/flow deltas) for sanity bounds.
  • Queue wiring governance checks pass for new Roads jobs:
    • update wepppy/rq/job-dependencies-catalog.md,
    • run wctl check-rq-graph (and regenerate graph if drift is reported).

Implementation Milestones (Completed)

  1. Finalized segment utility behavior for both inslope designs (Inslope_bd, Inslope_rd), including channel/hillslope lowpoint attribution and deterministic IDs.
  2. Completed unit/integration coverage for monotonic_segments outputs and invariants.
  3. Implemented Roads(NoDbBase) controller (roads.py) with persisted state contract and status lifecycle.
  4. Implemented Roads API/UI scaffolding:
    • blueprint routes in roads_bp.py,
    • Roads controls panel with upload/config/run actions,
    • summary + run-results report endpoints/templates.
  5. Added RQ workers for prepare_segments and run orchestration.
  6. Implemented single-OFE segment WEPP run assembly under wepp/roads/{runs,output}/ using this parameterization contract.
  7. Integrated pass combiner in wepppyo3, then wired watershed rerun assembly with make_watershed_omni_contrasts_run.
  8. Added diagnostics/reporting, roads-scoped report-resource regeneration, and end-to-end validation on fixture runs.

Non-Goals (Phase 1)

  • Non-inslope designs (Outslope_*, etc.).
  • Exact replication of legacy 3-OFE WEPP:Road behavior.
  • Full WEPP:Road 3-OFE decomposition (road/fill/buffer) inside Roads phase 1.
  • Advanced physics-based hydrograph merge beyond the documented phase-1 approximation.

Future Concept Draft: Outslope_unrutted MOFE Hillslope Replacement

Status: concept draft only (not implemented in phase 1).

This concept treats Roads as an enhanced scenario model, not a baseline-vs-roads delta workflow.

Intent

  • model Outslope_unrutted with a multi-OFE profile that preserves explicit road, fill, and buffer behavior.
  • replace targeted receiving hillslope pass files with synthetic roads-aware pass files.
  • avoid additive double counting by replacing (not adding to) the targeted hillslope response.

Flow Regime and Routing Semantics (Concept Draft)

Design-regime mapping:

  • outslope unrutted: sheet-flow abstraction.
  • outslope rutted: point-source abstraction for water/sediment discharge from road low point.
  • inslope bare ditch and inslope rocky ditch: point-source abstraction for water/sediment discharge from road low point.

Physical assumptions:

  • inslope point-source cases assume ditch/culvert bypass of fill-slope dynamics.
  • outslope rutted assumes no culvert bypass; concentrated flow can erode across fill slope before entering downslope buffer.

Current implementation boundary (phase 2 step-2):

  • only inslope designs are implemented.
  • channel-associated low points keep the phase-1 behavior (topaz_id_chn_lowpoint + topaz_id_hill_lowpoint prepare mapping).
  • non-channel low points are now routable when the low-point subwta suffix is 1|2|3; run-stage tracing routes those segments to channel and executes routed contributors as road OFE + buffer OFE.
  • routed contributors are merged through the existing pass-combine flow using traced receiving-hillslope attribution.

Implemented step-2 non-channel low-point path:

  1. If low point is not channel-associated, evaluate low-point cell in subwta.
  2. If subwta value is a hillslope pixel (int value ending in 1|2|3), mark segment as non-channel routable in prepare outputs.
  3. During run, trace routable low points to channel via wepppyo3.roads_flowpath.trace_downslope_flowpath(...).
  4. If trace reaches channel, build routed contributor profile as road OFE + flowpath buffer OFE:
    • road OFE from existing inslope segment parameterization,
    • buffer OFE from trace path length/slope.
  5. Resolve receiving hillslope from traced pre-channel cell (subwta suffix 1|2|3) and merge resulting contributor pass into mapped hillslope output.
  6. If trace does not reach channel, skip contributor generation and persist explicit diagnostics in run summary/logs.

Step-2 as-implemented contract notes (2026-03-27 review):

  • point-source contributor execution currently requires channel delivery (trace_reaches_channel == true); traces that terminate on hillslope interior are explicitly skipped (trace_did_not_reach_channel).
  • run summary diagnostics include trace_invocation_count, trace_reached_channel_count, trace_termination_reason_counts, and segment_routing_mode_counts for deterministic branch accounting.
  • run input parameter trace_max_steps (default 20000) is exposed and validated as a positive integer.
  • this behavior applies only to inslope point-source designs in step-2; outslope_rutted and outslope_unrutted remain follow-on implementations.

High-Fidelity Concept (Top-Level)

  1. Identify outsloped road discharge strips and trace downslope delivery paths to the channel network.
  2. Partition traced strips by receiving WEPP hillslope ID so each contributor maps deterministically.
  3. For each affected strip, build a roads-aware MOFE profile with ordering:
    • upslope hillslope segment -> road -> fill -> downslope buffer segment.
  4. For each affected receiving hillslope, represent unaffected remainder area with non-road hillslope contributor profile(s).
  5. Run WEPP for all contributors, then assemble one synthetic H<wepp_id>.pass.dat per affected hillslope by contributor aggregation.
  6. Stage synthetic hillslope pass files as replacements for affected hillslopes; keep baseline pass files for untouched hillslopes.
  7. Run watershed routing once using the full set of staged pass files.

Fidelity Invariants

  • Replacement semantics: targeted hillslope response is replaced, not incrementally added.
  • Area conservation: affected-strip plus unaffected remainder area equals original hillslope area.
  • Buffer preservation: final downslope buffer OFE remains explicit in the roads-aware contributor profile.
  • Topology preservation: watershed structure remains unchanged (left/right/top hillslope-channel linkage stays canonical).
  • Road geometry parity: Outslope_unrutted road OFE parameterization follows legacy outslope geometry intent (including area-preserving transform behavior).

Deferred Details (To Be Specified Later)

  • exact strip delineation and flowpath tracing rules.
  • hillslope-boundary splitting when a traced strip crosses multiple hillslopes.
  • contributor aggregation math for hydrograph-shape terms in replacement mode.
  • parameter defaults and per-segment overrides for outsloped designs.
  • handling for segments that terminate on channels vs within hillslope interiors.

Point-Source Flowpath Trace Contract in Rust

Status: step-1 substrate implemented (2026-03-27).

Direction

  • keep the point-source routing engine in Rust (no pure-Python reimplementation).
  • keep one shared implementation in peridot and expose it through both CLI and wepppyo3.

Implemented Step-1 Surfaces

  • peridot core API:
    • roads_trace::trace_downslope_flowpath(...) -> TraceDownslopeResult
  • peridot CLI wrapper:
    • trace_downslope_flowpath --subwta --flovec --relief --seed-row --seed-col [--channel] [--max-steps] [--out-json]
  • wepppyo3 runtime API:
    • wepppyo3.roads_flowpath.trace_downslope_flowpath(subwta_path, flovec_path, relief_path, seed_row, seed_col, channel_path=None, max_steps=20000) -> dict

Implemented v1 Trace Result Contract

Returned fields (CLI JSON and wepppyo3 dict keys):

  • seed_row, seed_col, seed_topaz_id
  • reaches_channel
  • channel_row, channel_col, channel_topaz_id
  • termination_reason
  • rows, cols, indices
  • distance_m, elevation_m, segment_slope
  • path_length_m, drop_m, mean_slope, max_slope

Termination labels:

  • hit_channel
  • invalid_flow_direction
  • loop_detected
  • raster_edge
  • max_steps_exceeded

Channel-detection rule in v1:

  • when channel_path/channel_mask is provided, channel is mask > 0.
  • when channel mask is absent, channel is inferred from SUBWTA suffix 4 (topaz_id % 10 == 4).

v1 integration constraints:

  • subwta, flovec, relief, and optional channel-mask rasters must share identical raster shape (width/height); shape mismatches fail explicitly.
  • seed inputs are raster cell coordinates (seed_row, seed_col, 0-based), not projected/map coordinates.
  • v1 does not perform raster reprojection/resampling inside trace execution; callers are responsible for providing aligned rasters.
  • CLI and wepppyo3 wrappers are contract-bound to the shared peridot core API (validated by parity tests).

Ordered Follow-On Plan

  1. Implement outslope_rutted point-source routing for both channel-associated and non-channel low points, including explicit fill OFE handling.
  2. Implement outslope_unrutted as MOFE hillslope replacement (hill -> road -> fill -> hill) with replacement semantics (no double counting).