name: change-ensemble
version: "0.1"
category: change-detection
themes: [vegetation, forest, disturbance, ensemble]
family: forest-loss-detector-ensemble
description: >
  Forest-loss ensemble over any AOI (default Coast Range checkerboard): runs the
  detectors that already exist in Folia (LandTrendr + BULC-D) on a shared annual-NBR
  front-end, maps each to a 0-100 loss probability, and combines them with the
  mask-aware naive average (the `ensemble` op), mirroring the eMapR
  forestChangeEnsemble `getNaiveLossAverage` (research/change-detection/emapr-ensemble/
  CALIBRATION.md). Adding CCDC/COLD, OPERA, etc. is just another detector input.

# EXECUTION TARGET: the JS runner (registry/recipes/_runner/), not `folia run --local`.
# The annual-NBR front-end (imagery_composite) materializes locally, but the
# LandTrendr->loss_prob stretch uses named-band raster_calc + output_band (TS-runner
# syntax; the local engine names bands b1..bN and rejects output_band), and bulc_d +
# ensemble are TS ops absent from the local op map. v1 detector set = LandTrendr + BULC-D.
# NOTE on LandTrendr loss_prob: eMapR uses the per-year FTV difference; this recipe
# uses a whole-series magnitude stretch as a v1 placeholder until the FTV-diff signal
# is wired (CALIBRATION.md reconciliation). BULC-D loss_prob is eMapR-exact (P(dec)*100).

settings:
  composite_method: median
  limit: 6                       # scenes PER YEAR (imagery_composite 3.0.0), not per window.
                                 # Was 24 across the whole range, which resolved ~2/yr; 6 is the
                                 # per-year density POLICY (not a LandTrendr invariant) and is now window-width
                                 # independent, so a wider date_range no longer thins each year.
  # LandTrendr
  max_segments: 8
  recovery_threshold: 0.25
  lt_loss_ref: 0.5               # NBR loss depth mapped to loss_prob=100 (placeholder; calibrate)
  # BULC-D — keep >=~6 reference years; a 3-year baseline over-flags ~3x (T02, Belize META.md).
  reference_end: 2020
  bin_thresholds: [1.0, 2.0]
  # Ensemble weights (eMapR counts BULC-D twice when running its two variants)
  w_landtrendr: 1
  w_bulcd: 1

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]
  forest_mask: "@umd/hansen/gfc" # eMapR gladGfc (Hansen treecover2000>=10%, UN threshold).
                                 # Masks each detector's loss_prob to forest BEFORE the ensemble;
                                 # the mask-aware average then excludes non-forest (NaN) per pixel.

layers:

  # ---- shared annual-NBR front-end (identical to the detector recipes) ----
  # imagery_composite resolves @usgs/landsat/c2-l2, fetches the [N, S2] band pair, and
  # composites per year; the pair → normalized difference = NBR (dataset owns the SR
  # scale/offset). A bare `source:` layer + raster_calc does NOT materialize under
  # `folia run --local`; this op-based pattern (matching landtrendr-change) 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}"

  # ---- forest mask (T01): eMapR "just deforestation", shared by every detector ----
  # DEFERRED: @umd/hansen/gfc (Hansen treecover2000>=10) 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). Until it lands the detectors feed the ensemble unmasked; re-add
  # compute/forest-mask + raster_mask on each detector to restore "just deforestation".

  # ---- detector 1: LandTrendr -> loss_prob (placeholder stretch, see note) ----
  detector/landtrendr:
    type: raster
    compute:
      op: landtrendr
      inputs:
        time_series: { layer: compute/nbr-annual }
      params:
        max_segments: "${settings.max_segments}"
        recovery_threshold: "${settings.recovery_threshold}"

  detector/landtrendr-loss:
    type: raster
    compute:
      op: raster_calc
      inputs:
        data: { layer: detector/landtrendr }
      params:
        # loss_prob = clamp(-magnitude / lt_loss_ref, 0, 1) * 100  (magnitude<0 = loss)
        expression: >
          where((0 - magnitude) / ${settings.lt_loss_ref} < 0, 0,
            where((0 - magnitude) / ${settings.lt_loss_ref} > 1, 1,
              (0 - magnitude) / ${settings.lt_loss_ref})) * 100
        output_band: loss_prob

  # detector/landtrendr-masked (raster_mask on compute/forest-mask) is DEFERRED with the
  # forest mask above; the unmasked loss_prob feeds the ensemble until tile-pattern Hansen
  # materialization lands.

  # ---- detector 2: BULC-D -> loss_prob (eMapR-exact: P(decrease)*100) ----
  detector/bulcd:
    type: raster
    compute:
      op: bulc_d
      inputs:
        time_series: { layer: compute/nbr-annual }
      params:
        reference_end: "${settings.reference_end}"
        bin_thresholds: "${settings.bin_thresholds}"

  # detector/bulcd-masked is DEFERRED with the forest mask above (same reason).

  # ---- ensemble: mask-aware weighted average of the detector loss_probs ----
  # The ensemble reads band "loss_prob" and falls back to each input's sole band, so the
  # single-band detector outputs feed in correctly. (Forest masking is deferred — see
  # above; once it lands, point these inputs back at detector/{landtrendr,bulcd}-masked.)
  compute/ensemble:
    type: raster
    compute:
      op: ensemble
      inputs:
        landtrendr: { layer: detector/landtrendr-loss }
        bulcd: { layer: detector/bulcd }
      params:
        band: loss_prob
        weights:
          landtrendr: "${settings.w_landtrendr}"
          bulcd: "${settings.w_bulcd}"
    style:
      band: loss_prob
      palette: ylorrd
      clim: [0, 100]              # 0 = no loss (pale), 100 = confident loss (red)

views:
  - name: Ensemble loss probability
    layers: [compute/ensemble]
  - name: Detectors
    layers: [detector/landtrendr-loss, detector/bulcd]

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 }

# Combination + per-detector calibration: research/change-detection/emapr-ensemble/CALIBRATION.md
# Ops: packages/compute/src/ops/{landtrendr,bulc-d,ensemble}.ts
