Skip to content

hypertidy/controlledburn

Repository files navigation

controlledburn

R-CMD-check

Rasterize polygons without materializing any pixel values. controlledburn computes exact coverage fractions for polygon-grid intersections and returns results as sparse tables: run-length encoded interior cells and individually weighted boundary cells.

The scanline algorithm is O(perimeter) in both time and memory.

Installation

remotes::install_github("hypertidy/controlledburn")

Usage

library(controlledburn)
library(geos)

poly <- as_geos_geometry("POLYGON ((1 1, 9 1, 9 9, 1 9, 1 1))")

# Sparse output — no dense matrix allocated
r <- burn_scanline(poly, extent = c(0, 10, 0, 10), dimension = c(20L, 20L))
r
#> <controlledburn> 20 x 20 grid, 1 geometry
#>   runs:  74 (256 interior cells)
#>   edges: 0 boundary cells
#>   sparsity: 36.0% empty
#> <controlledburn> 20 x 20 grid, 1 geometry
#>   runs:  N (interior cells)
#>   edges: N boundary cells

# Materialise only when you need it
mat <- materialise_chunk(r)

Default grid parameters

With no extent or dimension, controlledburn derives both from the geometry:

r <- burn_scanline(poly)
# extent from wk::wk_bbox(), 256 cells on the long axis

Or specify resolution:

r <- burn_scanline(poly, resolution = 0.5)

Geometry input

Accepts geos_geometry, sfc, wk::wkb(), blob, or raw WKB list (compatible with vapour/gdalraster):

Note that geos::as_geos_geometry() provides interchange for terra::vect().

Shared boundary complementarity

Adjacent polygons with shared edges produce complementary coverage fractions that sum to exactly 1.0 in every boundary cell — no gaps, no overlaps:

left  <- as_geos_geometry("POLYGON ((0 0, 5 0, 5 10, 0 10, 0 0))")
right <- as_geos_geometry("POLYGON ((5 0, 10 0, 10 10, 5 10, 5 0))")

r <- burn_scanline(c(left, right), extent = c(0,10,0,10), dimension = c(20L,20L))

# Coverage sums to 1.0 in every touched cell
mat1 <- materialise_chunk(r, id = 1)
mat2 <- materialise_chunk(r, id = 2)
max(mat1 + mat2)
#> [1] 1
#> [1] 1

Output format

burn_scanline() and burn_sparse() return a list with class "controlledburn" containing:

  • runs: data.frame(row, col_start, col_end, id) — interior cells with coverage = 1.0, run-length encoded by row.
  • edges: data.frame(row, col, weight, id) — boundary cells with exact partial coverage in (0, 1).
  • extent: c(xmin, xmax, ymin, ymax)
  • dimension: c(ncol, nrow)

This is the natural output of scanline rasterization — no dense matrix is allocated until materialise_chunk() is called.

Performance

Scanline algorithm scales with perimeter, not area:

Shape Resolution Scanline Dense (burn_sparse) Speedup
Star 3200×3200 13 ms 225 ms 17×
Jagged 3200×3200 15 ms 136 ms
NC counties 2000×800 29 ms 61 ms

Memory for real-world grids (CGAZ at 32K×16K, ~500M cells): ~50 MB sparse vs ~2 GB dense.

Fast

v <- new(gdalraster::GDALVector, "/vsicurl/https://github.com/mdsumner/geoboundaries/releases/download/latest/geoBoundariesCGAZ_ADM0.parquet")
v$returnGeomAs ## WKB
#> [1] "WKB"
gcol <- v$getGeometryColumn()
v$setIgnoredFields( setdiff(v$getFieldNames(), gcol))
wkbgeom <- wk::wkb(v$fetch(-1)[[gcol]])
v$close()

system.time(burn_scanline(wkbgeom))
#>    user  system elapsed 
#>   0.524   0.015   0.538

system.time(r1 <- burn_scanline(wkbgeom, dimension = c(8192, 4096)))
#>    user  system elapsed 
#>   0.953   0.000   0.953
str(r1)
#> List of 4
#>  $ runs     :'data.frame':   81149 obs. of  4 variables:
#>   ..$ row      : int [1:81149] 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 ...
#>   ..$ col_start: int [1:81149] 5712 5706 5706 5704 5704 5703 5702 5702 5701 5699 ...
#>   ..$ col_end  : int [1:81149] 5712 5712 5715 5717 5719 5719 5719 5719 5718 5718 ...
#>   ..$ id       : int [1:81149] 1 1 1 1 1 1 1 1 1 1 ...
#>  $ edges    :'data.frame':   469461 obs. of  4 variables:
#>   ..$ row   : int [1:469461] 1065 1066 1066 1066 1066 1066 1066 1066 1067 1067 ...
#>   ..$ col   : int [1:469461] 5712 5707 5708 5709 5710 5711 5712 5713 5705 5706 ...
#>   ..$ weight: num [1:469461] 0.0126 0.1343 0.0792 0.296 0.1872 ...
#>   ..$ id    : int [1:469461] 1 1 1 1 1 1 1 1 1 1 ...
#>  $ extent   : num [1:4] -180 180 -90 83.6
#>  $ dimension: int [1:2] 8192 4096
#>  - attr(*, "class")= chr "controlledburn"

Really fast.

system.time(r1 <- burn_scanline(wkbgeom, dimension = c(8192, 4096) * 20))
#>    user  system elapsed 
#>  17.005   0.625  17.629
pryr::object_size(r1)
#> 278.56 MB
tibble::as_tibble(r1$runs)
#> # A tibble: 2,402,331 × 4
#>      row col_start col_end    id
#>    <int>     <int>   <int> <int>
#>  1 21300    114229  114229     1
#>  2 21301    114228  114231     1
#>  3 21302    114227  114232     1
#>  4 21303    114226  114232     1
#>  5 21304    114225  114232     1
#>  6 21305    114224  114232     1
#>  7 21306    114210  114215     1
#>  8 21306    114221  114234     1
#>  9 21307    114209  114235     1
#> 10 21308    114209  114236     1
#> # ℹ 2,402,321 more rows
tibble::as_tibble(r1$edges)
#> # A tibble: 12,006,186 × 4
#>      row    col   weight    id
#>    <int>  <int>    <dbl> <int>
#>  1 21299 114228 0.49016      1
#>  2 21299 114229 0.52762      1
#>  3 21299 114230 0.029348     1
#>  4 21300 114227 0.14377      1
#>  5 21300 114228 0.91647      1
#>  6 21300 114230 0.95528      1
#>  7 21300 114231 0.61698      1
#>  8 21300 114232 0.36007      1
#>  9 21301 114226 0.44189      1
#> 10 21301 114227 0.95691      1
#> # ℹ 12,006,176 more rows
r1[c("extent", "dimension")]
#> $extent
#> [1] -180.00000  180.00000  -90.00000   83.63339
#> 
#> $dimension
#> [1] 163840  81920

History

controlledburn was derived from fasterize by Noam Ross (EcoHealth Alliance), removing Armadillo and raster package dependencies to return bare scanline indexes.

Version 0.1.0 is a complete rewrite using the exactextract algorithm (Daniel Baston, vendored from exactextractr) for exact coverage fractions via a new O(perimeter) scanline sweep.

See also: vaster for grid cell indexing, exactextractr for raster extraction with polygon weights.

Code of Conduct

Please note that the controlledburn project is released with a Contributor Code of Conduct. By contributing to this project, you agree to abide by its terms.

About

don't materialize, just rasterize!

Topics

Resources

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •