Skip to content

Commit 1f77c91

Browse files
committed
amend me: adding aux from props
1 parent 64d3a90 commit 1f77c91

3 files changed

Lines changed: 347 additions & 38 deletions

File tree

odc/stac/_mdtools.py

Lines changed: 141 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,21 @@
1717
Iterable,
1818
Iterator,
1919
List,
20+
Mapping,
2021
Optional,
2122
Sequence,
2223
Set,
2324
Tuple,
2425
Union,
2526
)
2627

28+
import numpy as np
2729
import pystac.asset
2830
import pystac.collection
2931
import pystac.errors
3032
import pystac.item
3133
import shapely.geometry
34+
import xarray as xr
3235
from affine import Affine
3336
from odc.geo import (
3437
CRS,
@@ -49,8 +52,10 @@
4952
from odc.loader.types import (
5053
AuxBandMetadata,
5154
AuxDataSource,
55+
AuxLoadParams,
5256
BandKey,
5357
BandQuery,
58+
GlobalLoadContext,
5459
MDParser,
5560
RasterBandMetadata,
5661
RasterGroupMetadata,
@@ -65,7 +70,12 @@
6570
from pystac.extensions.raster import RasterBand, RasterExtension
6671
from toolz import dicttoolz
6772

68-
from .model import MDParseConfig, ParsedItem, RasterCollectionMetadata
73+
from .model import (
74+
MDParseConfig,
75+
ParsedItem,
76+
PropertyLoadRequest,
77+
RasterCollectionMetadata,
78+
)
6979

7080
ConversionConfig = Dict[str, Any]
7181

@@ -485,6 +495,7 @@ def _config(self, collection_id: str | None) -> MDParseConfig:
485495
return MDParseConfig.from_dict(self._cfg, collection_id)
486496

487497
def extract(self, md: Any) -> RasterGroupMetadata:
498+
# pylint: disable=too-many-locals
488499
assert isinstance(md, pystac.item.Item)
489500
item = md
490501
c = self._config(item.collection_id)
@@ -501,7 +512,7 @@ def _keep(kv: tuple[str, pystac.asset.Asset]) -> bool:
501512

502513
data_bands = dicttoolz.itemfilter(_keep, item.assets)
503514

504-
bands: dict[BandKey, RasterBandMetadata] = {}
515+
bands: dict[BandKey, RasterBandMetadata | AuxBandMetadata] = {}
505516
aliases = alias_map_from_eo(item)
506517

507518
# 1. If band in user config -- use that
@@ -513,12 +524,30 @@ def _keep(kv: tuple[str, pystac.asset.Asset]) -> bool:
513524
for alias, bkey in c.aliases.items():
514525
aliases.setdefault(alias, []).insert(0, bkey)
515526

527+
for idx, prop in enumerate(c.with_props):
528+
bk: BandKey = ("_stac_metadata", idx + 1)
529+
bands[bk] = AuxBandMetadata(
530+
prop.dtype,
531+
nodata=prop.nodata,
532+
units=prop.units,
533+
)
534+
aliases[prop.output_name] = [bk]
535+
516536
return RasterGroupMetadata(bands, aliases, c.extra_dims, c.extra_coords)
517537

518538
def driver_data(self, md: Any, band_key: BandKey) -> Any:
539+
# None for raster bands
540+
# (PropertyLoadRequest, Value|None)
519541
assert isinstance(md, pystac.item.Item)
520-
assert band_key
542+
c = self._config(md.collection_id)
543+
544+
asset_name, band_idx = band_key
521545
driver_data = None
546+
547+
if asset_name == "_stac_metadata":
548+
prop_cfg = c.with_props[band_idx - 1]
549+
driver_data = (prop_cfg, md.properties.get(prop_cfg.key, None))
550+
522551
return driver_data
523552

524553
def _extract_bands(
@@ -537,6 +566,82 @@ def _extract_bands(
537566
return {(name, idx + 1): bm for idx, bm in enumerate(bands)}
538567

539568

569+
class StacAuxReader:
570+
"""
571+
Implements AuxReader protocol for STAC items.
572+
573+
Handles reading auxiliary data from STAC items, particularly metadata properties
574+
that are exposed as auxiliary bands.
575+
"""
576+
577+
# pylint: disable=too-few-public-methods
578+
579+
def read(
580+
self,
581+
srcs: Sequence[Sequence[AuxDataSource]],
582+
cfg: AuxLoadParams,
583+
used_names: set[str],
584+
available_coords: Mapping[str, xr.DataArray],
585+
ctx: GlobalLoadContext,
586+
*,
587+
dask_layer_name: str | None = None,
588+
) -> xr.DataArray:
589+
"""
590+
Read auxiliary data from STAC items.
591+
592+
:param srcs: Auxiliary data sources grouped by time
593+
:param cfg: Loading configuration
594+
:param used_names: Names claimed by raster bands and their coordinates
595+
:param available_coords: Available coordinates, must include time
596+
:param ctx: Load context
597+
:param dask_layer_name: Suggested dask layer name when reading with dask
598+
:return: Auxiliary data loaded into a xarray.DataArray
599+
"""
600+
assert (used_names, ctx, dask_layer_name) is not None
601+
# driver_data: (PropertyLoadRequest, None|float|str|int)
602+
603+
def extract_cfg() -> PropertyLoadRequest:
604+
for row in srcs:
605+
for src in row:
606+
if isinstance(src.driver_data, tuple) and len(src.driver_data) == 2:
607+
prop_cfg, *_ = src.driver_data
608+
return prop_cfg
609+
return PropertyLoadRequest(
610+
key="",
611+
name="",
612+
dtype=cfg.dtype or "float32",
613+
nodata=cfg.fill_value,
614+
)
615+
616+
prop_cfg = extract_cfg()
617+
_fill = prop_cfg.fill_value
618+
assert _fill is not None
619+
620+
def _value(row: Sequence[AuxDataSource]) -> Any:
621+
if len(row) == 0:
622+
return _fill
623+
624+
dd: Iterator[tuple[PropertyLoadRequest, Any]] = (
625+
src.driver_data for src in row
626+
)
627+
return prop_cfg.fuser([v for _, v in dd if v is not None])
628+
629+
values = [_value(row) for row in srcs]
630+
data = np.array(values, dtype=cfg.dtype)
631+
attrs: dict[str, Any] = {"units": prop_cfg.units}
632+
if cfg.fill_value is not None:
633+
attrs["nodata"] = cfg.fill_value
634+
635+
# Use time coordinate
636+
time = available_coords["time"]
637+
return xr.DataArray(
638+
data,
639+
coords={"time": time},
640+
dims=["time"],
641+
attrs=attrs,
642+
)
643+
644+
540645
class _CMDAssembler:
541646
"""
542647
Incrementally build up collection metadata from item stream.
@@ -708,32 +813,36 @@ def _get_grid(grid_name: str, asset: pystac.asset.Asset) -> GeoBox:
708813
_grids[grid_name] = grid
709814
return grid
710815

711-
band_names = []
712-
for bk, meta in template.meta.bands.items():
713-
asset_name, band_idx = bk
714-
asset = _assets.get(asset_name)
715-
if asset is None:
716-
continue
717-
band_names.append(asset_name)
718-
719-
grid_name = band2grid.get(asset_name, "default")
720-
geobox: Optional[GeoBox] = _get_grid(grid_name, asset) if has_proj else None
721-
722-
uri = asset.get_absolute_href()
723-
if uri is None:
724-
raise ValueError(
725-
f"Can not determine absolute path for band: {asset_name}"
726-
) # pragma: no cover (https://github.com/stac-utils/pystac/issues/754)
727-
816+
def _get_driver_data(bk: BandKey) -> tuple[Any, str | None]:
728817
driver_data: Any = None
729818
subdataset: str | None = None
730819
driver_data = md_plugin.driver_data(item, bk)
731820
if isinstance(driver_data, dict):
732821
subdataset = driver_data.get("subdataset", None)
822+
return driver_data, subdataset
823+
824+
for bk, meta in template.meta.bands.items():
825+
uri: str | None = None
826+
asset_name, band_idx = bk
827+
asset = _assets.get(asset_name)
828+
829+
if asset is not None:
830+
uri = asset.get_absolute_href()
831+
if uri is None:
832+
raise ValueError(
833+
f"Can not determine absolute path for asset: {asset_name}"
834+
) # pragma: no cover (https://github.com/stac-utils/pystac/issues/754)
733835

734836
if isinstance(meta, RasterBandMetadata):
837+
if asset is None or uri is None:
838+
continue
839+
840+
driver_data, subdataset = _get_driver_data(bk)
841+
grid_name = band2grid.get(asset_name, "default")
842+
geobox: Optional[GeoBox] = _get_grid(grid_name, asset) if has_proj else None
843+
735844
# Assumption: if extra dims are defined then asset bands are loaded into 3d+ array
736-
# RastetSource.band == 0 indicates "all the bands"
845+
# RasterSource.band == 0 indicates "all the bands"
737846
if meta.extra_dims:
738847
band_idx = 0
739848

@@ -746,16 +855,23 @@ def _get_grid(grid_name: str, asset: pystac.asset.Asset) -> GeoBox:
746855
driver_data=driver_data,
747856
)
748857
elif isinstance(meta, AuxBandMetadata):
858+
if uri is None:
859+
uri = f"virtual://{asset_name}/{band_idx}"
860+
861+
driver_data, subdataset = _get_driver_data(bk)
749862
bands[bk] = AuxDataSource(
750863
uri=uri,
751864
subdataset=subdataset,
752865
meta=meta,
753866
driver_data=driver_data,
754867
)
755868

756-
# the assets that aren't bands are accessories
757-
acc_names = set(_assets.keys()).difference(set(band_names))
758-
accessories = {name: {"path": _assets[name].href} for name in acc_names}
869+
data_asset_names = set(template.asset_names())
870+
accessories = {
871+
name: asset.to_dict()
872+
for name, asset in _assets.items()
873+
if name not in data_asset_names
874+
}
759875

760876
_cmd = item.common_metadata
761877
return ParsedItem(
@@ -1062,7 +1178,7 @@ def _resolve_driver(
10621178
stac_cfg = {} if stac_cfg is None else stac_cfg
10631179
if driver is None:
10641180
md_parser = StacMDParser(stac_cfg)
1065-
return RioDriver(md_parser=md_parser), md_parser
1181+
return RioDriver(md_parser=md_parser, aux_reader=StacAuxReader()), md_parser
10661182
rdr = reader_driver(driver)
10671183
md_parser = rdr.md_parser
10681184
if md_parser is None:

0 commit comments

Comments
 (0)