Skip to content

Repository files navigation

garry

R-CMD-check

garry is a lazy, spatial-aware raster engine for R. You describe a whole raster computation as a graph and it runs it fast: chunked, distributed across processes, with the numeric kernels JIT-compiled to XLA (CPU or GPU). It is built for cloud-native earth observation, turning a STAC search of remote COGs into an analysis-ready composite without leaving R.

What it is:

  • Lazy. Every operation (lazy_map(), focal(), reduce_over(), mask(), align()) adds a node to a computation graph and returns immediately. Nothing reads or computes until collect().
  • Spatial-aware. Arrays carry their CRS, transform and extent. Alignment is explicit: binary ops never silently resample, so a pixel is never quietly moved half a cell.
  • Fast. collect() plans the graph into chunks, streams reads through a GDAL daemon pool, and runs the fused kernels through anvl (XLA). On a three-band annual HLS composite it matches Python’s ODC + dask on the same machine, in idiomatic R.
  • GDAL-faithful. IO is gdalraster; grid math is vaster. Results track what GDAL would give you.

Installation

You can install the development version of garry from GitHub with:

# install.packages("pak")
pak::pak("belian-earth/garry")

Example

A cloud-masked annual median composite of HLS Sentinel-2 imagery, straight from the Microsoft Planetary Computer. The whole pipeline is lazy: it reads and computes only at the final collect().

library(garry)
#> garry: GDAL 3.13.0 "Iowa City", released 2026/05/04

# 1. Discover: HLS S30 over an AOI, all of 2023, pre-signed for the PC.
aoi <- c(-78.4, 24.4, -77.65, 24.8)   # lon/lat bounding box
src <- stac_query(
  bbox        = aoi,
  stac_source = "https://planetarycomputer.microsoft.com/api/stac/v1/",
  collection  = "hls2-s30",
  start_date  = "2023-01-01", end_date = "2023-12-31"
) |>
  stac_sign_mpc() |> 
  stac_filter_cloud(30) |> 
  stac_filter_assets(c("B04", "B03", "B02", "B08", "Fmask"))


# 2. The analysis grid, straight from the AOI: an equal-area (LAEA) grid at 30 m
#    centred on the bbox. No hand-picked EPSG or projected extent.
target <- grid_from_bbox(aoi, res = 30)

# 3. A read/compute daemon pool, auto-sized to the machine.
garry_daemons()

# 4. Build the pipeline. A `LazyDataset` holds every band over time; the verbs
#    apply across all bands at once. Still nothing has been read or computed.
composite <- lazy_dataset(
  src, 
  grid = target,
  assets = c("B04", "B03", "B02", "B08"), 
  mask_asset = "Fmask",
  nodata = c(B04 = -9999, B03 = -9999, B02 = -9999, B08 = -9999, Fmask = 255),
  resampling = "bilinear"
) |>
  mask(where = qa_bits(0:3), open = 2, dilate = 3) |>  # clouds/shadows + cleanup
  reduce_over("median", over = "t")       # per-band temporal median

# A derived band is just more graph: NDVI from the NIR/red composites. It joins
# the lazy pipeline like any other band, computed only at the final collect().
composite[["ndvi"]] <- (composite[["B08"]] - composite[["B04"]]) /
  (composite[["B08"]] + composite[["B04"]])

# Inspect the pipeline before running anything. print() summarises the dataset
# (bands + grid); draw() renders the IR: a LazyDataset as its ordered pipeline
# steps, a single band as its node tree.
print(composite)
#> ── <LazyDataset> ───────────────────────────────────────────────────────────────
#>   bands  B04 B03 B02 B08 ndvi
#>   time   44 slices
#>   grid   2536 x 1480 • f32
#>   crs    Lambert Azimuthal Equal Area
#>   graph  583 nodes • lazy
#>   ℹ draw(x) to see the pipeline
draw(composite)              # the dataset's pipeline steps
#> ── <LazyDataset> pipeline ──────────────────────────────────────────────────────
#>   ◈ source    B04 B03 B02 B08 ndvi  •  44 slices • 2536×1480 f32
#>   ✕ mask      from Fmask • bits 0–3 • open 2 • dilate 3
#>   ▸ reduce    median over t
#>   ⊕ derive    ndvi
#>   ─ 583 nodes • crs Lambert Azimuthal Equal Area
draw(composite[["ndvi"]])    # the NDVI band's node tree
#> ── <LazyRaster> 2536 x 1480 • f32 ──────────────────────────────────────────────
#> ƒ map  (2 inputs)
#> └─ ƒ map  (2 inputs)  ×2
#>    └─ ▸ median  over t  ×2
#>       └─ ⬚ stack  along t
#>          └─ ƒ map  (2 inputs)  ×44
#>             ├─ ◈ source  2536×1480 f32
#>             └─ ◫ focal  r=3
#>                └─ ◫ focal  r=2
#>                   └─ ◫ focal  r=2
#>                      └─ ƒ map
#>                         └─ ◈ source  2536×1480 f32

# preview() estimates a coarse grid from the graph and device, runs the pipeline
# at that reduced resolution (reading only overviews / the windows it needs), and
# plots the result -- a cheap look before committing to the full collect().
preview(composite[["ndvi"]])   # single band -> colour ramp + colourbar

# 5. Execute across the daemons. collect() returns a (y, x, band) array carrying
#    a `gis` attribute (extent/CRS), so the result is self-describing; pass a
#    `path=` to stream it straight to a GeoTIFF instead.
a <- collect(composite, nodata = -9999)

# preview() on a materialised array reads that `gis` attribute for real-world axes.
preview(a)     # multi-band  -> RGB (first three bands)

The same verbs work on a single raster. lazy_source() opens one COG or a GDAL mosaic; lazy_map() / focal() / reduce_over() build map-algebra graphs; align() reprojects onto a target grid; collect() runs it. See benchmarks/compare.sh for the back-to-back garry-vs-ODC benchmark.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages