Skip to content

Brazil Data Cube Integration

BDC grids and tiles

The Brazil Data Cube (BDC) partitions Brazil into hierarchical tiles. DisSCube represents this with:

  • Master Grid: definition of the resolution and CRS for the whole country.
  • Tiles: a SpatialSource carrying the bbox of each partition.

Registering BDC grids and tiles

The bdc_importer utility registers the national simulation grids (BR/5km, BR/1km) and each BDC tile as a SpatialSource in the catalog. The BDC Grid V2 shapefiles are bundled with DisSCube (disscube/data/bdc_grids/), so no download is needed:

from disscube.utils.bdc_importer import import_bdc_grids

import_bdc_grids(cube)   # bundled BDC_SM_V2, BDC_MD_V2, BDC_LG_V2

To use another copy of the grids, pass sm_path, md_path and/or lg_path (any path or zip:// URL fiona can open).

Level Tiles Tile size Source ID
SM 871 105.6 km (~1°) BDC_SM_<tile>
MD 242 211.2 km (~2°) BDC_MD_<tile>
LG 75 422.4 km (~4°) BDC_LG_<tile>

Tile IDs repeat across levels (e.g. 005004 exists in both SM and MD), so a tile is identified by level and ID. Provenance, checksums and licensing of the bundled files are documented in disscube/data/bdc_grids/README.md; the files are © INPE and are not covered by DisSCube's MIT license.

Tiles index geometry, not data

bdc_importer indexes the BDC grid and its tiles (geometry and metadata). The registered tile SpatialSources have a placeholder asset_url (data/bdc/<LEVEL>/<tile>.tif) and cannot be loaded as raster data. To bring BDC data in, use disscube.sources.bdc (next section), which writes local GeoTIFFs you register as ordinary sources.

Reading data cubes via STAC

The BDC publishes analysis-ready 16-day data cubes — LANDSAT-16D-1 (30 m, from 1990), S2-16D-2 (10 m, from 2017) and CBERS4-WFI-16D-2 (64 m, from 2016) — as Cloud-Optimized GeoTIFFs indexed by a public STAC API (https://data.inpe.br/bdc/stac/v1/). Each item carries spectral bands and ready-made indices (NDVI, EVI; NBR for Sentinel-2). disscube.sources.bdc reads only the pixels of an area of interest, so an area of a few kilometres costs a few HTTP range requests per item instead of a full tile download. Searching needs pystac-client (pip install disscube[bdc]).

from disscube.sources import normalized_difference, write_geotiff
from disscube.sources.bdc import (
    BDC_INDEX_SCALE, fetch_composite, read_composite, search_items,
)

bbox = (-44.35, -2.62, -44.20, -2.47)          # WGS84
period = "2020-07-01/2020-09-30"

# one asset, one call: search → windowed reads → per-pixel median → GeoTIFF
fetch_composite("LANDSAT-16D-1", "NDVI", bbox, period, "raw/ndvi.tif",
                scale=BDC_INDEX_SCALE)            # indices are int16 × 10 000

# several assets of the same items: search once, derive an index
items = search_items("LANDSAT-16D-1", bbox, period)
green = read_composite("LANDSAT-16D-1", "green", bbox, period, items=items)
swir = read_composite("LANDSAT-16D-1", "swir16", bbox, period, items=items)
write_geotiff(normalized_difference(green, swir), "raw/mndwi.tif")   # MNDWI

Registering with provenance

register_bdc_source() does the fetch and the registration in one call, and keeps the record of where the data came from:

from disscube.sources.bdc import BDC_INDEX_SCALE, register_bdc_source

src = register_bdc_source(
    cube, "ndvi_2020", "LANDSAT-16D-1", "NDVI", bbox, period, out_dir="raw",
    scale=BDC_INDEX_SCALE,
)
# raw/ndvi_2020.tif               the composite
# raw/ndvi_2020.provenance.json   STAC URL, collection, asset, bbox, period,
#                                 reducer, scale/offset, items (id, dates and
#                                 interval when declared, URL), checksum,
#                                 retrieval time, software versions (disscube,
#                                 rasterio, GDAL, numpy, pystac-client)

The source is registered with the SHA-256 of the file as checksum, the first year of the period as time, and tags naming the collection, asset, period and the provenance file. The checksum enters the spec_hash of every derivation from that source: fetching another period (or with another reducer or scale) produces a new file, a new checksum and a new product, while re-fetching the same data is a cache hit. For a layer computed from several assets — an index such as MNDWI — build the Window2D yourself and call disscube.sources.register_raster(cube, source_id, window, out_dir, provenance); see the ilha_maranhao case study in the recipes repository (cases/ilha_maranhao).

What the reader does for you:

  • Physical units. Nodata becomes NaN, and a scale/offset is applied: the one passed as scale=/offset=, else the one declared by the asset (STAC raster:bands), else the GeoTIFF's own. The BDC cubes declare none, so pass scale=BDC_INDEX_SCALE (1e-4) for NDVI, EVI and NBR; without it, raw int16 values are returned. Ratios of bands (e.g. MNDWI from green and swir16) do not need it, since a common scale factor cancels out.
  • Composites and mosaics. Items are reduced tile by tile (median, mean, max or min, ignoring NaN, i.e. clouds); when the area spans several BDC tiles, the per-tile composites are pasted into one layer (the tiles share one pixel mesh, so nothing is resampled).
  • A portable CRS. The cubes declare BDC Albers as EPSG:10857, a code registered in 2023 and missing from older PROJ databases. Outputs are written with the proj4 definition in disscube.utils.grids.BDC_CRS instead, which every PROJ version reads.

Not included

Land-use/land-cover classification products are not published in this STAC catalog, so the cubes serve as observations (vegetation and water indices, reflectance) rather than as land-use maps. Derived variables are not published back as STAC.

Per-tile derivation

from disscube.models import SpatialDerivation, Variable

derivation = SpatialDerivation(
    source_id="slope_brazil",
    grid_id="BR/5km",
    role="driver",
    variables=[Variable(name="slope", operator="mean")],
)

# Process one tile
cube.derive(derivation, tile_id="009002")

# Process all SM tiles in a loop
# Tiles are registered with IDs in the format BDC_SM_<tile> (e.g. BDC_SM_009002)
tiles = [s for s in cube.catalog.list_spatial_sources() if s.id.startswith("BDC_SM_")]
for tile_source in tiles:
    tile_id = tile_source.id.split("_")[-1]
    cube.derive(derivation, tile_id=tile_id)

Each tile is processed independently and can be parallelized.

Loading a tiled result

# A specific tile — always works
da = cube.load("slope", tile_id="009002")

# By grid — works only when the result has a single tile
da = cube.load("slope", grid_id="BR/5km")

Multi-tile loading

load() without tile_id raises ValueError when multiple tiles of the same variable exist on the same grid. Automatic mosaicking is not implemented. Always pass tile_id in multi-tile workloads.

National 100 m grid

For projects that need a finer resolution than the BR/1km simulation grid, DisSCube supports a custom national 100 m grid. BDC tiles can still be used to partition the processing, since they only define the bbox of each subset:

from disscube.utils.grids import register_local_grid

grid_100m = register_local_grid(
    cube,
    name="BR",
    bbox_geo=(-73.98, -33.75, -28.65, 5.27),  # Brazil bbox in WGS84
    resolution=100.0,
    snap=True,
)

Full workflow: setup → derivation → loading

from disscube.client import CubeClient
from disscube.models import SpatialSource, SpatialDerivation, Variable

cube = CubeClient(catalog="catalog.db", store="./data/")

# 1. Source
cube.register_spatial_source(SpatialSource(
    id="urban_centers",
    name="Centros Urbanos PNLT",
    format="vector",
    asset_url="data/raw/urban_centers.shp",
    crs="EPSG:5880",
))

# 2. Derivation — distance to urban centers on BR/5km
derivation = SpatialDerivation(
    source_id="urban_centers",
    grid_id="BR/5km",
    role="driver",
    variables=[Variable(name="dist_cidades", operator="min_distance")],
    valid_from="2000",
    valid_until="2014",
)

# 3. Run for one tile
cube.derive(derivation, tile_id="009002")

# 4. Load
da = cube.load("dist_cidades", tile_id="009002")
print(da.shape)   # (rows, cols)

Temporal variables with tiles

For time-varying drivers, derive multiple periods and load them as a series:

for start, end in [("2000", "2014"), ("2015", "2025")]:
    cube.derive(SpatialDerivation(
        source_id="urban_centers",
        grid_id="BR/5km",
        role="driver",
        variables=[Variable(name="dist_cidades", operator="min_distance")],
        valid_from=start, valid_until=end,
    ))

# Load the time series (time, y, x)
da = cube.load("dist_cidades", grid_id="BR/5km")
print(da.dims)   # ('time', 'y', 'x')