Skip to content

Merge & stack#

Free functions for combining multiple rasters into one.

Hold "Ctrl" to enable pan & zoom
flowchart LR
    M["<b>pyramids.dataset.merge</b>"]
    M --> MR["<b>merge_rasters</b><br/>mosaic overlapping rasters<br/>into one (reproject on the fly)"]
    M --> SB["<b>stack_bands</b><br/>stack single-band rasters<br/>into one multi-band Dataset"]
  • merge_rasters — mosaic several (overlapping or adjacent) rasters into a single raster covering their union.
  • stack_bands — stack several single-band rasters into one multi-band raster.

See the Mosaic & merge notebook for runnable examples.

pyramids.dataset.merge.merge_rasters(src, dst, no_data_value='0', init='nan', n='nan', method='last', dst_crs=None, resampling=DEFAULT_RESAMPLING, signer=None, *, bbox=None, bbox_crs=None) #

Merge a group of rasters into one raster, resolving overlaps by method.

The overlap-resolution method selects how overlapping pixels are combined:

  • "last" (default) / "first" — z-order compositing: the last (or first) source covering a pixel wins. Implemented cheaply with :func:gdal.BuildVRT + :func:gdal.Translate.
  • "min" / "max" / "sum" — per-pixel reduction across every source overlapping that pixel, ignoring no-data. Each source is aligned onto the union grid and the bands are stacked and reduced with NaN-aware numpy.

Parameters:

Name Type Description Default
src Sequence[str | Path]

Paths to all input rasters.

required
dst str | Path

Path to the output raster. Its extension alone selects the output driver (.tif -> GTiff, .nc -> netCDF, …) — the same resolution every other write path in the package uses — so one dst yields the same format for every method. COMPRESS=LZW is a GTiff creation option and is applied only when the extension resolves to GTiff; other formats are written with their driver defaults. A write-by-copy-only format is refused for every method -- that is .png, .jpg / .jpeg, .jp2 / .j2k and .asc, and .vrt on top (a VRT writes a reference, not a raster). The z-order path could produce several of them, since it writes via gdal.Translate, and the reduction path could not; letting method decide what dst may be is the same defect as letting it decide the format, so both take the stricter answer. .asc is the one this costs: it was writable before, through the z-order path only. Write a GTiff and convert.

required
no_data_value float | int | str

Stamped on the output bands as the nodata marker. For the reduction methods it also fills pixels with no source coverage.

'0'
init float | int | str

Reported value for pixels with no source coverage in the VRT (z-order methods only). Maps to :func:gdal.BuildVRTOptions VRTNodata.

'nan'
n float | int | str

Source pixels matching this value are ignored — both when building the VRT mosaic (z-order) and when reducing (the value is treated as source no-data).

'nan'
method str

Overlap-resolution rule: one of "first", "last" (default), "min", "max", "sum".

'last'
dst_crs int | str | None

Target CRS for the mosaic, as an EPSG code (32632) or any GDAL-parseable CRS string ("EPSG:32632", a WKT, a PROJ string). Each source whose CRS differs from the target is reprojected onto it (via :func:gdal.Warp) before compositing, so tiles in different CRSs — e.g. a Sentinel-2 AOI straddling two UTM zones — mosaic correctly. None (default) keeps the previous behaviour: sources are assumed to share a CRS and are composited as-is, except when they are found to disagree, in which case they are reprojected onto the first source's CRS. Reprojection always happens before the BuildVRT/Warp compositing step because that step has no reprojection capability and assumes a single shared grid.

None
resampling str

Resampling method used when a source is reprojected to dst_crs (or to the common CRS on auto-detect). Case-insensitive; any key of :data:pyramids.base._utils.INTERPOLATION_METHODS: "nearest" (alias "nearest neighbor", the default), "bilinear", "cubic", "cubic_spline", "lanczos", "average", "mode", "max", "min", "med", "q1", "q3", "sum", and "rms". Prefer "bilinear"/"cubic" for continuous data (reflectance, DEM) to avoid the blockiness nearest introduces across reprojection. Ignored when no source is reprojected.

DEFAULT_RESAMPLING
signer Any

