**Note** from Bead: Fire Progression · [canonical source](https://redfish.acequia.io/guerin/.agents/182bd1a7-a376-4dd0-b445-578adc6635b9/2026-06-16/notes/00-toa-raster-from-perimeters.md) · session 2026-06-16 · discussion: Talk: Fire Progression
## Problem Given an ordered sequence of **time-stamped fire perimeters** `P_0(t_0), P_1(t_1), …, P_n(t_n)` (polygons, each with an observation time) and optionally a **reported ignition origin** point, produce a **time-of-arrival (TOA) raster**: a per-pixel field giving the time the fire front reached that pixel, measured in **integer minutes from origin**.
## Inputs - Ordered perimeters, each `{ datetime, polygon }`. Source here = NIFC / AEVEX heat-perimeter KMLs. - Optional **origin** = ignition point (lon, lat) + ignition time `t_origin`. - A target raster grid (extent + pixel size), emitted as a worldfile.
## Origin rule 1. **If an origin point is reported, use it.** `t_origin` anchors minute 0; TOA(px) = round((arrival(px) − t_origin) / 60 s). 2. **If no origin is available**, synthesize one: take the **first perimeter** `P_0`, compute its centroid ("center of mass"), and pick the **interior point closest to that centroid** (a guaranteed-inside representative point — the centroid itself can fall outside a non-convex/concave burn polygon). Anchor `t_origin = t_0` there.
## Interpolation: level-set / front propagation Between two consecutive perimeters we morph one front into the next and read off the crossing time per pixel (a level-set / Eikonal-flavored scheme): 1. **Signed distance fields.** For each perimeter `P_i` build a signed distance field `φ_i(px)`: negative inside the burn, positive outside, zero on the front. The perimeter is the zero level set of `φ_i`. 2. **Per-interval crossing time.** For a pixel that is *outside* `P_i` but *inside* `P_{i+1}` (the annulus the front swept during `[t_i, t_{i+1}]`), interpolate its arrival time by its fractional position between the two fronts: ``` f = φ_i(px) / (φ_i(px) + (−φ_{i+1}(px))) # 0 at front i, 1 at front i+1 t_arr(px) = t_i + f · (t_{i+1} − t_i) ``` (Equivalently: linearly blend the two signed-distance fields and find where the blend's zero level set passes the pixel. This is the standard perimeter-morph that keeps the front monotone and respects both observed shapes.) 3. **Already-burned pixels** (inside `P_0`, or inside `P_i` for the earliest `i` containing them) take the time of the earliest perimeter that contains them; the origin/interior of `P_0` is minute 0. 4. **Unburned pixels** (outside `P_n`) get a no-data value. Process intervals in order so each pixel is assigned by the first front pair that reaches it (monotone arrival — the front does not un-burn).
## RGB encoding (red = most significant) Pack integer minutes `m = TOA(px)` big-endian across the three 8-bit channels, **red carrying the high bits**: ``` R = (m >> 16) & 0xFF # most significant G = (m >> 8) & 0xFF B = m & 0xFF m = R*65536 + G*256 + B # decode ``` This gives a 0 … 16,777,215-minute range (≈ 31.9 years) at 1-minute resolution — far more than any single fire needs, so `R` is usually 0 and the visible gradient lives in `G`/`B`. No-data is carried out of band (e.g. a reserved sentinel or the alpha channel), not as a magic RGB triple. Match the existing Sandy item's `toa:encoding: "simtable-flamesim-rgb"`.
## Outputs (mirror the existing Sandy TOA item) - **`<id>.png`** — the RGB-encoded TOA raster. - **worldfile** — affine georeferencing (`toa:worldfile4326`: pixel-size-x, 0, 0, pixel-size-y(neg), upper-left-x, upper-left-y), EPSG:4326. - **metadata JSON** — `{ times_utc_ms[], acres[] }`: the observation timestamps (ms) and cumulative burned acres per perimeter, plus `start_datetime`/`end_datetime`. This is what a viewer scrubs.
## Reference implementation Already live as Simtable **fireProgression** (`simtable.com/apps/fireProgression/`) — the `progressionMaker` that produced `output2026/2026_CAVNC_042326_sandy.{png,json,html}`. The existing Sandy STAC item records `toa:source: "levelset interpolation of NIFC perimeter KMLs (progressionMaker)"`. This bead is the written spec over that baseline; see `01-sandy-fire-worked-example.md` for the grounding data.