Skip to content

rslearn.data_sources.copernicus_sentinel3

copernicus_sentinel3

Data sources for Sentinel-3 data in the ESA Copernicus API.

OLCI_REFLECTANCE_BANDS module-attribute

OLCI_REFLECTANCE_BANDS = [f'Oa{idx:02d}_reflectance' for idx in range(1, 22)]

Sentinel-3 OLCI EFR top-of-atmosphere reflectance bands.

SLSTR_REFLECTANCE_BANDS module-attribute

SLSTR_REFLECTANCE_BANDS = [f'S{idx}_reflectance' for idx in range(1, 7)]

Sentinel-3 SLSTR RBT nadir-view reflectance bands.

SLSTR_BT_BANDS module-attribute

SLSTR_BT_BANDS = [f'S{idx}_BT' for idx in range(7, 10)]

Sentinel-3 SLSTR RBT nadir-view brightness-temperature bands.

Sentinel3OlciEFR

Bases: Copernicus

Sentinel-3 OLCI Level-1 EFR top-of-atmosphere reflectance.

Source code in rslearn/data_sources/copernicus_sentinel3.py
class Sentinel3OlciEFR(Copernicus):
    """Sentinel-3 OLCI Level-1 EFR top-of-atmosphere reflectance."""

    PRODUCT_TYPE = "OL_1_EFR___"
    DEFAULT_BANDS = OLCI_REFLECTANCE_BANDS

    def __init__(
        self,
        band_names: list[str] | None = None,
        grid_resolution: float = 0.0027,
        nodata_value: float = -9999.0,
        swath_padding: float = 0.1,
        context: DataSourceContext = DataSourceContext(),
        **kwargs: Any,
    ) -> None:
        """Create an OLCI EFR source.

        Args:
            band_names: bands to ingest when no layer configuration is available.
            grid_resolution: intermediate WGS84 grid resolution in degrees.
            nodata_value: value assigned outside the valid swath.
            swath_padding: padding in degrees around requested windows before gridding.
            context: data source context.
            kwargs: additional options for :class:`Copernicus`.
        """
        if grid_resolution <= 0:
            raise ValueError("grid_resolution must be positive")
        if swath_padding < 0:
            raise ValueError("swath_padding cannot be negative")
        self.band_names = _requested_bands(context, band_names, self.DEFAULT_BANDS)
        self.grid_resolution = grid_resolution
        self.nodata_value = nodata_value
        self.swath_padding = swath_padding
        super().__init__(
            glob_to_bands={"OLCI_EFR": self.band_names},
            query_filter=_sentinel3_query_filter("OLCI", self.PRODUCT_TYPE),
            context=context,
            **kwargs,
        )

    def _process_product_zip(
        self,
        tile_store: TileStoreWithLayer,
        item: CopernicusItem,
        local_zip_fname: str,
        geometries: list[STGeometry] | None = None,
    ) -> None:
        """Convert an OLCI SAFE product and ingest requested reflectance bands."""
        radiance_variables = {
            band: band.replace("_reflectance", "_radiance") for band in self.band_names
        }
        required = {
            "instrument_data.nc",
            "tie_geometries.nc",
            "geo_coordinates.nc",
            *[f"{name}.nc" for name in radiance_variables.values()],
        }
        with tempfile.TemporaryDirectory() as tmp_dir, ZipFile(local_zip_fname) as zipf:
            paths = _extract_zip_members(zipf, required, tmp_dir)

            solar_flux = _read_netcdf_variable(
                paths["instrument_data.nc"], "solar_flux", mask_and_scale=False
            ).astype(np.float32)
            detector_index = _read_netcdf_variable(
                paths["instrument_data.nc"], "detector_index", mask_and_scale=False
            ).astype(np.int64)
            target_shape = detector_index.shape

            solar_zenith = _interpolate_tie_points(
                _read_netcdf_variable(paths["tie_geometries.nc"], "SZA"),
                target_shape,
            )
            cos_solar_zenith = np.clip(
                np.cos(np.deg2rad(solar_zenith)), 0.01, None
            ).astype(np.float32)
            latitude = _read_netcdf_variable(
                paths["geo_coordinates.nc"], "latitude"
            ).astype(np.float32)
            longitude = _read_netcdf_variable(
                paths["geo_coordinates.nc"], "longitude"
            ).astype(np.float32)
            if latitude.shape != target_shape or longitude.shape != target_shape:
                raise ValueError(
                    "OLCI geolocation and detector grids have different shapes: "
                    f"{latitude.shape}, {longitude.shape}, and {target_shape}"
                )

            valid_detector = (detector_index >= 0) & (
                detector_index < solar_flux.shape[1]
            )
            safe_detector = np.clip(detector_index, 0, solar_flux.shape[1] - 1)
            arrays = []
            for band_name, variable_name in radiance_variables.items():
                band_index = int(band_name[2:4]) - 1
                radiance = _read_netcdf_variable(
                    paths[f"{variable_name}.nc"], variable_name
                ).astype(np.float32)
                reflectance = _radiance_to_reflectance(
                    radiance,
                    solar_flux[band_index][safe_detector],
                    cos_solar_zenith,
                )
                arrays.append(np.where(valid_detector, reflectance, np.nan))

        data, longitude, latitude = _crop_swath(
            np.stack(arrays),
            longitude,
            latitude,
            geometries,
            self.swath_padding,
        )
        _write_swath(
            tile_store,
            item,
            self.band_names,
            data,
            longitude,
            latitude,
            self.grid_resolution,
            self.nodata_value,
        )

Sentinel3SlstrRBT

Bases: Copernicus

Sentinel-3 SLSTR Level-1 nadir reflectance and brightness temperature.

Source code in rslearn/data_sources/copernicus_sentinel3.py
class Sentinel3SlstrRBT(Copernicus):
    """Sentinel-3 SLSTR Level-1 nadir reflectance and brightness temperature."""

    PRODUCT_TYPE = "SL_1_RBT___"
    DEFAULT_BANDS = SLSTR_REFLECTANCE_BANDS + SLSTR_BT_BANDS

    def __init__(
        self,
        band_names: list[str] | None = None,
        reflectance_grid_resolution: float = 0.0045,
        bt_grid_resolution: float = 0.009,
        nodata_value: float = -9999.0,
        swath_padding: float = 0.1,
        context: DataSourceContext = DataSourceContext(),
        **kwargs: Any,
    ) -> None:
        """Create an SLSTR RBT source.

        Args:
            band_names: bands to ingest when no layer configuration is available.
            reflectance_grid_resolution: intermediate WGS84 resolution for S1-S6.
            bt_grid_resolution: intermediate WGS84 resolution for S7-S9.
            nodata_value: value assigned outside the valid swath.
            swath_padding: padding in degrees around requested windows before gridding.
            context: data source context.
            kwargs: additional options for :class:`Copernicus`.
        """
        if reflectance_grid_resolution <= 0 or bt_grid_resolution <= 0:
            raise ValueError("grid resolutions must be positive")
        if swath_padding < 0:
            raise ValueError("swath_padding cannot be negative")
        self.band_names = _requested_bands(context, band_names, self.DEFAULT_BANDS)
        self.reflectance_bands = [
            band for band in SLSTR_REFLECTANCE_BANDS if band in self.band_names
        ]
        self.bt_bands = [band for band in SLSTR_BT_BANDS if band in self.band_names]
        self.reflectance_grid_resolution = reflectance_grid_resolution
        self.bt_grid_resolution = bt_grid_resolution
        self.nodata_value = nodata_value
        self.swath_padding = swath_padding
        band_groups = {}
        if self.reflectance_bands:
            band_groups["SLSTR_RBT_REFLECTANCE"] = self.reflectance_bands
        if self.bt_bands:
            band_groups["SLSTR_RBT_BT"] = self.bt_bands
        super().__init__(
            glob_to_bands=band_groups,
            query_filter=_sentinel3_query_filter("SLSTR", self.PRODUCT_TYPE),
            context=context,
            **kwargs,
        )

    def _process_product_zip(
        self,
        tile_store: TileStoreWithLayer,
        item: CopernicusItem,
        local_zip_fname: str,
        geometries: list[STGeometry] | None = None,
    ) -> None:
        """Convert and ingest requested SLSTR reflectance and thermal bands."""
        needs_reflectance = bool(
            self.reflectance_bands
        ) and not tile_store.is_raster_ready(item, self.reflectance_bands)
        needs_bt = bool(self.bt_bands) and not tile_store.is_raster_ready(
            item, self.bt_bands
        )
        if not needs_reflectance and not needs_bt:
            return

        required: set[str] = set()
        if needs_reflectance:
            required.update(
                {
                    "indices_an.nc",
                    "geometry_tn.nc",
                    "geodetic_an.nc",
                    "cartesian_tx.nc",
                    "cartesian_an.nc",
                }
            )
            for band_name in self.reflectance_bands:
                band = band_name.split("_")[0]
                required.update({f"{band}_radiance_an.nc", f"{band}_quality_an.nc"})
        if needs_bt:
            required.add("geodetic_in.nc")
            required.update(f"{band}_in.nc" for band in self.bt_bands)

        with tempfile.TemporaryDirectory() as tmp_dir, ZipFile(local_zip_fname) as zipf:
            paths = _extract_zip_members(zipf, required, tmp_dir)

            if needs_reflectance:
                detector = _read_netcdf_variable(
                    paths["indices_an.nc"], "detector_an"
                ).astype(np.float32)
                valid_detector = np.isfinite(detector) & (detector >= 0)
                safe_detector = np.clip(
                    np.nan_to_num(detector, nan=0.0).astype(np.int64), 0, None
                )
                image_x = _read_netcdf_variable(paths["cartesian_an.nc"], "x_an")
                image_y = _read_netcdf_variable(paths["cartesian_an.nc"], "y_an")
                solar_zenith = _interpolate_tie_points_xy(
                    _read_netcdf_variable(paths["geometry_tn.nc"], "solar_zenith_tn"),
                    _read_netcdf_variable(paths["cartesian_tx.nc"], "x_tx"),
                    _read_netcdf_variable(paths["cartesian_tx.nc"], "y_tx"),
                    image_x,
                    image_y,
                )
                cos_solar_zenith = np.clip(
                    np.cos(np.deg2rad(solar_zenith)), 0.01, None
                ).astype(np.float32)
                reflectance_arrays = []
                for band_name in self.reflectance_bands:
                    band = band_name.split("_")[0]
                    radiance_name = f"{band}_radiance_an"
                    irradiance_name = f"{band}_solar_irradiance_an"
                    radiance = _read_netcdf_variable(
                        paths[f"{radiance_name}.nc"], radiance_name
                    ).astype(np.float32)
                    detector_irradiance = _read_netcdf_variable(
                        paths[f"{band}_quality_an.nc"], irradiance_name
                    ).astype(np.float32)
                    band_detector = np.clip(
                        safe_detector, 0, detector_irradiance.size - 1
                    )
                    reflectance = _radiance_to_reflectance(
                        radiance,
                        detector_irradiance.reshape(-1)[band_detector],
                        cos_solar_zenith,
                    )
                    reflectance_arrays.append(
                        np.where(valid_detector, reflectance, np.nan)
                    )
                reflectance_latitude = _read_netcdf_variable(
                    paths["geodetic_an.nc"], "latitude_an"
                ).astype(np.float32)
                reflectance_longitude = _read_netcdf_variable(
                    paths["geodetic_an.nc"], "longitude_an"
                ).astype(np.float32)

            if needs_bt:
                bt_arrays = []
                for band in self.bt_bands:
                    variable_name = f"{band}_in"
                    bt_arrays.append(
                        _read_netcdf_variable(
                            paths[f"{variable_name}.nc"], variable_name
                        ).astype(np.float32)
                    )
                bt_latitude = _read_netcdf_variable(
                    paths["geodetic_in.nc"], "latitude_in"
                ).astype(np.float32)
                bt_longitude = _read_netcdf_variable(
                    paths["geodetic_in.nc"], "longitude_in"
                ).astype(np.float32)

        if needs_reflectance:
            data, longitude, latitude = _crop_swath(
                np.stack(reflectance_arrays),
                reflectance_longitude,
                reflectance_latitude,
                geometries,
                self.swath_padding,
            )
            _write_swath(
                tile_store,
                item,
                self.reflectance_bands,
                data,
                longitude,
                latitude,
                self.reflectance_grid_resolution,
                self.nodata_value,
            )

        if needs_bt:
            data, longitude, latitude = _crop_swath(
                np.stack(bt_arrays),
                bt_longitude,
                bt_latitude,
                geometries,
                self.swath_padding,
            )
            _write_swath(
                tile_store,
                item,
                self.bt_bands,
                data,
                longitude,
                latitude,
                self.bt_grid_resolution,
                self.nodata_value,
            )