Optional signer exposing sign_href(str) -> str and gdal_env() -> dict[str, str] (e.g. a :class:pyramids.stac.signers.Signer). When given, both hooks are applied — exactly as :func:pyramids.stac.load_asset does: signer.sign_href rewrites every source path first (e.g. grafting a SAS token onto a blob URL), then signer.gdal_env() is installed via :class:~pyramids.base.remote.CloudConfig for the duration of the merge. This means URL-signing signers (Planetary Computer SAS, whose credential rides the href and whose gdal_env() is empty) and env-based signers (Requester-Pays, bearer) both authenticate without wrapping the call in a with CloudConfig(...) block. None (default) leaves source hrefs untouched and installs no extra config.

None
bbox Sequence[float] | None

Optional (west, south, east, north) window to restrict the merge to. None (default) merges the full extent of every source, which is what the function has always done.

This is not a convenience for cropping afterwards: without it GDAL is given no reason to read less, so a mosaic of remote sources pulls the entire source extent through /vsicurl even when the caller wants a fraction of it. A full Sentinel-2 tile is 10980x10980 px (verified against a public Earth Search COG), so an area of interest covering a fraction of one tile still costs the whole tile without a window. How much that saves depends on the ratio between the source footprint and the window.

The window is resolved once, onto the mosaic's own pixel grid, and used by both methods: z-order passes it to :func:gdal.Translate as projWin, so only the byte ranges the window touches are requested; the reduction methods clip the union grid to it. Snapping outward onto whole pixels keeps the result a strict sub-grid, and resolving it once means both methods return the same grid for the same arguments.

A bbox must be ordered west < east and south < north. A window crossing the antimeridian (west > east) is rejected rather than silently reinterpreted as its own complement; split it and merge the two halves. :meth:pyramids.dataset.Dataset.crop handles the seam directly, but a mosaic is composited on one grid, which a seam-crossing window would not have.

For a lon/lat mosaic the window is rewritten into the mosaic's own longitude convention first, so a -180..180 bbox reads correctly against a 0..360 grid (and the reverse). A window that ends up spanning that convention's seam is rejected on the same grounds as an antimeridian one. A window extending past the mosaic is clipped to it; one that misses it entirely raises.

None
bbox_crs int | str | None

CRS that bbox is expressed in — an EPSG code (4326), an authority string ("EPSG:4326"), a WKT, or anything :meth:pyproj.CRS.from_user_input accepts. None (default) means bbox is already in the mosaic's own CRS — which, when dst_crs is given, is dst_crs, since sources are reprojected before the window is applied. Ignored when bbox is None. Named to match :meth:pyramids.dataset.engines.cog.COG.read_part.

None

Returns:

Type Description
None

None

Note

The z-order methods ("first"/"last") preserve each source's data type via BuildVRT + Translate. The reduction methods ("min"/"max"/"sum") align every source onto the union grid with gdal.Warp (nearest resampling — exact for already-aligned tiles) and write a single-precision-safe Float64 output regardless of the source dtype, so they may differ in dtype from a z-order merge of the same integer inputs.

Raises:

Type Description
TypeError

resampling is not a string, or bbox is not four numbers (a string, a scalar, or a sequence holding a non-numeric element).

ValueError

method/resampling is not a supported value, dst_crs cannot be parsed as a CRS, a source carries no CRS, or bbox is malformed (wrong length, non-finite, inverted, zero-area), crosses the antimeridian or the mosaic's longitude seam once reprojected, selects no whole pixel, does not overlap the mosaic, or cannot be projected into its CRS.

RuntimeError

GDAL failed to open a source, reproject it, or build the source mosaic.

DriverNotExistError

dst has no extension, or one the driver catalog does not know.

FileFormatNotSupportedError

dst's extension maps to a write-by-copy-only format, for any method.

