# catalog/operations/analysis/dnbr.yaml
#
# dNBR (delta Normalized Burn Ratio) — MTBS-convention burn severity.
# Producer for registry/concepts/burn-severity.yaml.

id: analysis_dnbr
name: dNBR Burn Severity
description: >
  Compute differenced Normalized Burn Ratio (dNBR) from a pre-fire and
  post-fire scene pair, emit the scaled (×1000) dNBR raster, and classify
  pixels into the canonical MTBS / Key & Benson (2006) severity codes.

  Two input modes are supported:

  - `from-scenes` (default): inputs are pre/post multispectral scenes with
    NIR and SWIR2 bands. NBR is computed internally as
    `NBR = (NIR − SWIR2) / (NIR + SWIR2)` for each scene.
  - `from-nbr`: inputs are already-computed NBR rasters (e.g. chained
    from `imagery_radiometric_indices`). The op skips NBR computation
    and only differences and classifies.

  dNBR is emitted scaled by 1000 to match the MTBS convention (the same
  axis used by the `burn-severity` concept class table). The categorical
  severity raster is always produced as a secondary output.
version: 1.0.0
category: analysis
type: raster-raster-to-raster
default_implementation: native

inputs:
  - name: pre
    type: raster
    format: geotiff
    description: >
      Pre-fire input. When `mode: from-scenes`, a multispectral scene
      containing NIR and SWIR2 bands (e.g. Sentinel-2 with B8 + B12).
      When `mode: from-nbr`, a single-band NBR raster (float, [-1, 1]).
    required: true
  - name: post
    type: raster
    format: geotiff
    description: >
      Post-fire input. Same band/format expectations as `pre`. Must be
      co-registered to `pre` to sub-pixel accuracy; phenological
      anniversary-date pairs are recommended to reduce false change.
    required: true

requires:
  # Op-level band rollup — both inputs need NIR + SWIR2 to compute NBR.
  # Ignored when `mode: from-nbr` (inputs are already NBR rasters).
  bands: [nir, swir2]

outputs:
  - name: dnbr
    type: raster
    format: cog
    dtype: float32
    unit: scaled_dnbr
    index: dNBR  # formula artifact — `dnbr` is an index (delta NBR), not a
                 # phenomenon. The categorical `severity` output (below) is
                 # the producer of registry/concepts/burn-severity.yaml.
                 # See OPERATIONS.md #outputs-concept-vs-index
                 # NOTE: registry/indices/dnbr.yaml does not yet exist
                 # (upstream Awesome Spectral Indices gap); slug is reserved.
    description: >
      dNBR raster, scaled ×1000 to match the MTBS convention
      (`(NBR_pre − NBR_post) * 1000`). Typical range −2000 to +2000;
      MTBS class breaks reference this scaled axis.
  - name: severity
    type: raster
    format: cog
    dtype: int8
    concept: burn-severity
    description: >
      Categorical MTBS burn-severity raster derived by applying
      `class_breaks` to the scaled dNBR. Codes follow
      `registry/concepts/burn-severity.yaml`:

        -1 = Increased greenness / regrowth (dNBR < -100)
         1 = Unburned to low (-100 to +99)
         2 = Low severity (+100 to +269)
         3 = Moderate severity (+270 to +439)
         4 = High severity (+440 to +659)
         5 = Increased greenness (post-fire) (≥ +660)

      NoData is encoded as the int8 sentinel `-128`. int8 is used so the
      legal class code `-1` (regrowth) can be represented natively.

validated_by:
  - "key-benson-2006"   # Key & Benson (2006), Landscape Assessment (FIREMON RMRS-GTR-164) — MTBS dNBR severity breaks (no DOI; citation key)

params:
  mode:
    type: string
    enum: [from-scenes, from-nbr]
    default: from-scenes
    description: >
      Input variant. `from-scenes` computes NBR internally from the NIR
      and SWIR2 bands of `pre` and `post`. `from-nbr` assumes `pre` and
      `post` are single-band NBR rasters already in [-1, 1].
  nir_band:
    type: string
    default: B8
    description: >
      Band name (or numeric index as string) for NIR in the pre/post
      scenes. Defaults to Sentinel-2 `B8`. Use `B5` for Landsat 8/9
      OLI, `nir` for the canonical Folia band alias, etc.
      Ignored when `mode: from-nbr`.
  swir2_band:
    type: string
    default: B12
    description: >
      Band name for SWIR2 in the pre/post scenes. Defaults to
      Sentinel-2 `B12`. Use `B7` for Landsat 8/9 OLI, `swir2` for the
      canonical Folia band alias. Ignored when `mode: from-nbr`.
  class_breaks:
    type: object
    description: >
      Override the MTBS class break thresholds (on the scaled ×1000
      dNBR axis). Defaults match Key & Benson (2006) / MTBS canonical
      values referenced by `registry/concepts/burn-severity.yaml`.
    default:
      regrowth_max: -100      # dNBR < -100        -> class -1
      unburned_max: 99        # -100 to 99         -> class  1
      low_max: 269            # 100 to 269         -> class  2
      moderate_max: 439       # 270 to 439         -> class  3
      high_max: 659           # 440 to 659         -> class  4
      # >= 660                                     -> class  5

