# catalog/operations/analysis/zonal_stats.yaml
#
# Zonal statistics. CANONICALIZED 2026-07-18 (ENGINE_PLAN.md §3 W3.3, engine-v0/zonal-stats-vector,
# ADR-0036 D2/D6) to describe the ACTUAL engine contract: the Rust dispatch arm (folia-engine
# zonal::zonal_stats_op -> folia_compute_core::stats_ops::zonal_stats_multi) takes ONE raster edge +
# ONE vector (table) zones edge and returns ONE vector-backed table (Data::Table) — the COMPLETE
# original zone row + geometry + CRS pass through, with appended {band}_{stat} statistic columns.
# The physical value is a table; its governed public value kind is `vector` (a table WITH a WKB
# geometry column IS a vector layer, ADR-0036 D1).
#
# SIGNATURE (ADR-0036 D6): raster-vector-to-vector. The output DOMAIN is `Table`, selected from this
# op's OUTPUT signature independent of input ordering (ENGINE_PLAN §8-C): zonal_stats is NEVER keyed
# as Grid merely because its first input is a raster.

id: zonal_stats
# ADR-0036 D6 prefix-drop: the governed prefixed name resolves to the ONE identity. The legacy
# `analysis_zonal_stats` alias keys converge onto builtin:zonal_stats@2 (a single canonicalization,
# not a second op — enforced by the op-identity coupling test in tests/lock_key.rs).
aliases: [analysis_zonal_stats]
name: Zonal Statistics
description: >
  Aggregate raster values within each polygon zone, appending the requested statistics as columns
  to the zone table. One pass over all raster bands: each zone is rasterized once and its covered
  pixel set is reused across every band. Linear bands use ordinary Euclidean accumulators; circular
  bands (e.g. harmonic phase) use circular mean/std on a declared period. For a GOVERNED labeled
  tensor edge the band names + linear/circular value spaces come from the value's OWN descriptors
  (WS-0B); a plain single-band raster keeps the legacy linear shorthand. Over a binary {0,1} mask,
  `mean` equals the covered-area fraction and `count` is exact.
version: 2.0.0
# ADR-0065 D4. @1 was the initial one-pass multi-band/circular arm keyed by a positional
# `band_contract`. @2 (greenfield re-baseline 2026-07-20, review round 6) RETIRES that param: a
# tensor edge's band names + value spaces derive from the value's descriptors (WS-0B), so the
# governed schema changed (band_contract removed, `stats` becomes a per-band map on a tensor edge).
# The op is downstream of harmonic_fit@1, so this key movement is sanctioned + declared.
op_identity: builtin:zonal_stats@2
category: analysis
type: raster-vector-to-vector
default_implementation: native

inputs:
  - name: raster
    type: tensor|raster
    format: ftn1
    description: >
      The values to aggregate. A GOVERNED labeled tensor (e.g. a harmonic_fit `[band,y,x]` phenology
      tensor ordered amplitude, phase, [trend], rmse) carries its band NAMES + linear/circular value
      spaces on its descriptors, so no positional band_contract is needed (WS-0B). A plain SINGLE-band
      raster keeps the legacy linear `stats` shorthand; a MULTI-band unlabeled raster has no
      discoverable band names and is a named error.
    required: true
  - name: zones
    type: vector
    format: geojson
    aliases: [vector]
    description: >
      Zone polygons (a Data::Table with a WKB geometry column). Polygon and MultiPolygon (including
      holes) are supported; a null/empty geometry yields count 0 and null value stats. The zones'
      CRS is reprojected to the raster CRS for rasterization; the ORIGINAL geometry/CRS pass through
      to the output unchanged.
    required: true

# Structural: works on any raster + polygon-vector pair, emits stats of whatever concept the raster
# carries. requires: omitted.

outputs:
  - name: stats
    # Physical Data::Table; public value kind `vector` (ADR-0036 D1). Output DOMAIN = Table (§8-C).
    type: vector
    format: geoparquet
    description: >
      The complete original zone table (every property column + WKB geometry + CRS) with appended
      {band}_{stat} Float64 columns, in band-major then stat order. A null stat (no covered valid
      pixel) is null in that column. Source license labels ride executor provenance
      (LayerMeta / the named-output envelope), NEVER a table column.

params:
  # WS-0B: `stats` has TWO shapes selected by the raster edge kind.
  #   - a plain SINGLE-band raster: a flat stat LIST applied to the one band (named `band0`).
  #   - a GOVERNED labeled tensor: a per-band MAP `{band_name: [stat, ...]}` — the band names come
  #     from the tensor's descriptors (NOT re-declared) and a band absent from the map defaults to
  #     `[mean]`. The positional `band_contract` param is RETIRED; the value spaces (linear/circular
  #     + phase period) travel WITH the value, so a circular band still rejects min/max/sum/median.
  stats:
    type: [array, object]
    default: [mean, min, max]
    description: >
      Requested statistics: count, min, max, mean, sum, std, median. For a single-band raster, a
      flat list on the one band. For a labeled tensor edge, a MAP {band_name: [stat, ...]} keyed by
      the tensor's descriptor band names (default [mean] for an unnamed band); a circular band (e.g.
      phase) accepts only circular mean/std/count. A positional `band_contract` on a tensor edge is a
      named error (retired in WS-0B — the band labels + value spaces ride the value's descriptors).

cache_policy:
  ttl_days: 30
  invalidate_on: [source_update, zones_change]

# 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:
  # ADR-0067 one-kernel rule: the engine arm, PyO3 zonal_stats, and wasm h_zonal_stats all share
  # folia_compute_core::stats_ops::zonal_stats_multi (the one-pass multi-band/circular core).
  rust-native:
    function: stats_ops::zonal_stats_multi
  rust-wasm:
    function: stats_ops::zonal_stats_multi
    handle_function: h_zonal_stats
  js:
    function: analysis_zonal_stats
  python:
    function: geo.stats.zonal_stats
    dispatch: folia/compute.py _BUILTIN_OP_MAP

display_hints:
  map:
    renderer: maplibre
    type: fill
  table:
    renderer: tanstack-table
  chart:
    renderer: observable-plot
    chartType: bar

ui:
  icon: pie-chart
  color: "#6A5ACD"