Examples:

  • Mosaic two tiles, keeping the larger value wherever they overlap:
    >>> from pyramids.dataset.merge import merge_rasters
    >>> merge_rasters(  # doctest: +SKIP
    ...     ["tile_a.tif", "tile_b.tif"],
    ...     "mosaic_max.tif",
    ...     no_data_value=-9999.0,
    ...     method="max",
    ... )
    
  • Default last-wins compositing (unchanged from the previous behaviour):
    >>> merge_rasters(["tile_a.tif", "tile_b.tif"], "mosaic.tif")  # doctest: +SKIP
    
  • Mosaic tiles from two UTM zones into a single CRS:
    >>> merge_rasters(  # doctest: +SKIP
    ...     ["utm32_tile.tif", "utm33_tile.tif"],
    ...     "mosaic_utm32.tif",
    ...     dst_crs=32632,
    ... )
    
  • Mosaic Requester-Pays S3 tiles by passing a signer (no with block):
    >>> from pyramids.stac import AWSRequesterPaysSigner  # doctest: +SKIP
    >>> merge_rasters(  # doctest: +SKIP
    ...     ["s3://bucket/a.tif", "s3://bucket/b.tif"],
    ...     "mosaic.tif",
    ...     signer=AWSRequesterPaysSigner(region="us-west-2"),
    ... )
    