granularity:
  min_resolution_m: 10
  max_resolution_m: 500
  recommended_resolution_m: 20
  output_resolution: inherit
  minimum_bands: 1
  requires_continuous: true
  suitable_aoi_km2:
    min: 0.1
    max: 100000
  realtime_max_km2: 100

uncertainty:
  error_model: propagated
  factors:
    - name: registration_error
      description: "Misalignment between pre and post scenes produces false dNBR signal"
      severity: high
      mitigations:
        - "Ensure both inputs are co-registered to sub-pixel accuracy"
        - "Use Sen2Cor / ARD products where geometric correction is enforced"
    - name: phenological_variation
      description: "Seasonal NBR variation can be confused with burn signal"
      severity: high
      mitigations:
        - "Use anniversary-date pre/post pairs where possible"
        - "Prefer composites over single-date scenes for the pre-fire reference"
    - name: cloud_and_shadow_contamination
      description: "Cloud/cloud-shadow pixels invalidate NBR and propagate to dNBR"
      severity: medium
      mitigations:
        - "Pre-mask with `analysis_cloud_mask` or scene-level QA bands"
        - "Reject pre/post pairs above a cloud-fraction threshold"
    - name: class_break_sensitivity
      description: "MTBS breaks are calibrated on Landsat / US ecoregions; transferability varies"
      severity: medium
      mitigations:
        - "Override `class_breaks` for non-US / Sentinel-2 calibrated thresholds"
        - "Validate against field burn-severity plots where available"
  limitations:
    - "Assumes radiometrically comparable inputs (surface reflectance, same sensor preferred)"
    - "MTBS class breaks are calibrated on Landsat dNBR; Sentinel-2 dNBR values are similar but not identical"

execution:
  realtime_max_km2: 100
  cost_per_km2: 0.001
  time_per_km2_sec: 0.3
  memory_profile: low
  profile:
    cpu: low
    memory: low
    io: medium
  scaling:
    model: linear
    parallelizable: true
    min_chunk_km2: 1

cache_policy:
  ttl_days: 30
  invalidate_on: [source_update]

# backends: audited 2026-08-14 (defect 50). NOTHING dispatches this op: no arm in folia-engine
# `dispatch_op`, no `registerOp`/OP_TABLE entry in packages/compute, no `_BUILTIN_OP_MAP`
# key in folia/compute.py, no backend manifest. Declared EMPTY on purpose — an absent
# block would be indistinguishable from one nobody ever wrote.
backends: {}

display_hints:
  map:
    renderer: maplibre
    type: raster
    palette: burn-severity
    colors: ["#1a9850", "#f7f7f7", "#fee08b", "#fdae61", "#f46d43", "#a50026"]
    labels: [regrowth, unburned, low, moderate, high, very-high]
  info:
    fields: [dnbr_value, severity_class]
    format: "{value}"
    section: Fire

ui:
  icon: flame
  color: "#D32F2F"

examples:
  - name: dNBR from Sentinel-2 pre/post scenes
    description: >
      Compute scaled dNBR + MTBS severity from a Sentinel-2 L2A pre/post
      pair (NIR = B8, SWIR2 = B12).
    inputs:
      pre: "s2_l2a_prefire_2023-06-15.tif"
      post: "s2_l2a_postfire_2023-08-20.tif"
    params:
      mode: from-scenes
      nir_band: B8
      swir2_band: B12

  - name: dNBR from pre-computed NBR rasters
    description: >
      Chain after `imagery_radiometric_indices` (or equivalent) to skip
      internal NBR computation. Use when NBR has already been cached.
    inputs:
      pre: "nbr_pre.tif"
      post: "nbr_post.tif"
    params:
      mode: from-nbr
