name: bulc-d-change
version: "0.1"
category: change-detection
themes: [vegetation, forest, disturbance, bayesian]
family: bayesian-land-cover-change
description: >
  BULC-D (Bayesian Updating of Land Cover, Cardille & Fortin 2016) over any AOI,
  defaulting to the Coast Range checkerboard. Reference vs target NBR periods,
  Bayesian evidence accumulation -> per-pixel change_magnitude [-1,1]. Detector for
  the emapr ensemble (research/change-detection/emapr-ensemble/CONTRACT.md). Shares
  the annual-NBR front-end with landtrendr-change verbatim (same Landsat NBR, so the
  detectors are comparable); only the final op differs. Generalizes
  registry/workspaces/bulc-d.folia.yaml (Camp Fire). Op: packages/compute/src/ops/bulc-d.ts.

# The annual-NBR front-end (imagery_composite) is identical to landtrendr-change and
# materializes under `folia run --local`. The bulc_d detector itself is a TS op
# (packages/compute/src/ops/bulc-d.ts), so the FULL recipe runs via the JS runner
# (registry/recipes/_runner/), not `--local` — bulc_d is not in the local engine op map.

settings:
  composite_method: median
  reference_end: 2020            # last year of the stable baseline; target = years after.
                                 # BULC-D needs a ROBUST multi-year baseline. Belize ablation
                                 # (T02, models/bulc-d/reference/belize/META.md): a 3-year
                                 # reference over-flags ~3x (14.7% vs 5.7% at 6yr, which matches
                                 # the GEE 5.3% density). Keep >=~6 reference years; a fitted
                                 # (detrend) expectation makes it WORSE, do not use it.
  bin_thresholds: [1.0, 2.0]     # z-score sigma bins (sensitivity)
  limit: 2                       # scenes PER YEAR (imagery_composite 3.0.0), not per window.
                                 # Was 24 across the 2015-2026 range, which resolved ~2/yr; the
                                 # per-year budget reproduces that band budget and is now window-
                                 # width independent, so moving date_range no longer re-thins it.

inputs:
  aoi:
    bbox: [-123.57, 45.72, -123.10, 46.07]   # default = Coast Range checkerboard
    crs_target: EPSG:32610
  date_range:
    start: "2015-01-01"
    end: "2026-12-31"
  collection:
    uri: "@usgs/landsat/c2-l2"
    assets: [nir08, swir22]      # standard NBR (SWIR2), matches landtrendr-change
  forest_mask: "@umd/hansen/gfc" # eMapR gladGfc: Hansen treecover2000>=10% (UN forest threshold).
                                 # "just deforestation" — masks the loss_prob OUTPUT to forest
                                 # (mirrors eMapR finalBULCprobs.updateMask, forestEnsembleFunctions L311).
                                 # Override with a custom binary-mask dataset URI; drop the mask layers to disable.

layers:

  # ── ANNUAL NBR (shared front-end, identical to landtrendr-change) ────────────────
  # imagery_composite resolves @usgs/landsat/c2-l2, fetches the [N, S2] band pair (nir,
  # swir-2), composites per year, and the pair → normalized difference = NBR. The dataset
  # owns the SR scale/offset. A bare `source:` layer + raster_calc does NOT materialize
  # under `folia run --local` (it only resolves the URI); this op-based pattern does.
  compute/nbr-annual:
    type: raster
    uri: "${inputs.collection.uri}"
    params:
      bands: [N, S2]                          # nir, swir-2 → normalized difference = NBR
      bbox: "${inputs.aoi.bbox}"
      temporal: ["${inputs.date_range.start}", "${inputs.date_range.end}"]
      method: "${settings.composite_method}"
      group_by: year
      limit: "${settings.limit}"
    style: { palette: rdylgn, clim: [-0.3, 0.7] }

  compute/bulc-d:
    type: raster
    compute:
      op: bulc_d
      inputs:
        time_series: { layer: compute/nbr-annual }
      params:
        reference_end: "${settings.reference_end}"
        bin_thresholds: "${settings.bin_thresholds}"
    style:
      band: loss_prob           # eMapR-aligned output: P(decrease)*100, 0-100 (CALIBRATION.md)
      palette: ylorrd
      clim: [0, 100]            # 0 = no loss (pale), 100 = confident loss (red)
      form:
        reference_end:  { type: slider, label: "Reference end year", min: 2015, max: 2024, step: 1, default: 2020 }
        bin_thresholds: { type: slider, label: "Sensitivity (sigma)", min: 0.5, max: 3.0, step: 0.25, default: 1.0 }

  # ── FOREST MASK — DEFERRED (eMapR "just deforestation") ──────────────────────────
  # Restrict loss to forest via Hansen GFC (@umd/hansen/gfc, treecover2000>=10, the UN
  # threshold; mirrors eMapR finalBULCprobs.updateMask, forestEnsembleFunctions L311).
  # DEFERRED: @umd/hansen/gfc has only a TILE-PATTERN (non-STAC) distribution, which
  # imagery_composite can't fetch; materializing a tile-pattern source under
  # `folia run --local` is a separate executor capability (tracked). Re-add
  # compute/forest-mask + a raster_mask on compute/bulc-d once it lands.

# Contract mapping (CONTRACT.md):
#   change_magnitude [-1,1] -> score = clamp(-change_magnitude, 0, 1)   (decrease = loss)
#   change_magnitude        -> magnitude (native)
#   sign(change_magnitude)  -> direction
#   |change_magnitude|      -> confidence (Bayesian posterior strength; BULC-D's strong field)
#   onset_year: WEAK -- BULC-D v0 gives a target-period magnitude, not a dated onset.
#               Set nodata unless the per-step belief is exposed. LandTrendr is the
#               strong-onset detector; the ensemble handles partial-field detectors.

views:
  - name: BULC-D loss probability
    layers: [compute/bulc-d]

dashboard:
  patterns:
    - slug: time-scrub-years
      slot: temporal
      layer: compute/nbr-annual

license:
  recipe: Apache-2.0
  output_dataset: CC-BY-4.0
  upstream_data:
    - { source: landsat-c2-l2, license: public-domain }

# References:
#   Cardille JA, Fortin MJ (2016) RSE 186:234-249 (BULC-D).
#   Contract: research/change-detection/emapr-ensemble/CONTRACT.md
#   Op: packages/compute/src/ops/bulc-d.ts ; prior recipe: registry/workspaces/bulc-d.folia.yaml