Source code in src/pyramids/dataset/merge.py
def merge_rasters(
    src: Sequence[str | Path],
    dst: str | Path,
    no_data_value: float | int | str = "0",
    init: float | int | str = "nan",
    n: float | int | str = "nan",
    method: str = "last",
    dst_crs: int | str | None = None,
    resampling: str = DEFAULT_RESAMPLING,
    signer: Any = None,
    *,
    bbox: Sequence[float] | None = None,
    bbox_crs: int | str | None = None,
) -> None:
    """Merge a group of rasters into one raster, resolving overlaps by ``method``.

    The overlap-resolution ``method`` selects how overlapping pixels are
    combined:

    * ``"last"`` (default) / ``"first"`` — z-order compositing: the last (or
      first) source covering a pixel wins. Implemented cheaply with
      :func:`gdal.BuildVRT` + :func:`gdal.Translate`.
    * ``"min"`` / ``"max"`` / ``"sum"`` — per-pixel reduction across every
      source overlapping that pixel, ignoring no-data. Each source is aligned
      onto the union grid and the bands are stacked and reduced with NaN-aware
      numpy.

    Args:
        src (Sequence[str | Path]):
            Paths to all input rasters.
        dst (str | Path):
            Path to the output raster. Its extension alone selects the output
            driver (`.tif` -> GTiff, `.nc` -> netCDF, …) — the same
            resolution every other write path in the package uses — so one
            `dst` yields the same format for every `method`. `COMPRESS=LZW`
            is a GTiff creation option and is applied only when the extension
            resolves to GTiff; other formats are written with their driver
            defaults. A write-by-copy-only format is refused for every
            `method` -- that is `.png`, `.jpg` / `.jpeg`, `.jp2` / `.j2k` and
            `.asc`, and `.vrt` on top (a VRT writes a reference, not a
            raster). The z-order path could produce several of them, since it
            writes via `gdal.Translate`, and the reduction path could not;
            letting `method` decide what `dst` may be is the same defect as
            letting it decide the format, so both take the stricter answer.
            `.asc` is the one this costs: it was writable before, through the
            z-order path only. Write a GTiff and convert.
        no_data_value (float | int | str):
            Stamped on the output bands as the nodata marker. For the reduction
            methods it also fills pixels with no source coverage.
        init (float | int | str):
            Reported value for pixels with no source coverage in the VRT (z-order
            methods only). Maps to :func:`gdal.BuildVRTOptions` ``VRTNodata``.
        n (float | int | str):
            Source pixels matching this value are ignored — both when building
            the VRT mosaic (z-order) and when reducing (the value is treated as
            source no-data).
        method (str):
            Overlap-resolution rule: one of ``"first"``, ``"last"`` (default),
            ``"min"``, ``"max"``, ``"sum"``.
        dst_crs (int | str | None):
            Target CRS for the mosaic, as an EPSG code (``32632``) or any
            GDAL-parseable CRS string (``"EPSG:32632"``, a WKT, a PROJ string).
            Each source whose CRS differs from the target is reprojected onto it
            (via :func:`gdal.Warp`) **before** compositing, so tiles in different
            CRSs — e.g. a Sentinel-2 AOI straddling two UTM zones — mosaic
            correctly. ``None`` (default) keeps the previous behaviour: sources
            are assumed to share a CRS and are composited as-is, *except* when
            they are found to disagree, in which case they are reprojected onto
            the first source's CRS. Reprojection always happens before the
            ``BuildVRT``/``Warp`` compositing step because that step has no
            reprojection capability and assumes a single shared grid.
        resampling (str):
            Resampling method used when a source is reprojected to ``dst_crs``
            (or to the common CRS on auto-detect). Case-insensitive; any key
            of :data:`pyramids.base._utils.INTERPOLATION_METHODS`: ``"nearest"``
            (alias ``"nearest neighbor"``, the default), ``"bilinear"``,
            ``"cubic"``, ``"cubic_spline"``, ``"lanczos"``, ``"average"``,
            ``"mode"``, ``"max"``, ``"min"``, ``"med"``, ``"q1"``, ``"q3"``,
            ``"sum"``, and ``"rms"``.
            Prefer ``"bilinear"``/``"cubic"`` for continuous data (reflectance,
            DEM) to avoid the blockiness nearest introduces across reprojection.
            Ignored when no source is reprojected.
        signer (Any):
            Optional signer exposing ``sign_href(str) -> str`` and
            ``gdal_env() -> dict[str, str]`` (e.g. a
            :class:`pyramids.stac.signers.Signer`). When given, **both** hooks
            are applied — exactly as :func:`pyramids.stac.load_asset` does:
            ``signer.sign_href`` rewrites every source path first (e.g. grafting
            a SAS token onto a blob URL), then ``signer.gdal_env()`` is installed
            via :class:`~pyramids.base.remote.CloudConfig` for the duration of
            the merge. This means URL-signing signers (Planetary Computer SAS,
            whose credential rides the href and whose ``gdal_env()`` is empty)
            and env-based signers (Requester-Pays, bearer) both authenticate
            without wrapping the call in a ``with CloudConfig(...)`` block.
            ``None`` (default) leaves source hrefs untouched and installs no
            extra config.
        bbox (Sequence[float] | None):
            Optional ``(west, south, east, north)`` window to restrict the merge
            to. ``None`` (default) merges the full extent of every source, which
            is what the function has always done.

            This is not a convenience for cropping afterwards: without it GDAL is
            given no reason to read less, so a mosaic of remote sources pulls the
            **entire** source extent through ``/vsicurl`` even when the caller
            wants a fraction of it. A full Sentinel-2 tile is 10980x10980 px
            (verified against a public Earth Search COG), so an area of interest
            covering a fraction of one tile still costs the whole tile without a
            window. How much that saves depends on the ratio between the source
            footprint and the window.

            The window is resolved once, onto the mosaic's own pixel grid, and
            used by both methods: z-order passes it to :func:`gdal.Translate` as
            ``projWin``, so only the byte ranges the window touches are requested;
            the reduction methods clip the union grid to it. Snapping outward onto
            whole pixels keeps the result a strict sub-grid, and resolving it once
            means both methods return the same grid for the same arguments.

            A bbox must be ordered ``west < east`` and ``south < north``. A window
            crossing the antimeridian (``west > east``) is rejected rather than
            silently reinterpreted as its own complement; split it and merge the
            two halves. :meth:`pyramids.dataset.Dataset.crop` handles the seam
            directly, but a mosaic is composited on one grid, which a seam-crossing
            window would not have.

            For a lon/lat mosaic the window is rewritten into the mosaic's own
            longitude convention first, so a ``-180..180`` bbox reads correctly
            against a ``0..360`` grid (and the reverse). A window that ends up
            spanning that convention's seam is rejected on the same grounds as an
            antimeridian one. A window extending past the mosaic is clipped to it;
            one that misses it entirely raises.
        bbox_crs (int | str | None):
            CRS that ``bbox`` is expressed in — an EPSG code (``4326``), an
            authority string (``"EPSG:4326"``), a WKT, or anything
            :meth:`pyproj.CRS.from_user_input` accepts. ``None`` (default) means
            ``bbox`` is already in the mosaic's own CRS — which, when ``dst_crs``
            is given, is ``dst_crs``, since sources are reprojected before the
            window is applied. Ignored when ``bbox`` is ``None``. Named to match
            :meth:`pyramids.dataset.engines.cog.COG.read_part`.

    Returns:
        None

    Note:
        The z-order methods (``"first"``/``"last"``) preserve each source's data
        type via ``BuildVRT`` + ``Translate``. The reduction methods
        (``"min"``/``"max"``/``"sum"``) align every source onto the union grid
        with ``gdal.Warp`` (nearest resampling — exact for already-aligned tiles)
        and write a single-precision-safe **Float64** output regardless of the
        source dtype, so they may differ in dtype from a z-order merge of the
        same integer inputs.

    Raises:
        TypeError: ``resampling`` is not a string, or ``bbox`` is not four numbers
            (a string, a scalar, or a sequence holding a non-numeric element).
        ValueError: ``method``/``resampling`` is not a supported value,
            ``dst_crs`` cannot be parsed as a CRS, a source carries no CRS, or
            ``bbox`` is malformed (wrong length, non-finite, inverted, zero-area),
            crosses the antimeridian or the mosaic's longitude seam once
            reprojected, selects no whole pixel, does not overlap the mosaic, or
            cannot be projected into its CRS.
        RuntimeError: GDAL failed to open a source, reproject it, or build the
            source mosaic.
        DriverNotExistError: `dst` has no extension, or one the driver catalog
            does not know.
        FileFormatNotSupportedError: `dst`'s extension maps to a
            write-by-copy-only format, for any `method`.

    Examples:
        - Mosaic two tiles, keeping the larger value wherever they overlap:
            ```python
            >>> from pyramids.dataset.merge import merge_rasters
            >>> merge_rasters(  # doctest: +SKIP
            ...     ["tile_a.tif", "tile_b.tif"],
            ...     "mosaic_max.tif",
            ...     no_data_value=-9999.0,
            ...     method="max",
            ... )

            ```
        - Default last-wins compositing (unchanged from the previous behaviour):
            ```python
            >>> merge_rasters(["tile_a.tif", "tile_b.tif"], "mosaic.tif")  # doctest: +SKIP

            ```
        - Mosaic tiles from two UTM zones into a single CRS:
            ```python
            >>> merge_rasters(  # doctest: +SKIP
            ...     ["utm32_tile.tif", "utm33_tile.tif"],
            ...     "mosaic_utm32.tif",
            ...     dst_crs=32632,
            ... )

            ```
        - Mosaic Requester-Pays S3 tiles by passing a signer (no ``with`` block):
            ```python
            >>> from pyramids.stac import AWSRequesterPaysSigner  # doctest: +SKIP
            >>> merge_rasters(  # doctest: +SKIP
            ...     ["s3://bucket/a.tif", "s3://bucket/b.tif"],
            ...     "mosaic.tif",
            ...     signer=AWSRequesterPaysSigner(region="us-west-2"),
            ... )

            ```
    """
    if method not in _MERGE_METHODS:
        raise ValueError(
            f"method must be one of {list(_MERGE_METHODS)}, got {method!r}."
        )
    # Resolve `dst` here, before anything is opened. Both write paths resolve it
    # again where they need the driver name, but a destination the catalog
    # cannot answer for is a pure argument error -- and it used to be reported
    # only after every source had been opened and possibly reprojected, which
    # for /vsicurl/ inputs is network work spent to reach a typo. It also
    # reported at two different points depending on `method`.
    # Resolved here so a destination the catalog cannot answer for fails
    # before any source is opened, and so both write paths below agree. They
    # call it again where they need the name; it is a cached catalog lookup, so
    # the repeat is free and keeps each path readable on its own.
    resolve_output_driver(dst)

    # SMELL: `init` and `n` default to the string `"nan"`, which
    # round-trips through GDAL as float NaN. For integer-typed
    # rasters (e.g. UInt16) GDAL emits a warning per band:
    # `Band data type of <T> cannot represent the specified NoData
    # value of nan`. The defaults are kept for backwards-compat
    # with the previous gdal_merge.main-based signature; callers
    # that hit integer rasters should pass an explicit numeric
    # value instead of relying on the default.
    src_paths = [str(p) for p in src]
    if signer is not None:
        # Apply the signer's href rewrite to every source (e.g. graft a SAS
        # token onto a blob URL) so URL-signing signers authenticate. A no-op
        # for signers that authenticate via gdal_env() only — the base
        # sign_href returns the href unchanged. This mirrors load_asset, which
        # applies BOTH signer hooks (sign_href + gdal_env); applying only the
        # env half here would silently read URL-signed sources unauthenticated.
        src_paths = [signer.sign_href(p) for p in src_paths]

    # All GDAL reads/writes run under the signer's cloud config (a no-op when
    # signer is None) so authenticated remote sources open with the right
    # credentials for the whole merge.
    with _cloud_config(signer, path=src_paths):
        # Put every source on one CRS before compositing. The BuildVRT/Warp
        # mosaic below cannot reproject — it stitches pixel grids assuming a
        # shared CRS — so mismatched sources must be warped first or they would
        # mis-align silently. `_keepalive` holds the in-memory warped VRTs so
        # GDAL does not free them while the mosaic is built.
        sources, _keepalive = _prepare_sources(src_paths, dst_crs, resampling)

        if method in _REDUCE_METHODS:
            _merge_reduce(sources, str(dst), method, no_data_value, n, bbox, bbox_crs)
            return

        # z-order: "last" keeps natural order (last source wins); "first"
        # reverses so the original first source is placed last in the VRT and
        # therefore wins.
        ordered = list(reversed(sources)) if method == "first" else sources
        vrt_opts = gdal.BuildVRTOptions(
            srcNodata=str(n),
            VRTNodata=str(init),
        )
        vrt_ds = gdal.BuildVRT("", ordered, options=vrt_opts)
        if vrt_ds is None:
            raise RuntimeError(
                f"gdal.BuildVRT returned None for sources {src_paths!r}; "
                "check that all paths are readable rasters with consistent "
                "band counts and CRS."
            )
        proj_win = None
        if bbox is not None:
            # Resolve the window here, in the mosaic's own CRS, and hand Translate a
            # `projWin` that already lies on the mosaic's pixel boundaries -- rather
            # than passing the caller's bbox with `projWinSRS` and letting GDAL
            # reproject it. Two independent reprojections of the same window round to
            # opposite sides of a pixel edge, and the two merge paths then return
            # different rasters for identical arguments (measured 3x3 at x=111364.8
            # against 4x3 at x=0.0 for `dst_crs=3857`). One resolver for both paths
            # makes them agree by construction, and `_restrict_grid` also rejects a
            # disjoint window, which GDAL does not: it writes a 1x1 no-data raster at
            # the window's origin, which reads back as a successful merge of nothing.
            clipped, window_x, window_y = _restrict_grid(
                vrt_ds.GetGeoTransform(),
                vrt_ds.RasterXSize,
                vrt_ds.RasterYSize,
                vrt_ds.GetProjection(),
                bbox,
                bbox_crs,
            )
            # (ulx, uly, lrx, lry) — already snapped, so GDAL's own rounding is a
            # no-op and the output grid matches the reduction path's exactly.
            proj_win = [
                clipped[0],
                clipped[3],
                clipped[0] + window_x * clipped[1],
                clipped[3] + window_y * clipped[5],
            ]

        # `projWin` is what stops the read at the window: without it GDAL has no
        # reason to restrict what it pulls through /vsicurl and materialises the
        # whole mosaic extent.
        # Hand gdal.Translate the driver the catalog resolved rather than
        # letting it re-infer from the extension. The two tables disagree: the
        # catalog knows `.nc4` as a netCDF alias, GDAL's netCDF driver
        # advertises only `nc`, so the same `dst` wrote a netCDF through the
        # reduction path and died with "Could not identify an output driver"
        # here -- the format depending on `method` again, which is exactly what
        # resolving once was meant to stop.
        # Strict gate, not `for_copy`: this method has two write paths and the
        # other builds with `Create`. Relaxing only this one would make `.png`
        # legal for method="last" and illegal for method="min" -- the very
        # asymmetry the shared resolution above exists to remove.
        out_driver = resolve_output_driver(dst)
        # LZW is a GTiff creation option; other drivers reject it.
        translate_opts = gdal.TranslateOptions(
            format=out_driver,
            creationOptions=["COMPRESS=LZW"] if out_driver == "GTiff" else [],
            noData=str(no_data_value),
            projWin=proj_win,
        )
        out_ds = gdal.Translate(str(dst), vrt_ds, options=translate_opts)
        if out_ds is None:
            raise RuntimeError(
                f"gdal.Translate returned None writing the mosaic to {str(dst)!r}."
            )
        out_ds.FlushCache()
        out_ds = None
        vrt_ds = None

