# catalog/operations/analysis/landtrendr.yaml

id: analysis_landtrendr
name: LandTrendr Temporal Segmentation
description: >
  Temporal segmentation of annual spectral time series using the LandTrendr
  algorithm (Kennedy et al. 2010).  Identifies breakpoints in per-pixel
  trajectories to detect disturbance events (abrupt drops), recovery trends
  (gradual rises), and periods of stability.

  The algorithm fits piecewise linear segments to yearly composite values
  (e.g. NBR, NDVI), balancing goodness-of-fit against model complexity.
  Outputs include fitted values, vertex flags, RMSE, and per-segment
  metadata (magnitude, duration, rate of change).

  Two backends are available: a pure Python/NumPy implementation that runs
  on any array source (COGs, Zarr, GEE exports), and a GEE backend that
  wraps ee.Algorithms.TemporalSegmentation.LandTrendr.
# 2.0.0 (2026-07-01, sdk/001): canonical kernel is the external `landtrendr` crate
# (repo nthh/LT-rs), validated against LandTrendr-IDL and native GEE. Op identity:
# crate:landtrendr@79cd7d1992a5d71c99a6900c3436e10aadd947e6 (ADR-0065 D4). Defaults are
# the LT-GEE runParams (p_value 0.05, best_model 0.75 — legacy in-repo defaults were
# 0.01/1.25). The legacy Python/NumPy backend is KNOWN-DIVERGENT from the validated
# algorithm (83% of pixels on the frozen Bootleg golden differ under identical params)
# and retires with the strangler.
version: 2.0.0
op_identity: crate:landtrendr@79cd7d1992a5d71c99a6900c3436e10aadd947e6
category: analysis
type: raster-to-raster
# Recipes address this op as `landtrendr` (engine dispatch accepts both names).
aliases: [landtrendr]
default_implementation: native

inputs:
  - name: time_series
    type: raster_stack
    format: geotiff
    description: >
      Annual spectral composite stack with one band per year (n_years x height x width).
      Values should be a spectral index where higher = more vegetation (e.g. NBR, NDVI).
      If inverted, negate before passing.
    required: true
  - name: years
    type: integer[]
    description: >
      Array of observation years corresponding to the time axis.
      Must be sorted ascending with no duplicates.  Length must match
      the number of bands/layers in time_series.
    required: true

# Parametric temporal segmentation — agnostic to the underlying spectral
# index; consumes any annual time-series of any concept. requires: omitted.

outputs:
  - name: fitted
    type: raster_stack
    format: cog
    dtype: float32
    description: >
      Fitted (segmented) values for each year.  Same shape as input
      time_series.  Values between vertices are linearly interpolated.
  - name: is_vertex
    type: raster_stack
    format: cog
    dtype: uint8
    description: >
      Boolean flag (0/1) per pixel per year indicating breakpoint vertices.
  - name: rmse
    type: raster
    format: cog
    dtype: float32
    description: >
      Per-pixel root mean square error between source and fitted values.
  - name: segments
    type: json
    concept: disturbance-regime
    description: >
      Per-pixel segment metadata: start_year, end_year, magnitude, duration,
      rate.  Useful for thresholding disturbance/recovery events.

params:
  max_segments:
    type: integer
    default: 6
    description: >
      Maximum number of segments to fit.  The trajectory will have at most
      max_segments + 1 vertices (including endpoints).  Higher values capture
      more detail but risk overfitting.
  spike_threshold:
    type: number
    default: 0.9
    description: >
      Threshold for dampening ephemeral spikes (0 to 1).  1.0 = no filtering.
      Lower values apply more aggressive spike removal.  Spikes are single-year
      deviations that reverse in the following year.
  vertex_count_overshoot:
    type: integer
    default: 3
    description: >
      Number of extra candidate vertices beyond max_segments + 1 identified
      in the initial pass.  These are later culled by angle-based removal.
  recovery_threshold:
    type: number
    default: 0.25
    description: >
      Controls maximum allowable recovery rate.  A segment cannot fully recover
      in fewer than 1/recovery_threshold years.  E.g. 0.25 means recovery
      cannot complete in fewer than 4 years.  1.0 disables the constraint.
  p_value_threshold:
    type: number
    default: 0.05
    description: >
      P-value threshold for model acceptance during iterative simplification.
  best_model_proportion:
    type: number
    default: 0.75
    description: >
      During model selection, a more complex model is preferred if its score
      is within this proportion of the best (simplest acceptable) score.
      Lower values favor simpler models.
  min_observations_needed:
    type: integer
    default: 6
    description: >
      Minimum number of valid (non-NaN) observations required to run
      segmentation.  Pixels with fewer valid years are returned unfitted.
  prevent_one_year_recovery:
    type: boolean
    default: true
    description: >
      Disallow recovery segments with a duration of exactly one year.
      Prevents overfitting in environments where such rapid recovery
      is not ecologically realistic.

# backends: audited 2026-08-14 (defect 50). A key means a runtime that DISPATCHES this op —
# folia-engine `dispatch_op` (products/sdk/folia-engine/src/lib.rs), a `registerOp`/OP_TABLE
# entry in packages/compute, `_BUILTIN_OP_MAP` in folia/compute.py, or a backend manifest
# (folia/backends/*/backend.yaml).
backends:
  rust-engine:
    op: analysis_landtrendr
    function: landtrendr
    dispatch: products/sdk/folia-engine/src/lib.rs:1003
  rust-wasm:
    function: temporal_ops::landtrendr_flat
    handle_function: h_landtrendr
  js:
    function: landtrendr
  python:
    function: geo.imagery.landtrendr_disturbance
    dispatch: folia/compute.py _BUILTIN_OP_MAP
  gee:
    function: folia.backends.gee.ops.landtrendr
    dispatch: folia/backends/gee/backend.yaml

performance:
  reduction_class: reduce                # per plan-lock.md#placement — 4 summary bands out of an n-year stack
  throughput:                            # measured, never asserted (sdk/007)
    # px = input samples (f32 values), matching plan.rs px = est_input_bytes/4. Release
    # build; 32k pixels x 32-year trajectories, half carrying a disturbance vertex.
    native_px_per_s: 7807324
    measured: { on: "darwin-arm64 Apple M1 Max", date: 2026-07-01, via: "folia-compute tests/op_throughput.rs::op_throughput" }

cache_policy:
  ttl_days: 90
  invalidate_on: [source_update, param_change]

display_hints:
  map:
    renderer: maplibre
    type: raster
    palette: diverging
    colors: ["#d73027", "#f7f7f7", "#1a9850"]
    labels: [disturbance, stable, recovery]
  chart:
    renderer: observable-plot
    chartType: line
    x: year
    y: fitted_value
    series: pixel
  info:
    fields: [rmse, n_segments, greatest_disturbance_year, greatest_disturbance_magnitude]
    section: Temporal Analysis

views:
  - type: map
    title: Greatest Disturbance
    description: Year and magnitude of the largest disturbance segment per pixel
  - type: chart
    title: Pixel Trajectory
    description: Interactive time series plot showing source vs. fitted values with vertex markers

ui:
  icon: activity
  color: "#2E7D32"
