# catalog/operations/raster/calc.yaml

id: raster_calc
name: Band Math
description: >
  Apply a band math expression to ONE raster: bands bind positionally as
  b1..bN in the input's band order, and scalar sibling params bind by name.
  There are no additional named raster inputs — two-raster arithmetic (a
  dNBR, a change ratio) composes as `stack_concat` (stack the pair into one
  2-band raster) followed by this op over `b1`/`b2`; see stack_concat.yaml
  and the proven end-to-end composition in
  products/sdk/folia-engine/tests/stack_concat.rs.
version: 1.0.0
category: raster
type: raster-to-raster
default_implementation: native

inputs:
  - name: raster
    type: raster
    format: geotiff
    description: >
      The ONE input raster (bands accessible as b1, b2, etc.). The engine reads
      this edge under the name `data` or `raster` and no other; extra named
      raster edges are never bound. Stack multiple rasters with stack_concat
      first when the expression spans layers.
    required: true

requires: {}  # structural — parametric band math on any raster

outputs:
  - name: result
    type: raster
    format: cog
    dtype: float32
    description: Result of band math expression

params:
  expression:
    type: string
    required: true
    description: >
      Band math expression. Examples:
      - "(b1 - b2) / (b1 + b2)" for NDVI-like index
      - "b1 * 0.1" for scaling
      - "where(b1 > 0, b1, 0)" for conditional
      Available: b1, b2, ..., sqrt, log, exp, sin, cos, abs, min, max, where, np

      This is the ONE canonical spelling. `formula:` was never a param of this op —
      it was drift in shipped recipes (23 files, converted 2026-08-06) that no backend
      ever read, so those layers computed against an ABSENT expression.
  output_band:
    type: string
    required: false
    description: >
      Label for the single output band. Documentation only on the Rust engine, which
      always emits one unnamed band and explicitly drops this from layer key material
      (`lock.rs::effective_params`) so naming a band can never churn a cache key. The JS
      runner uses it to name the band for downstream by-name reference. Declared here
      because 36 shipped recipes pass it and its absence from this schema is what made
      it read as a rogue dialect.

expression_functions: _functions.yaml

# 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: raster_calc
    function: raster_calc_op
    dispatch: products/sdk/folia-engine/src/lib.rs:905
  rust-wasm:
    function: core_ops::raster_calc
    mode: bytecode
  js:
    function: raster_calc
    mode: function-body
  python:
    function: geo.raster.calc
    dispatch: folia/compute.py _BUILTIN_OP_MAP

granularity:
  min_resolution_m: 0.1
  max_resolution_m: 100000
  output_resolution: inherit
  requires_continuous: false
  suitable_aoi_km2:
    min: 0.001
    max: 500000
  realtime_max_km2: 300

execution:
  realtime_max_km2: 300
  cost_per_km2: 0.001
  time_per_km2_sec: 0.5
  memory_profile: medium
  profile:
    cpu: medium
    memory: medium
    io: medium
  scaling:
    model: linear
    parallelizable: true
    min_chunk_km2: 1

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

display_hints:
  map:
    renderer: maplibre

ui:
  icon: code
  color: "#FF8C00"