pyramids.dataset.merge.stack_bands(files, *, band_names=None, align=False, no_data_value=_INHERIT_NO_DATA, path=None, signer=None) #

Stack N single-band rasters into one multi-band :class:Dataset.

Free-function alias for :meth:pyramids.dataset.Dataset.from_band_files — see that method for the full contract, edge cases, and examples.

Parameters:

Name Type Description Default
files list[str | Path]

Single-band raster paths/URLs to stack (order = band order).

required
band_names list[str] | None

Explicit per-band names; None derives them from the file names.

None
align bool

When True, resample mismatched inputs onto files[0]'s grid instead of raising :class:~pyramids.base._errors.AlignmentError.

False
no_data_value Any

No-data value for the output bands; omitted means "inherit from the source rasters".

_INHERIT_NO_DATA
path str | Path | None

Output path, whose extension selects the driver (.tif -> GTiff, .nc -> netCDF, …); None keeps the result in memory. COMPRESS=LZW is applied only when the extension resolves to GTiff. A write-by-copy-only format such as PNG is refused — see :meth:pyramids.dataset.Dataset.from_band_files for why both of its write paths answer alike.

None
signer Any

Optional signer exposing sign_href(str) -> str and gdal_env() -> dict[str, str] (e.g. a :class:pyramids.stac.signers.Signer). When given, both hooks are applied (as in :func:pyramids.stac.load_asset): every input href is rewritten through signer.sign_href first, then signer.gdal_env() is installed via :class:~pyramids.base.remote.CloudConfig for the duration of the stack, so authenticated cloud inputs (URL-signed or env-credentialed) read with the right credentials. None (default) leaves behaviour unchanged.

