---
title: "Architecture: how controlledburn developed"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Architecture: how controlledburn developed}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

## Origins: fasterize

controlledburn began as a fork of
[fasterize](https://github.com/hypertidy/fasterize) by Noam
Ross and EcoHealth Alliance. fasterize was a fast polygon rasterizer
for R: given a set of polygons and a raster template, it produced a
materialised raster using a scanline algorithm based on Wylie et al.
(1967). Its key properties:

- **Cell-centre rule**: a cell is "inside" the polygon if the cell
  centre falls inside the polygon boundary. Binary in/out, no coverage
  fractions.
- **Pixel functions**: Sum, First, Last, Min, Max, Count, Any —
  applied per-cell across overlapping polygons.
- **Dense output**: always materialised a full raster object in memory.
- **Dependencies**: Rcpp, sf (for geometry input), Armadillo (for the
  raster matrix).

fasterize was fast — much faster than `raster::rasterize()` — but it
required a dense raster template and could not express partial
coverage at polygon boundaries.

## The sparse insight

controlledburn's founding insight was that the scanline sweep could
emit **sparse run-length encoded output** instead of filling a dense
matrix. For a polygon on a grid, most cells are either fully inside
(runs of consecutive columns) or fully outside. Only the boundary
cells need individual attention. The output:

- **`$runs`**: `(row, col_start, col_end, id)` — interior cells, full
  coverage. One record per horizontal span.
- **`$edges`**: `(row, col, fraction, id)` — boundary cells with a
  coverage fraction in (0, 1).

This is O(perimeter) in both time and memory, rather than O(area).
A small polygon on a 100,000 × 100,000 grid produces a handful of
run records, not 10 billion pixels.

## Integrating exactextract

The exact coverage fractions came from Daniel Baston's
[exactextract](https://github.com/isciences/exactextract) C++
library. The algorithm traces polygon rings through grid cells,
computing analytically exact sub-pixel coverage fractions — not
sampling, not approximation. It was the gold standard for zonal
statistics.

The first integration vendored exactextract's C++ code into
controlledburn. This brought exact fractions but also brought the
**GEOS C API** as a hard dependency. exactextract's
`raster_cell_intersection()` function took `GEOSGeometry*` pointers
and called GEOS functions throughout: `GEOSGetExteriorRing_r`,
`GEOSCoordSeq_getXY_r`, `GEOSisEmpty_r`, etc.

The pipeline at this stage:

```
WKB bytes (from R)
  → GEOS WKBReader → GEOSGeometry*
    → exactextract::raster_cell_intersection(grid, ctx, geom)
      → dense Raster<float> (bounding-box-sized matrix)
    → dense_to_sparse() → runs + edges tables
```

This was `burn_sparse()`. It worked, but it allocated a dense
intermediate for each geometry's bounding box, required GEOS at
runtime, and only handled polygons.

## Replacing GEOS: the native core

The next major step was replacing every GEOS dependency with native
C++17 code. This happened in stages:

1. **WKB parser** (~230 lines): reads ISO WKB and EWKB, both byte
   orders, silently skips Z/M ordinates. No external dependencies.
   Replaces `GEOSWKBReader_read_r`.

2. **Ring walker**: walks polygon rings through the grid cell-by-cell,
   tracking entry/exit sides and building traversal coordinate lists.
   Replaces exactextract's GEOS-dependent ring iteration
   (`GEOSGetExteriorRing_r`, `GEOSGetInteriorRingN_r`,
   `GEOSCoordSeq_getXY_r`).

3. **Shoelace orientation**: determines ring winding (CCW vs CW) via
   the signed-area formula. Replaces `geos_is_ccw`.

4. **Coordinate min/max envelopes**: simple loop over coordinates.
   Replaces `GEOSEnvelope_r` and the component-box queries.

The analytical coverage math — the actual geometric computation of
how much of a cell is covered by a polygon edge — was kept from
exactextract. These files are pure computational geometry with no
GEOS dependency: `cell.cpp`, `traversal.cpp`, `traversal_areas.cpp`,
`box.cpp`, `grid.cpp`, `coordinate.cpp`, `side.cpp`, `measures.cpp`,
`perimeter_distance.cpp`. Nine files, vendored in
`src/exactextract/`.

The result was `burn()` (originally `burn_scanline()`), a
GEOS-free scanline engine producing the same sparse output.

## Unified geometry rasterization

With the native core in place, the engine was extended beyond polygons
to handle all geometry types through a single `burn()` entry point:

- **Polygons** produce `$runs` (interior cells, full coverage) and
  `$edges` (boundary cells, exact coverage fractions).
- **Lines** produce `$lines` with `(row, col, length, id)` — the
  absolute length of line within each cell, in CRS units.
- **Points** produce `$points` with `(row, col, id)` — no measure
  column (a point either falls in a cell or it doesn't).

Each table's measure means exactly one thing. No semantic confusion
from mixing polygon fractions with line lengths in a shared column.

## Two burn modes

The `mode` parameter on `burn()` selects between two code paths:

### Coverage mode (default)

Uses the full exactextract walker to compute exact coverage fractions
for every boundary cell. The walker traces each ring through the grid,
building coordinate-level traversal records, then computes analytical
cell-intersection areas. O(perimeter) time and memory.

This is the mode to use when exact area conservation matters — the
total burned coverage equals the polygon's true area to floating-point
precision.

### Approx mode

Reimplements the fasterize cell-centre rule with a dedicated
lightweight sweep that **bypasses the walker entirely**. For each
polygon edge, it computes x-intercepts at each row's scanline
y-midpoint, accumulates winding numbers per row, and sweeps
left-to-right to emit runs. ~120 lines of C++.

No traversal coordinates, no `CellRecord` objects, no
`BoundaryCellRecord` bookkeeping — just edge-row intersections and
winding arithmetic. The output is runs only, no `$edges` for
polygons.

The boundary convention is left-inclusive (a cell whose centre falls
exactly on a polygon edge is classified as "inside"), matching
fasterize's behaviour. On constructed geometries (aligned rectangles,
offset rectangles, triangles, holes), approx mode produces
cell-for-cell identical results to fasterize.

## Performance

Benchmarked on CGAZ (218 country polygons, 10.1M vertices):

| Grid | Cells | cb approx | fasterize | Winner |
|------|-------|-----------|-----------|--------|
| 256 × 128 | 33K | 1.0s | 0.5s | fasterize |
| 4096 × 2048 | 8.4M | 1.1s | 0.4s | fasterize |
| **16384 × 8192** | **134M** | **1.2s** | **2.8s** | **cb** |
| 32768 × 16384 | 537M | 1.4s | 9.7s | cb |
| 65536 × 32768 | 2.1B | 1.7s | OOM | cb only |
| 131072 × 65536 | 8.6B | 2.4s | OOM | cb only |

The crossover is around 134 million cells. Below that, fasterize's
simpler per-cell cost wins. Above it, controlledburn's sparse output
and O(perimeter) memory win decisively — fasterize runs out of memory
above ~2 billion cells, while controlledburn reaches 8.6 billion in
2.4 seconds.

## Current dependency tree

After removing `burn_sparse()` and the GEOS dependency:

- **C++ core** (`cpp/`): zero external dependencies. Pure C++17.
- **R package**: `cpp11` (LinkingTo), `wk` (Imports). No GEOS, no sf,
  no Armadillo.
- **Python package**: pybind11 + scikit-build-core. No GEOS.
- **Vendored exactextract**: 9 GEOS-free analytical geometry files
  (coverage fractions, box intersections, traversal areas).

## The three surfaces

The C++ core is canonical. R and Python are thin bindings:

```
                     ┌──────────────────────┐
                     │    C++ core (cpp/)    │
                     │  scanline engine      │
                     │  WKB parser           │
                     │  exactextract subset  │
                     │  materialize.hpp      │
                     └─────────┬────────────┘
                               │
               ┌───────────────┼───────────────┐
               │               │               │
        ┌──────▼──────┐ ┌─────▼──────┐ ┌──────▼──────┐
        │  R package  │ │   Python   │ │   C++ lib   │
        │  cpp11 shim │ │  pybind11  │ │  CMake      │
        │  burn()     │ │  burn()    │ │  burn()     │
        └─────────────┘ └────────────┘ └─────────────┘
```

`tools/sync-core.sh` copies the canonical C++ source from `cpp/` into
the R package's `src/` and `inst/include/` directories. The Python
package's `CMakeLists.txt` references `cpp/` directly.

Shared parity fixtures in `fixtures/` (CSV with WKT, WKB hex, and
expected results) are read by test suites in all three languages,
ensuring cross-language consistency.

## Acknowledgements

- **Noam Ross** and **EcoHealth Alliance**: the original fasterize
  scanline algorithm.
- **Daniel Baston** and **ISciences, LLC**: the exactextract C++
  analytical coverage algorithm, vendored under Apache-2.0.
- The boundary handling work was informed by discussion in
  [hypertidy/fasterize#6](https://github.com/hypertidy/fasterize/issues/6)
  and [OSGeo/gdal#14615](https://github.com/OSGeo/gdal/issues/14615).
  Dan Baston has since folded the exactextract logic into GEOS itself.