None

Returns:

Name Type Description
Dataset Dataset

A multi-band dataset, one band per input file.

Raises:

Type Description
DriverNotExistError

path has no extension, or one the driver catalog does not know.

FileFormatNotSupportedError

path's extension maps to a write-by-copy-only format, whichever write path the inputs take.

Source code in src/pyramids/dataset/merge.py
def stack_bands(
    files: list[str | Path],
    *,
    band_names: list[str] | None = None,
    align: bool = False,
    no_data_value: Any = _INHERIT_NO_DATA,
    path: str | Path | None = None,
    signer: Any = None,
) -> Dataset:
    """Stack N single-band rasters into one multi-band :class:`Dataset`.

    Free-function alias for :meth:`pyramids.dataset.Dataset.from_band_files`
    — see that method for the full contract, edge cases, and examples.

    Args:
        files: Single-band raster paths/URLs to stack (order = band order).
        band_names: Explicit per-band names; ``None`` derives them from the
            file names.
        align: When ``True``, resample mismatched inputs onto ``files[0]``'s
            grid instead of raising :class:`~pyramids.base._errors.AlignmentError`.
        no_data_value: No-data value for the output bands; omitted means
            "inherit from the source rasters".
        path: Output path, whose extension selects the driver (``.tif`` ->
            GTiff, ``.nc`` -> netCDF, …); ``None`` keeps the result in memory.
            `COMPRESS=LZW` is applied only when the extension resolves to
            GTiff. A write-by-copy-only format such as PNG is refused — see
            :meth:`pyramids.dataset.Dataset.from_band_files` for why both of
            its write paths answer alike.
        signer: Optional signer exposing ``sign_href(str) -> str`` and
            ``gdal_env() -> dict[str, str]`` (e.g. a
            :class:`pyramids.stac.signers.Signer`). When given, **both** hooks
            are applied (as in :func:`pyramids.stac.load_asset`): every input
            href is rewritten through ``signer.sign_href`` first, then
            ``signer.gdal_env()`` is installed via
            :class:`~pyramids.base.remote.CloudConfig` for the duration of the
            stack, so authenticated cloud inputs (URL-signed or env-credentialed)
            read with the right credentials. ``None`` (default) leaves behaviour
            unchanged.

    Returns:
        Dataset: A multi-band dataset, one band per input file.

    Raises:
        DriverNotExistError: `path` has no extension, or one the driver
            catalog does not know.
        FileFormatNotSupportedError: `path`'s extension maps to a
            write-by-copy-only format, whichever write path the inputs take.
    """
    if signer is not None:
        files = [signer.sign_href(str(f)) for f in files]
    with _cloud_config(signer, path=[str(f) for f in files]):
        result = Dataset.from_band_files(
            files,
            band_names=band_names,
            align=align,
            no_data_value=no_data_value,
            path=path,
        )
    return result