diff --git a/satpy/etc/readers/olci_l1b.yaml b/satpy/etc/readers/olci_l1b.yaml index d36279a262..5be42d7a7e 100644 --- a/satpy/etc/readers/olci_l1b.yaml +++ b/satpy/etc/readers/olci_l1b.yaml @@ -1,7 +1,7 @@ reader: name: olci_l1b short_name: OLCI Level 1b - long_name: Sentinel-3 A and B OLCI Level 1B data in netCDF4 format + long_name: Sentinel-3 OLCI Level 1B data in netCDF4 format description: NC Reader for OLCI data status: Nominal supports_fsspec: true diff --git a/satpy/etc/readers/olci_l2.yaml b/satpy/etc/readers/olci_l2.yaml index 95cb024e72..f08475e254 100644 --- a/satpy/etc/readers/olci_l2.yaml +++ b/satpy/etc/readers/olci_l2.yaml @@ -1,7 +1,7 @@ reader: name: olci_l2 short_name: OLCI Level 2 - long_name: Sentinel-3 A and B OLCI Level 2 data in netCDF4 format + long_name: Sentinel-3 OLCI Level 2 data in netCDF4 format description: NC Reader for OLCI data status: Nominal supports_fsspec: true diff --git a/satpy/etc/readers/slstr_l1b.yaml b/satpy/etc/readers/slstr_l1b.yaml index 1ca76c5294..99fc19e444 100644 --- a/satpy/etc/readers/slstr_l1b.yaml +++ b/satpy/etc/readers/slstr_l1b.yaml @@ -1,7 +1,7 @@ reader: name: slstr_l1b short_name: SLSTR l1b - long_name: Sentinel-3 A and B SLSTR data in netCDF4 format + long_name: Sentinel-3 SLSTR data in netCDF4 format description: NC Reader for SLSTR data status: Alpha supports_fsspec: false @@ -57,19 +57,22 @@ reader: file_types: esa_l1b_refl: file_reader: !!python/name:satpy.readers.slstr_l1b.NCSLSTR1B - file_patterns: ['{mission_id:3s}_SL_{processing_level:1s}_{datatype_id:_<6s}_{start_time:%Y%m%dT%H%M%S}_{end_time:%Y%m%dT%H%M%S}_{creation_time:%Y%m%dT%H%M%S}_{duration:4d}_{cycle:3d}_{relative_orbit:3d}_{frame:4s}_{centre:3s}_{mode:1s}_{timeliness:2s}_{baseline:3d}.SEN3/{dataset_name}_radiance_{stripe:1s}{view:1s}.nc'] + file_patterns: ['{mission_id:3s}_SL_1_{datatype_id:_<6s}_{start_time:%Y%m%dT%H%M%S}_{end_time:%Y%m%dT%H%M%S}_{creation_time:%Y%m%dT%H%M%S}_{duration:4d}_{cycle:3d}_{relative_orbit:3d}_{frame:4s}_{centre:3s}_{mode:1s}_{timeliness:2s}_{baseline:3d}.SEN3/{dataset_name}_radiance_{stripe:1s}{view:1s}.nc'] esa_l1b_tir: file_reader: !!python/name:satpy.readers.slstr_l1b.NCSLSTR1B - file_patterns: ['{mission_id:3s}_SL_{processing_level:1s}_{datatype_id:_<6s}_{start_time:%Y%m%dT%H%M%S}_{end_time:%Y%m%dT%H%M%S}_{creation_time:%Y%m%dT%H%M%S}_{duration:4d}_{cycle:3d}_{relative_orbit:3d}_{frame:4s}_{centre:3s}_{mode:1s}_{timeliness:2s}_{baseline:3d}.SEN3/{dataset_name}_BT_{stripe:1s}{view:1s}.nc'] + file_patterns: ['{mission_id:3s}_SL_1_{datatype_id:_<6s}_{start_time:%Y%m%dT%H%M%S}_{end_time:%Y%m%dT%H%M%S}_{creation_time:%Y%m%dT%H%M%S}_{duration:4d}_{cycle:3d}_{relative_orbit:3d}_{frame:4s}_{centre:3s}_{mode:1s}_{timeliness:2s}_{baseline:3d}.SEN3/{dataset_name}_BT_{stripe:1s}{view:1s}.nc'] esa_angles: - file_reader: !!python/name:satpy.readers.slstr_l1b.NCSLSTRAngles - file_patterns: ['{mission_id:3s}_SL_{processing_level:1s}_{datatype_id:_<6s}_{start_time:%Y%m%dT%H%M%S}_{end_time:%Y%m%dT%H%M%S}_{creation_time:%Y%m%dT%H%M%S}_{duration:4d}_{cycle:3d}_{relative_orbit:3d}_{frame:4s}_{centre:3s}_{mode:1s}_{timeliness:2s}_{baseline:3d}.SEN3/geometry_t{view:1s}.nc'] + file_reader: !!python/name:satpy.readers.core.slstr.NCSLSTRAngles + file_patterns: ['{mission_id:3s}_SL_1_{datatype_id:_<6s}_{start_time:%Y%m%dT%H%M%S}_{end_time:%Y%m%dT%H%M%S}_{creation_time:%Y%m%dT%H%M%S}_{duration:4d}_{cycle:3d}_{relative_orbit:3d}_{frame:4s}_{centre:3s}_{mode:1s}_{timeliness:2s}_{baseline:3d}.SEN3/geometry_t{view:1s}.nc'] esa_geo: - file_reader: !!python/name:satpy.readers.slstr_l1b.NCSLSTRGeo - file_patterns: ['{mission_id:3s}_SL_{processing_level:1s}_{datatype_id:_<6s}_{start_time:%Y%m%dT%H%M%S}_{end_time:%Y%m%dT%H%M%S}_{creation_time:%Y%m%dT%H%M%S}_{duration:4d}_{cycle:3d}_{relative_orbit:3d}_{frame:4s}_{centre:3s}_{mode:1s}_{timeliness:2s}_{baseline:3d}.SEN3/geodetic_{stripe:1s}{view:1s}.nc'] + file_reader: !!python/name:satpy.readers.core.slstr.NCSLSTRGeo + file_patterns: ['{mission_id:3s}_SL_1_{datatype_id:_<6s}_{start_time:%Y%m%dT%H%M%S}_{end_time:%Y%m%dT%H%M%S}_{creation_time:%Y%m%dT%H%M%S}_{duration:4d}_{cycle:3d}_{relative_orbit:3d}_{frame:4s}_{centre:3s}_{mode:1s}_{timeliness:2s}_{baseline:3d}.SEN3/geodetic_{stripe:1s}{view:1s}.nc'] esa_l1b_flag: - file_reader: !!python/name:satpy.readers.slstr_l1b.NCSLSTRFlag - file_patterns: ['{mission_id:3s}_SL_{processing_level:1s}_{datatype_id:_<6s}_{start_time:%Y%m%dT%H%M%S}_{end_time:%Y%m%dT%H%M%S}_{creation_time:%Y%m%dT%H%M%S}_{duration:4d}_{cycle:3d}_{relative_orbit:3d}_{frame:4s}_{centre:3s}_{mode:1s}_{timeliness:2s}_{baseline:3d}.SEN3/flags_{stripe:1s}{view:1s}.nc'] + file_reader: !!python/name:satpy.readers.core.slstr.NCSLSTRFlag + file_patterns: ['{mission_id:3s}_SL_1_{datatype_id:_<6s}_{start_time:%Y%m%dT%H%M%S}_{end_time:%Y%m%dT%H%M%S}_{creation_time:%Y%m%dT%H%M%S}_{duration:4d}_{cycle:3d}_{relative_orbit:3d}_{frame:4s}_{centre:3s}_{mode:1s}_{timeliness:2s}_{baseline:3d}.SEN3/flags_{stripe:1s}{view:1s}.nc'] + esa_l1b_cartesian: + file_reader: !!python/name:satpy.readers.core.slstr.NCSLSTRGeo + file_patterns: ['{mission_id:3s}_SL_1_{datatype_id:_<6s}_{start_time:%Y%m%dT%H%M%S}_{end_time:%Y%m%dT%H%M%S}_{creation_time:%Y%m%dT%H%M%S}_{duration:4d}_{cycle:3d}_{relative_orbit:3d}_{frame:4s}_{centre:3s}_{mode:1s}_{timeliness:2s}_{baseline:3d}.SEN3/cartesian_{stripe:1s}{view:1s}.nc'] datasets: longitude: @@ -82,6 +85,16 @@ datasets: standard_name: longitude units: degree + longitude_orphan: + name: longitude_orphan + resolution: [500, 1000] + view: [nadir, oblique] + stripe: [a, b, i, f] + file_type: esa_geo + file_key: longitude_orphan_{stripe:1s}{view:1s} + standard_name: longitude + units: degree + latitude: name: latitude resolution: [500, 1000] @@ -92,6 +105,16 @@ datasets: standard_name: latitude units: degree + latitude_orphan: + name: latitude_orphan + resolution: [500, 1000] + view: [nadir, oblique] + stripe: [a, b, i, f] + file_type: esa_geo + file_key: latitude_orphan_{stripe:1s}{view:1s} + standard_name: latitude_orphan + units: degree + elevation: name: elevation resolution: [500, 1000] @@ -102,6 +125,56 @@ datasets: standard_name: elevation units: m + elevation_orphan: + name: elevation_orphan + resolution: [500, 1000] + view: [nadir, oblique] + stripe: [a, b, i, f] + file_type: esa_geo + file_key: elevation_{stripe:1s}{view:1s} + standard_name: elevation_orphan + units: m + + x_coords: + name: x_coords + resolution: [500, 1000] + view: [nadir, oblique] + stripe: [a, b, i, f] + file_type: esa_geo + file_key: x_{stripe:1s}{view:1s} + standard_name: x_coords + units: m + + y_coords: + name: y_coords + resolution: [500, 1000] + view: [nadir, oblique] + stripe: [a, b, i, f] + file_type: esa_geo + file_key: y_{stripe:1s}{view:1s} + standard_name: y_coords + units: m + + x_orphan_coords: + name: x_orphan_coords + resolution: [500, 1000] + view: [nadir, oblique] + stripe: [a, b, i, f] + file_type: esa_geo + file_key: x_orphan_{stripe:1s}{view:1s} + standard_name: x_orphan_coords + units: m + + y_orphan_coords: + name: y_orphan_coords + resolution: [500, 1000] + view: [nadir, oblique] + stripe: [a, b, i, f] + file_type: esa_geo + file_key: y_orphan_{stripe:1s}{view:1s} + standard_name: y_orphan_coords + units: m + # The channels S1-S3 are available in nadir (default) and oblique view. S1: name: S1 diff --git a/satpy/etc/readers/slstr_l2.yaml b/satpy/etc/readers/slstr_l2.yaml new file mode 100644 index 0000000000..4252324750 --- /dev/null +++ b/satpy/etc/readers/slstr_l2.yaml @@ -0,0 +1,245 @@ +reader: + name: slstr_l2 + short_name: SLSTR l2 + long_name: Sentinel-3 SLSTR Level-2 data in netCDF4 format + description: NC Reader for SLSTR L2 data + status: Alpha + supports_fsspec: false + sensors: [slstr] + reader: !!python/name:satpy.readers.core.yaml_reader.FileYAMLReader + data_identification_keys: + name: + required: true + wavelength: + type: !!python/name:satpy.dataset.dataid.WavelengthRange + resolution: + transitive: false + calibration: + enum: + - reflectance + - brightness_temperature + - radiance + - counts + transitive: true + view: + enum: + - nadir + - oblique + transitive: true + stripe: + enum: + - a + - b + - i + - f + modifiers: + default: [] + type: !!python/name:satpy.dataset.dataid.ModifierTuple + + coord_identification_keys: + name: + required: true + resolution: + transitive: false + view: + enum: + - nadir + - oblique + transitive: true + stripe: + enum: + - a + - b + - i + - f + +file_types: + esa_lst: + file_reader: !!python/name:satpy.readers.slstr_l2_nc.NCSLSTRL2LST + file_patterns: + - '{mission_id:3s}_SL_2_LST____{start_time:%Y%m%dT%H%M%S}_{end_time:%Y%m%dT%H%M%S}_{creation_time:%Y%m%dT%H%M%S}_{duration:4d}_{cycle:3d}_{relative_orbit:3d}_{frame:4d}_{centre:3s}_{platform_mode:1s}_{timeliness:2s}_{collection:3s}.SEN3/LST_{stripe:1s}{view:1s}.nc' + esa_angles: + file_reader: !!python/name:satpy.readers.core.slstr.NCSLSTRAngles + file_patterns: + - '{mission_id:3s}_SL_2_{datatype_id:_<6s}_{start_time:%Y%m%dT%H%M%S}_{end_time:%Y%m%dT%H%M%S}_{creation_time:%Y%m%dT%H%M%S}_{duration:4d}_{cycle:3d}_{relative_orbit:3d}_{frame:4s}_{centre:3s}_{mode:1s}_{timeliness:2s}_{baseline:3d}.SEN3/geometry_{stripe:1s}{view:1s}.nc' + esa_geo: + file_reader: !!python/name:satpy.readers.core.slstr.NCSLSTRGeo + file_patterns: + - '{mission_id:3s}_SL_2_{datatype_id:_<6s}_{start_time:%Y%m%dT%H%M%S}_{end_time:%Y%m%dT%H%M%S}_{creation_time:%Y%m%dT%H%M%S}_{duration:4d}_{cycle:3d}_{relative_orbit:3d}_{frame:4s}_{centre:3s}_{mode:1s}_{timeliness:2s}_{baseline:3d}.SEN3/geodetic_{stripe:1s}{view:1s}.nc' + esa_cartesian: + file_reader: !!python/name:satpy.readers.core.slstr.NCSLSTRGeo + file_patterns: + - '{mission_id:3s}_SL_2_{datatype_id:_<6s}_{start_time:%Y%m%dT%H%M%S}_{end_time:%Y%m%dT%H%M%S}_{creation_time:%Y%m%dT%H%M%S}_{duration:4d}_{cycle:3d}_{relative_orbit:3d}_{frame:4s}_{centre:3s}_{mode:1s}_{timeliness:2s}_{baseline:3d}.SEN3/cartesian{stripe:1s}{view:1s}.nc' + +datasets: + longitude: + name: longitude + resolution: 1000 + view: nadir + stripe: i + file_type: esa_geo + file_key: longitude_{stripe:1s}{view:1s} + standard_name: longitude + units: degree + + longitude_orphan: + name: longitude_orphan + resolution: 1000 + view: nadir + stripe: i + file_type: esa_geo + file_key: longitude_orphan_{stripe:1s}{view:1s} + standard_name: longitude + units: degree + + latitude: + name: latitude + resolution: 1000 + view: nadir + stripe: i + file_type: esa_geo + file_key: latitude_{stripe:1s}{view:1s} + standard_name: latitude + units: degree + + latitude_orphan: + name: latitude_orphan + resolution: 1000 + view: nadir + stripe: i + file_type: esa_geo + file_key: latitude_orphan_{stripe:1s}{view:1s} + standard_name: latitude_orphan + units: degree + + elevation: + name: elevation + resolution: 1000 + view: nadir + stripe: i + file_type: esa_geo + file_key: elevation_{stripe:1s}{view:1s} + standard_name: elevation + units: m + + elevation_orphan: + name: elevation_orphan + resolution: 1000 + view: nadir + stripe: i + file_type: esa_geo + file_key: elevation_{stripe:1s}{view:1s} + standard_name: elevation_orphan + units: m + + x_coords: + name: x_coords + resolution: 1000 + view: nadir + stripe: i + file_type: esa_geo + file_key: x_{stripe:1s}{view:1s} + standard_name: x_coords + units: m + + y_coords: + name: y_coords + resolution: 1000 + view: nadir + stripe: i + file_type: esa_geo + file_key: y_{stripe:1s}{view:1s} + standard_name: y_coords + units: m + + x_orphan_coords: + name: x_orphan_coords + resolution: 1000 + view: nadir + stripe: i + file_type: esa_geo + file_key: x_orphan_{stripe:1s}{view:1s} + standard_name: x_orphan_coords + units: m + + y_orphan_coords: + name: y_orphan_coords + resolution: 1000 + view: nadir + stripe: i + file_type: esa_geo + file_key: y_orphan_{stripe:1s}{view:1s} + standard_name: y_orphan_coords + units: m + + LST: + name: LST + sensor: slstr + stripe: i + view: nadir + resolution: 1000 + standard_name: land_surface_temperature + units: K + coordinates: [longitude, latitude] + file_type: esa_lst + file_key: LST + + LST_orphan: + name: LST_orphan + sensor: slstr + stripe: i + view: nadir + resolution: 1000 + standard_name: land_surface_temperature + units: K + coordinates: [longitude_orphan, latitude_orphan] + file_type: esa_lst + file_key: LST_orphan + + LST_exception: + name: LST_exception + sensor: slstr + stripe: i + view: nadir + resolution: 1000 + standard_name: LST_quality_flag + units: 1 + coordinates: [longitude, latitude] + file_type: esa_lst + file_key: exception + + LST_exception_orphan: + name: LST_exception_orphan + sensor: slstr + stripe: i + view: nadir + resolution: 1000 + standard_name: LST_quality_flag + units: 1 + coordinates: [longitude_orphan, latitude_orphan] + file_type: esa_lst + file_key: exception_orphan + + LST_uncertainty: + name: LST_uncertainty + sensor: slstr + stripe: i + view: nadir + resolution: 1000 + standard_name: LST_uncertainty + units: K + coordinates: [longitude, latitude] + file_type: esa_lst + file_key: LST_uncertainty + + LST_uncertainty_orphan: + name: LST_uncertainty_orphan + sensor: slstr + stripe: i + view: nadir + resolution: 1000 + standard_name: LST_uncertainty + units: K + coordinates: [longitude_orphan, latitude_orphan] + file_type: esa_lst + file_key: LST_uncertainty_orphan diff --git a/satpy/readers/core/config.py b/satpy/readers/core/config.py index 34abc56d0a..3363c55b95 100644 --- a/satpy/readers/core/config.py +++ b/satpy/readers/core/config.py @@ -31,9 +31,7 @@ # Old Name -> New Name PENDING_OLD_READER_NAMES = {"fci_l1c_fdhsi": "fci_l1c_nc", "viirs_l2_cloud_mask_nc": "viirs_edr"} -OLD_READER_NAMES: dict[str, str] = { - "slstr_l2": "ghrsst_l2", -} +OLD_READER_NAMES: dict[str, str] = {} def read_reader_config(config_files, loader=UnsafeLoader): diff --git a/satpy/readers/core/slstr.py b/satpy/readers/core/slstr.py new file mode 100644 index 0000000000..c054670d71 --- /dev/null +++ b/satpy/readers/core/slstr.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2016-2020 Satpy developers +# +# This file is part of satpy. +# +# satpy is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# satpy is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# satpy. If not, see . + +"""SLSTR common (L1+L2) reader functions.""" + +import datetime as dt +import logging +import os + +import dask.array as da +import numpy as np +import xarray as xr + +from satpy.readers.core.file_handlers import BaseFileHandler +from satpy.utils import get_legacy_chunk_size + +logger = logging.getLogger(__name__) + +CHUNK_SIZE = get_legacy_chunk_size() + +PLATFORM_NAMES = {"S3A": "Sentinel-3A", + "S3B": "Sentinel-3B", + "S3C": "Sentinel-3C", + "S3D": "Sentinel-3D"} + +# These are the default channel adjustment factors. +# Defined in the product notice: S3.PN-SLSTR-L1.10 +# https://user.eumetsat.int/s3/eup-strapi-media/S3_PN_SLSTR_L1_10_i1r0_SLSTR_L1_PB_SL_L1_004_05_00_fbf01b8813.pdf +CHANCALIB_FACTORS = {"S1_nadir": 0.97, + "S2_nadir": 0.98, + "S3_nadir": 0.98, + "S4_nadir": 1.0, + "S5_nadir": 1.11, + "S6_nadir": 1.13, + "S7_nadir": 1.0, + "S8_nadir": 1.0, + "S9_nadir": 1.0, + "S1_oblique": 0.94, + "S2_oblique": 0.95, + "S3_oblique": 0.95, + "S4_oblique": 1.0, + "S5_oblique": 1.04, + "S6_oblique": 1.07, + "S7_oblique": 1.0, + "S8_oblique": 1.0, + "S9_oblique": 1.0, } + + +class NCSLSTRGeo(BaseFileHandler): + """Filehandler for geo info.""" + + def __init__(self, filename, filename_info, filetype_info): + """Initialize the geo filehandler.""" + super(NCSLSTRGeo, self).__init__(filename, filename_info, + filetype_info) + self.nc = xr.open_dataset(self.filename, + decode_cf=True, + mask_and_scale=True, + chunks={"columns": CHUNK_SIZE, + "rows": CHUNK_SIZE}) + self.nc = self.nc.rename({"columns": "x", "rows": "y"}) + + self.cache = {} + + def get_dataset(self, key, info): + """Load a dataset.""" + logger.debug("Reading %s.", key["name"]) + file_key = info["file_key"].format(view=key["view"].name[0], + stripe=key["stripe"].name) + try: + variable = self.nc[file_key] + except KeyError: + return + + info = info.copy() + info.update(variable.attrs) + + variable.attrs = info + return variable + + @property + def start_time(self): + """Get the start time.""" + return dt.datetime.strptime(self.nc.attrs["start_time"], "%Y-%m-%dT%H:%M:%S.%fZ") + + @property + def end_time(self): + """Get the end time.""" + return dt.datetime.strptime(self.nc.attrs["stop_time"], "%Y-%m-%dT%H:%M:%S.%fZ") + + +class NCSLSTRAngles(BaseFileHandler): + """Filehandler for angles.""" + + def _loadcart(self, fname): + """Load a cartesian file of appropriate type.""" + cartf = xr.open_dataset(fname, + decode_cf=True, + mask_and_scale=True, + chunks={"columns": CHUNK_SIZE, + "rows": CHUNK_SIZE}) + return cartf + + def __init__(self, filename, filename_info, filetype_info): + """Initialize the angles reader.""" + super(NCSLSTRAngles, self).__init__(filename, filename_info, + filetype_info) + + self.nc = xr.open_dataset(self.filename, + decode_cf=True, + mask_and_scale=True, + chunks={"columns": CHUNK_SIZE, + "rows": CHUNK_SIZE}) + + # TODO: get metadata from the manifest file (xfdumanifest.xml) + self.platform_name = PLATFORM_NAMES[filename_info["mission_id"]] + self.sensor = "slstr" + self.view = filename_info["view"] + self._start_time = filename_info["start_time"] + self._end_time = filename_info["end_time"] + + carta_file = os.path.join( + os.path.dirname(self.filename), "cartesian_a{}.nc".format(self.view[0])) + carti_file = os.path.join( + os.path.dirname(self.filename), "cartesian_i{}.nc".format(self.view[0])) + cartx_file = os.path.join( + os.path.dirname(self.filename), "cartesian_tx.nc") + if os.path.exists(carta_file): + self.carta = self._loadcart(carta_file) + if os.path.exists(carti_file): + self.carti = self._loadcart(carti_file) + if os.path.exists(cartx_file): + self.cartx = self._loadcart(cartx_file) + + @staticmethod + def _interp_data(indata, full_grid, tie_grid, ds_name): + """Interpolate data from tie point grid to full image grid.""" + from scipy.interpolate import RectBivariateSpline + + # Check if we are interpolating angles + if "angle" in ds_name: + # If we are interpolating the angles, we need to do so with the sine and cosine to prevent + # interpolation artifacts such as values <0 or >360. + indat = indata[:, ::-1] + sin_angles = np.sin(np.radians(indat)) + cos_angles = np.cos(np.radians(indat)) + sin_interp = RectBivariateSpline(tie_grid[1], tie_grid[0], sin_angles) + cos_interp = RectBivariateSpline(tie_grid[1], tie_grid[0], cos_angles) + values_sin = sin_interp.ev(full_grid[1], full_grid[0]) + values_cos = cos_interp.ev(full_grid[1], full_grid[0]) + values = np.degrees(np.arctan2(values_sin, values_cos)) % 360 + else: + # Otherwise, interpolate as normal. + spl = RectBivariateSpline(tie_grid[1], tie_grid[0], indata[:, ::-1]) + values = spl.ev(full_grid[1], full_grid[0]) + return values + + + def get_dataset(self, key, info): + """Load a dataset.""" + if not key["view"].name.startswith(self.view[0]): + return + logger.debug("Reading %s.", key["name"]) + # Check if file_key is specified in the yaml + file_key = info["file_key"].format(view=key["view"].name[0]) + + variable = self.nc[file_key] + l_step = self.nc.attrs.get("al_subsampling_factor", 1) + c_step = self.nc.attrs.get("ac_subsampling_factor", 16) + + if key.get("resolution", 1000) == 500: + l_step *= 2 + c_step *= 2 + + if c_step != 1 or l_step != 1: + logger.debug("Interpolating %s.", key["name"]) + # TODO: do it in cartesian coordinates ! pbs at date line and + # possible + tie_x = self.cartx["x_tx"].data[0, :][::-1] + tie_y = self.cartx["y_tx"].data[:, 0] + if key.get("resolution", 1000) == 500: + full_x = self.carta["x_a" + self.view[0]].data + full_y = self.carta["y_a" + self.view[0]].data + else: + full_x = self.carti["x_i" + self.view[0]].data + full_y = self.carti["y_i" + self.view[0]].data + + variable = variable.fillna(0) + variable.attrs["resolution"] = key.get("resolution", 1000) + + # Interpolate the data to the full image grid + values = self._interp_data(variable.data, + [full_x, full_y], + [tie_x, tie_y], + key["name"]) + + variable = xr.DataArray(da.from_array(values, chunks=(CHUNK_SIZE, CHUNK_SIZE)), + dims=["y", "x"], attrs=variable.attrs) + + variable.attrs["platform_name"] = self.platform_name + variable.attrs["sensor"] = self.sensor + + if "units" not in variable.attrs: + variable.attrs["units"] = "degrees" + + variable.attrs.update(key.to_dict()) + + return variable + + @property + def start_time(self): + """Get the start time.""" + return dt.datetime.strptime(self.nc.attrs["start_time"], "%Y-%m-%dT%H:%M:%S.%fZ") + + @property + def end_time(self): + """Get the end time.""" + return dt.datetime.strptime(self.nc.attrs["stop_time"], "%Y-%m-%dT%H:%M:%S.%fZ") + + +class NCSLSTRFlag(BaseFileHandler): + """File handler for flags.""" + + def __init__(self, filename, filename_info, filetype_info): + """Initialize the flag reader.""" + super(NCSLSTRFlag, self).__init__(filename, filename_info, + filetype_info) + self.nc = xr.open_dataset(self.filename, + decode_cf=True, + mask_and_scale=True, + chunks={"columns": CHUNK_SIZE, + "rows": CHUNK_SIZE}) + self.nc = self.nc.rename({"columns": "x", "rows": "y"}) + self.stripe = filename_info["stripe"] + views = {"n": "nadir", "o": "oblique"} + self.view = views[filename_info["view"]] + # TODO: get metadata from the manifest file (xfdumanifest.xml) + self.platform_name = PLATFORM_NAMES[filename_info["mission_id"]] + self.sensor = "slstr" + + def get_dataset(self, key, info): + """Load a dataset.""" + if (self.stripe != key["stripe"].name or + self.view != key["view"].name): + return + logger.debug("Reading %s.", key["name"]) + file_key = info["file_key"].format(view=key["view"].name[0], + stripe=key["stripe"].name) + variable = self.nc[file_key] + + info = info.copy() + info.update(variable.attrs) + info.update(key.to_dict()) + info.update(dict(platform_name=self.platform_name, + sensor=self.sensor)) + + variable.attrs = info + return variable + + @property + def start_time(self): + """Get the start time.""" + return dt.datetime.strptime(self.nc.attrs["start_time"], "%Y-%m-%dT%H:%M:%S.%fZ") + + @property + def end_time(self): + """Get the end time.""" + return dt.datetime.strptime(self.nc.attrs["stop_time"], "%Y-%m-%dT%H:%M:%S.%fZ") diff --git a/satpy/readers/slstr_l1b.py b/satpy/readers/slstr_l1b.py index fb29732b32..bbab60816e 100644 --- a/satpy/readers/slstr_l1b.py +++ b/satpy/readers/slstr_l1b.py @@ -29,83 +29,13 @@ import xarray as xr from satpy.readers.core.file_handlers import BaseFileHandler +from satpy.readers.core.slstr import CHANCALIB_FACTORS, PLATFORM_NAMES from satpy.utils import get_legacy_chunk_size logger = logging.getLogger(__name__) CHUNK_SIZE = get_legacy_chunk_size() -PLATFORM_NAMES = {"S3A": "Sentinel-3A", - "S3B": "Sentinel-3B", - "S3C": "Sentinel-3C", - "S3D": "Sentinel-3D"} - -# These are the default channel adjustment factors. -# Defined in the product notice: S3.PN-SLSTR-L1.10 -# https://user.eumetsat.int/s3/eup-strapi-media/S3_PN_SLSTR_L1_10_i1r0_SLSTR_L1_PB_SL_L1_004_05_00_fbf01b8813.pdf -CHANCALIB_FACTORS = {"S1_nadir": 0.97, - "S2_nadir": 0.98, - "S3_nadir": 0.98, - "S4_nadir": 1.0, - "S5_nadir": 1.11, - "S6_nadir": 1.13, - "S7_nadir": 1.0, - "S8_nadir": 1.0, - "S9_nadir": 1.0, - "S1_oblique": 0.94, - "S2_oblique": 0.95, - "S3_oblique": 0.95, - "S4_oblique": 1.0, - "S5_oblique": 1.04, - "S6_oblique": 1.07, - "S7_oblique": 1.0, - "S8_oblique": 1.0, - "S9_oblique": 1.0, } - - -class NCSLSTRGeo(BaseFileHandler): - """Filehandler for geo info.""" - - def __init__(self, filename, filename_info, filetype_info): - """Initialize the geo filehandler.""" - super(NCSLSTRGeo, self).__init__(filename, filename_info, - filetype_info) - self.nc = xr.open_dataset(self.filename, - decode_cf=True, - mask_and_scale=True, - chunks={"columns": CHUNK_SIZE, - "rows": CHUNK_SIZE}) - self.nc = self.nc.rename({"columns": "x", "rows": "y"}) - - self.cache = {} - - def get_dataset(self, key, info): - """Load a dataset.""" - logger.debug("Reading %s.", key["name"]) - file_key = info["file_key"].format(view=key["view"].name[0], - stripe=key["stripe"].name) - try: - variable = self.nc[file_key] - except KeyError: - return - - info = info.copy() - info.update(variable.attrs) - - variable.attrs = info - return variable - - @property - def start_time(self): - """Get the start time.""" - return dt.datetime.strptime(self.nc.attrs["start_time"], "%Y-%m-%dT%H:%M:%S.%fZ") - - @property - def end_time(self): - """Get the end time.""" - return dt.datetime.strptime(self.nc.attrs["stop_time"], "%Y-%m-%dT%H:%M:%S.%fZ") - - class NCSLSTR1B(BaseFileHandler): """Filehandler for l1 SLSTR data. @@ -237,179 +167,3 @@ def start_time(self): def end_time(self): """Get the end time.""" return dt.datetime.strptime(self.nc.attrs["stop_time"], "%Y-%m-%dT%H:%M:%S.%fZ") - - -class NCSLSTRAngles(BaseFileHandler): - """Filehandler for angles.""" - - def _loadcart(self, fname): - """Load a cartesian file of appropriate type.""" - cartf = xr.open_dataset(fname, - decode_cf=True, - mask_and_scale=True, - chunks={"columns": CHUNK_SIZE, - "rows": CHUNK_SIZE}) - return cartf - - def __init__(self, filename, filename_info, filetype_info): - """Initialize the angles reader.""" - super(NCSLSTRAngles, self).__init__(filename, filename_info, - filetype_info) - - self.nc = xr.open_dataset(self.filename, - decode_cf=True, - mask_and_scale=True, - chunks={"columns": CHUNK_SIZE, - "rows": CHUNK_SIZE}) - - # TODO: get metadata from the manifest file (xfdumanifest.xml) - self.platform_name = PLATFORM_NAMES[filename_info["mission_id"]] - self.sensor = "slstr" - self.view = filename_info["view"] - self._start_time = filename_info["start_time"] - self._end_time = filename_info["end_time"] - - carta_file = os.path.join( - os.path.dirname(self.filename), "cartesian_a{}.nc".format(self.view[0])) - carti_file = os.path.join( - os.path.dirname(self.filename), "cartesian_i{}.nc".format(self.view[0])) - cartx_file = os.path.join( - os.path.dirname(self.filename), "cartesian_tx.nc") - self.carta = self._loadcart(carta_file) - self.carti = self._loadcart(carti_file) - self.cartx = self._loadcart(cartx_file) - - @staticmethod - def _interp_data(indata, full_grid, tie_grid, ds_name): - """Interpolate data from tie point grid to full image grid.""" - from scipy.interpolate import RectBivariateSpline - - # Check if we are interpolating angles - if "angle" in ds_name: - # If we are interpolating the angles, we need to do so with the sine and cosine to prevent - # interpolation artifacts such as values <0 or >360. - indat = indata[:, ::-1] - sin_angles = np.sin(np.radians(indat)) - cos_angles = np.cos(np.radians(indat)) - sin_interp = RectBivariateSpline(tie_grid[1], tie_grid[0], sin_angles) - cos_interp = RectBivariateSpline(tie_grid[1], tie_grid[0], cos_angles) - values_sin = sin_interp.ev(full_grid[1], full_grid[0]) - values_cos = cos_interp.ev(full_grid[1], full_grid[0]) - values = np.degrees(np.arctan2(values_sin, values_cos)) % 360 - else: - # Otherwise, interpolate as normal. - spl = RectBivariateSpline(tie_grid[1], tie_grid[0], indata[:, ::-1]) - values = spl.ev(full_grid[1], full_grid[0]) - return values - - - def get_dataset(self, key, info): - """Load a dataset.""" - if not key["view"].name.startswith(self.view[0]): - return - logger.debug("Reading %s.", key["name"]) - # Check if file_key is specified in the yaml - file_key = info["file_key"].format(view=key["view"].name[0]) - - variable = self.nc[file_key] - l_step = self.nc.attrs.get("al_subsampling_factor", 1) - c_step = self.nc.attrs.get("ac_subsampling_factor", 16) - - if key.get("resolution", 1000) == 500: - l_step *= 2 - c_step *= 2 - - if c_step != 1 or l_step != 1: - logger.debug("Interpolating %s.", key["name"]) - # TODO: do it in cartesian coordinates ! pbs at date line and - # possible - tie_x = self.cartx["x_tx"].data[0, :][::-1] - tie_y = self.cartx["y_tx"].data[:, 0] - if key.get("resolution", 1000) == 500: - full_x = self.carta["x_a" + self.view[0]].data - full_y = self.carta["y_a" + self.view[0]].data - else: - full_x = self.carti["x_i" + self.view[0]].data - full_y = self.carti["y_i" + self.view[0]].data - - variable = variable.fillna(0) - variable.attrs["resolution"] = key.get("resolution", 1000) - - # Interpolate the data to the full image grid - values = self._interp_data(variable.data, - [full_x, full_y], - [tie_x, tie_y], - key["name"]) - - variable = xr.DataArray(da.from_array(values, chunks=(CHUNK_SIZE, CHUNK_SIZE)), - dims=["y", "x"], attrs=variable.attrs) - - variable.attrs["platform_name"] = self.platform_name - variable.attrs["sensor"] = self.sensor - - if "units" not in variable.attrs: - variable.attrs["units"] = "degrees" - - variable.attrs.update(key.to_dict()) - - return variable - - @property - def start_time(self): - """Get the start time.""" - return dt.datetime.strptime(self.nc.attrs["start_time"], "%Y-%m-%dT%H:%M:%S.%fZ") - - @property - def end_time(self): - """Get the end time.""" - return dt.datetime.strptime(self.nc.attrs["stop_time"], "%Y-%m-%dT%H:%M:%S.%fZ") - - -class NCSLSTRFlag(BaseFileHandler): - """File handler for flags.""" - - def __init__(self, filename, filename_info, filetype_info): - """Initialize the flag reader.""" - super(NCSLSTRFlag, self).__init__(filename, filename_info, - filetype_info) - self.nc = xr.open_dataset(self.filename, - decode_cf=True, - mask_and_scale=True, - chunks={"columns": CHUNK_SIZE, - "rows": CHUNK_SIZE}) - self.nc = self.nc.rename({"columns": "x", "rows": "y"}) - self.stripe = filename_info["stripe"] - views = {"n": "nadir", "o": "oblique"} - self.view = views[filename_info["view"]] - # TODO: get metadata from the manifest file (xfdumanifest.xml) - self.platform_name = PLATFORM_NAMES[filename_info["mission_id"]] - self.sensor = "slstr" - - def get_dataset(self, key, info): - """Load a dataset.""" - if (self.stripe != key["stripe"].name or - self.view != key["view"].name): - return - logger.debug("Reading %s.", key["name"]) - file_key = info["file_key"].format(view=key["view"].name[0], - stripe=key["stripe"].name) - variable = self.nc[file_key] - - info = info.copy() - info.update(variable.attrs) - info.update(key.to_dict()) - info.update(dict(platform_name=self.platform_name, - sensor=self.sensor)) - - variable.attrs = info - return variable - - @property - def start_time(self): - """Get the start time.""" - return dt.datetime.strptime(self.nc.attrs["start_time"], "%Y-%m-%dT%H:%M:%S.%fZ") - - @property - def end_time(self): - """Get the end time.""" - return dt.datetime.strptime(self.nc.attrs["stop_time"], "%Y-%m-%dT%H:%M:%S.%fZ") diff --git a/satpy/readers/slstr_l2_nc.py b/satpy/readers/slstr_l2_nc.py new file mode 100644 index 0000000000..961c5e23fe --- /dev/null +++ b/satpy/readers/slstr_l2_nc.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2016-2020 Satpy developers +# +# This file is part of satpy. +# +# satpy is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# satpy is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# satpy. If not, see . + +"""SLSTR L2 reader.""" + +import datetime as dt +import logging + +import xarray as xr + +from satpy.readers.core.file_handlers import BaseFileHandler +from satpy.readers.core.slstr import PLATFORM_NAMES +from satpy.utils import get_legacy_chunk_size + +logger = logging.getLogger(__name__) + +CHUNK_SIZE = get_legacy_chunk_size() + +class NCSLSTRL2LST(BaseFileHandler): + """Filehandler for L2 SLSTR Land Surface Temperature (LST) data.""" + + def __init__(self, filename, filename_info, filetype_info, + user_calibration=None): + """Initialize the SLSTR l2 data filehandler.""" + super(NCSLSTRL2LST, self).__init__(filename, filename_info, + filetype_info) + + self.nc = xr.open_dataset(self.filename, + decode_cf=True, + mask_and_scale=True, + chunks={"columns": CHUNK_SIZE, + "rows": CHUNK_SIZE}) + self.nc = self.nc.rename({"columns": "x", "rows": "y"}) + self.stripe = filename_info["stripe"] + views = {"n": "nadir", "o": "oblique"} + self.view = views[filename_info["view"]] + + self.platform_name = PLATFORM_NAMES[filename_info["mission_id"]] + self.sensor = "slstr" + + def get_dataset(self, key, info): + """Load a dataset.""" + if (self.stripe != key["stripe"].name or self.view != key["view"].name): + return + file_key = info["file_key"] + logger.debug("Reading %s.", key["name"]) + data = self.nc[f"{file_key}"] + + units = data.attrs["units"] + + info = info.copy() + info.update(data.attrs) + info.update(key.to_dict()) + info.update(dict(units=units, + platform_name=self.platform_name, + sensor=self.sensor, + view=self.view)) + + data.attrs = info + return data + + @property + def start_time(self): + """Get the start time.""" + return dt.datetime.strptime(self.nc.attrs["start_time"], "%Y-%m-%dT%H:%M:%S.%fZ") + + @property + def end_time(self): + """Get the end time.""" + return dt.datetime.strptime(self.nc.attrs["stop_time"], "%Y-%m-%dT%H:%M:%S.%fZ") diff --git a/satpy/tests/reader_tests/test_slstr_common.py b/satpy/tests/reader_tests/test_slstr_common.py new file mode 100644 index 0000000000..25d365bd87 --- /dev/null +++ b/satpy/tests/reader_tests/test_slstr_common.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2018 Satpy developers +# +# This file is part of satpy. +# +# satpy is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# satpy is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# satpy. If not, see . + +"""Module for testing the satpy.readers.nc_slstr module.""" + +import datetime as dt +import unittest +import unittest.mock as mock + +import numpy as np +import xarray as xr + +from satpy.dataset.dataid import DataID, ModifierTuple, WavelengthRange +from satpy.readers.core.slstr import NCSLSTRAngles, NCSLSTRFlag, NCSLSTRGeo + +local_id_keys_config = { + "name": { + "required": True, + }, + "wavelength": { + "type": WavelengthRange, + }, + "resolution": None, + "calibration": { + "enum": [ + "reflectance", + "brightness_temperature", + "radiance", + "counts", + ], + }, + "stripe": { + "enum": [ + "a", + "b", + "c", + "i", + "f", + ], + }, + "view": { + "enum": [ + "nadir", + "oblique", + ], + }, + "modifiers": { + "required": True, + "default": ModifierTuple(), + "type": ModifierTuple, + }, +} + + +class TestSLSTRL1B(unittest.TestCase): + """Common setup for SLSTR_L1B tests.""" + + def _make_fake_dataset(self): + """Create a fake dataset for use in testing.""" + det = xr.DataArray( + self.base_data, + dims=("columns", "rows"), + attrs={ + "scale_factor": 1.0, + "add_offset": 0.0, + "_FillValue": 255, + }, + ) + + x_in = xr.DataArray(self.ix_data, dims=("columns", "rows")) + y_in = xr.DataArray(self.iy_data, dims=("columns", "rows")) + + saa = xr.DataArray(self.ang_data, dims=("columns_t", "rows_t")) + x_tx = xr.DataArray(self.tx_data, dims=("columns_t", "rows_t")) + y_tx = xr.DataArray(self.ty_data, dims=("columns_t", "rows_t")) + + self.fake_dataset = xr.Dataset( + data_vars={ + "S5_radiance_an": self.rad, + "S9_BT_ao": self.rad, + "foo_radiance_an": self.rad, + "S5_solar_irradiances": self.rad, + "geometry_tn": self.rad, + "latitude_an": self.rad, + "x_in": x_in, + "y_in": y_in, + "x_an": x_in, + "y_an": y_in, + "flags_an": self.rad, + "detector_an": det, + "x_tx": x_tx, + "y_tx": y_tx, + "solar_azimuth_tn": saa, + }, + attrs={ + "start_time": self.start_time, + "stop_time": self.end_time, + }, + ) + + def setUp(self): + """Create a fake dataset using the given radiance data.""" + self.base_data = np.array(( + [1., 2., 3., 4., 5., 6., 7., 8., 9.], + [7., 8., 9., 10., 11., 12., 13., 14., 15.], + [16., 17., 18., 19., 20., 21., 22., 23., 24.], + [25., 26., 27., 28., 29., 30., 31., 32., 33.], + )) + + self.ang_data = np.array(( + [345., 355., 1., 5., 10.], + [346., 355., 0.5, 4.2, 15.], + [342., 356., 1., 5.3, 12.], + [344.3, 356.1, 0.0001, 4.9, 9.2], + )) + + self.tx_data = np.array(( + [101000, 100500, 100000, 99500, 99000], + [101000, 100500, 100000, 99500, 99000], + [101000, 100500, 100000, 99500, 99000], + [101000, 100500, 100000, 99500, 99000], + )) + + self.ty_data = np.array(( + [199500, 199500, 199500, 199500, 199500], + [200000, 200000, 200000, 200000, 200000], + [200500, 200500, 200500, 200500, 200500], + [201000, 201000, 201000, 201000, 201000], + )) + + self.ix_data = np.array(( + [100800, 100600, 100400, 100200, 100000, 99800, 99600, 99400, 99200], + [100800, 100600, 100400, 100200, 100000, 99800, 99600, 99400, 99200], + [100800, 100600, 100400, 100200, 100000, 99800, 99600, 99400, 99200], + [100800, 100600, 100400, 100200, 100000, 99800, 99600, 99400, 99200], + )) + + self.iy_data = np.array(( + [199800, 199800, 199800, 199800, 199800, 199800, 199800, 199800, 199800], + [200000, 200000, 200000, 200000, 200000, 200000, 200000, 200000, 200000], + [200600, 200600, 200600, 200600, 200600, 200600, 200600, 200600, 200600], + [200800, 200800, 200800, 200800, 200800, 200800, 200800, 200800, 200800], + )) + + self.det_data = np.array(( + [0, 1, 1, 0, 1, 0, 1, 0, 1], + [1, 0, 0, 0, 0, 1, 0, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 0, 1], + [0, 1, 1, 0, 1, 1, 1, 1, 0], + )) + + self.start_time = "2020-05-10T12:01:15.585Z" + self.end_time = "2020-05-10T12:06:18.012Z" + + self.rad = xr.DataArray( + self.base_data, + dims=("columns", "rows"), + attrs={ + "scale_factor": 1.0, + "add_offset": 0.0, + "_FillValue": -32768, + "units": "mW.m-2.sr-1.nm-1", + }, + ) + + self._make_fake_dataset() + + + + +def make_dataid(**items): + """Make a data id.""" + return DataID(local_id_keys_config, **items) + + +class TestSLSTRReader(TestSLSTRL1B): + """Test various nc_slstr file handlers.""" + + class FakeSpl: + """Fake return function for SPL interpolation.""" + + @staticmethod + def ev(foo_x, foo_y): + """Fake function to return interpolated data.""" + return np.zeros((3, 2)) + + @mock.patch("satpy.readers.core.slstr.os.path.exists") + @mock.patch("satpy.readers.core.slstr.xr.open_dataset") + @mock.patch("scipy.interpolate.RectBivariateSpline") + def test_instantiate(self, bvs_, open_dataset_, exists_): + """Test initialization of file handlers.""" + bvs_.return_value = self.FakeSpl + open_dataset_.return_value = self.fake_dataset + exists_.side_effect = lambda path: "cartesian_" in path + + good_start = dt.datetime.strptime(self.start_time, "%Y-%m-%dT%H:%M:%S.%fZ") + good_end = dt.datetime.strptime(self.end_time, "%Y-%m-%dT%H:%M:%S.%fZ") + + ds_id = make_dataid( + name="foo", + calibration="radiance", + stripe="a", + view="nadir", + ) + ds_id_500 = make_dataid( + name="foo", + calibration="radiance", + stripe="a", + view="nadir", + resolution=500, + ) + + filename_info = { + "mission_id": "S3A", + "dataset_name": "foo", + "start_time": 0, + "end_time": 0, + "stripe": "a", + "view": "n", + "baseline": 4, + } + + test = NCSLSTRGeo("somedir/geometry_an.nc", filename_info, "c") + test.get_dataset(ds_id, dict(filename_info, **{"file_key": "latitude_{stripe:1s}{view:1s}"})) + assert test.start_time == good_start + assert test.end_time == good_end + open_dataset_.assert_called() + open_dataset_.reset_mock() + + test = NCSLSTRFlag("somedir/S1_radiance_an.nc", filename_info, "c") + test.get_dataset(ds_id, dict(filename_info, **{"file_key": "flags_{stripe:1s}{view:1s}"}),) + assert test.view == "nadir" + assert test.stripe == "a" + assert test.start_time == good_start + assert test.end_time == good_end + open_dataset_.assert_called() + open_dataset_.reset_mock() + + test = NCSLSTRAngles("somedir/S1_radiance_an.nc", filename_info, "c") + + assert hasattr(test, "cartx") + assert hasattr(test, "carti") + assert hasattr(test, "carta") + + test.get_dataset(ds_id, dict(filename_info, **{"file_key": "geometry_t{view:1s}"})) + assert test.start_time == good_start + assert test.end_time == good_end + open_dataset_.assert_called() + open_dataset_.reset_mock() + + test.get_dataset(ds_id_500, dict(filename_info, **{"file_key": "geometry_t{view:1s}"})) + + +class TestSLSTRAngles(TestSLSTRL1B): + """Test the implementation of the angle reconstruction.""" + + @mock.patch("satpy.readers.core.slstr.os.path.exists") + @mock.patch("satpy.readers.core.slstr.xr.open_dataset") + def test_angles_loading(self, open_dataset_, exists_): + """Test ability to load interpolated SLSTR angle data.""" + open_dataset_.return_value = self.fake_dataset + exists_.side_effect = lambda path: "cartesian_" in path + + expected = np.array([ + [350.17659522, 353.31335956, 356.1441152, 358.55896366, + 0.4400064, 1.79903533, 3.18424353, 5.28496166, 8.80226547], + [350.02717525, 353.48225908, 356.37905054, 358.72059252, + 0.5, 1.81840943, 3.23819125, 5.44552178, 9.13958444], + [349.30986294, 354.38594648, 357.53203205, 359.49732861, + 1.00001215, 2.61842911, 4.45389453, 6.49348929, 8.72979111], + [349.68116993, 354.62824808, 357.55405421, 359.31582013, + 0.74004914, 2.46431559, 4.44154365, 6.45655991, 8.30055946], + ]) + + ds_id = make_dataid(name="solar_azimuth_angle", view="nadir") + filename_info = { + "mission_id": "S3A", + "dataset_name": "solar_azimuth_angle", + "start_time": 0, + "end_time": 0, + "stripe": "t", + "view": "n", + "baseline": 4, + } + + test = NCSLSTRAngles("somedir/geometry_tn.nc", filename_info, "c") + + assert hasattr(test, "cartx") + assert hasattr(test, "carti") + assert hasattr(test, "carta") + + data = test.get_dataset( + ds_id, + dict(filename_info, **{"file_key": "solar_azimuth_tn"}), + ) + + assert isinstance(data, xr.DataArray) + assert data.attrs["units"] == "degrees" + assert data.attrs["platform_name"] == "Sentinel-3A" + assert data.attrs["sensor"] == "slstr" + assert data.shape == expected.shape + np.testing.assert_allclose(data.values, expected) diff --git a/satpy/tests/reader_tests/test_slstr_l1b.py b/satpy/tests/reader_tests/test_slstr_l1b.py index a15cf999c8..ee43812c78 100644 --- a/satpy/tests/reader_tests/test_slstr_l1b.py +++ b/satpy/tests/reader_tests/test_slstr_l1b.py @@ -27,7 +27,7 @@ import xarray as xr from satpy.dataset.dataid import DataID, ModifierTuple, WavelengthRange -from satpy.readers.slstr_l1b import NCSLSTR1B, NCSLSTRAngles, NCSLSTRFlag, NCSLSTRGeo +from satpy.readers.slstr_l1b import NCSLSTR1B local_id_keys_config = {"name": { "required": True, @@ -71,7 +71,8 @@ class TestSLSTRL1B(unittest.TestCase): """Common setup for SLSTR_L1B tests.""" @mock.patch("satpy.readers.slstr_l1b.xr") - def setUp(self, xr_): + @mock.patch("satpy.readers.core.slstr.xr") + def setUp(self, xr_, xr2_): """Create a fake dataset using the given radiance data.""" self.base_data = np.array(([1., 2., 3., 4., 5., 6., 7., 8., 9.], [7., 8., 9., 10., 11., 12., 13., 14., 15.], @@ -119,7 +120,7 @@ def setUp(self, xr_): x_in = xr.DataArray(self.ix_data,dims=("columns", "rows")) y_in = xr.DataArray(self.iy_data,dims=("columns", "rows")) - saa = xr.DataArray(self.ang_data,dims=("columns_t", "rows_t")) + xr.DataArray(self.ang_data,dims=("columns_t", "rows_t")) x_tx = xr.DataArray(self.tx_data,dims=("columns_t", "rows_t")) y_tx = xr.DataArray(self.ty_data,dims=("columns_t", "rows_t")) @@ -129,17 +130,13 @@ def setUp(self, xr_): "S9_BT_ao": self.rad, "foo_radiance_an": self.rad, "S5_solar_irradiances": self.rad, - "geometry_tn": self.rad, - "latitude_an": self.rad, "x_in": x_in, "y_in": y_in, "x_an": x_in, "y_an": y_in, - "flags_an": self.rad, "detector_an": det, "x_tx": x_tx, "y_tx": y_tx, - "solar_azimuth_tn": saa, }, attrs={ "start_time": self.start_time, @@ -178,8 +175,6 @@ def test_instantiate(self, bvs_, xr_): ds_id = make_dataid(name="foo", calibration="radiance", stripe="a", view="nadir") - ds_id_500 = make_dataid(name="foo", calibration="radiance", - stripe="a", view="nadir", resolution=500) filename_info = {"mission_id": "S3A", "dataset_name": "foo", "start_time": 0, "end_time": 0, "stripe": "a", "view": "n", "baseline":4} @@ -205,41 +200,13 @@ def test_instantiate(self, bvs_, xr_): xr_.open_dataset.assert_called() xr_.open_dataset.reset_mock() - filename_info = {"mission_id": "S3A", "dataset_name": "foo", - "start_time": 0, "end_time": 0, - "stripe": "a", "view": "n", "baseline": 4} - test = NCSLSTRGeo("somedir/geometry_an.nc", filename_info, "c") - test.get_dataset(ds_id, dict(filename_info, **{"file_key": "latitude_{stripe:1s}{view:1s}"})) - assert test.start_time == good_start - assert test.end_time == good_end - xr_.open_dataset.assert_called() - xr_.open_dataset.reset_mock() - - test = NCSLSTRFlag("somedir/S1_radiance_an.nc", filename_info, "c") - test.get_dataset(ds_id, dict(filename_info, **{"file_key": "flags_{stripe:1s}{view:1s}"})) - assert test.view == "nadir" - assert test.stripe == "a" - assert test.start_time == good_start - assert test.end_time == good_end - xr_.open_dataset.assert_called() - xr_.open_dataset.reset_mock() - - test = NCSLSTRAngles("somedir/S1_radiance_an.nc", filename_info, "c") - test.get_dataset(ds_id, dict(filename_info, **{"file_key": "geometry_t{view:1s}"})) - assert test.start_time == good_start - assert test.end_time == good_end - xr_.open_dataset.assert_called() - xr_.open_dataset.reset_mock() - test.get_dataset(ds_id_500, dict(filename_info, **{"file_key": "geometry_t{view:1s}"})) - - class TestSLSTRCalibration(TestSLSTRL1B): """Test the implementation of the calibration factors.""" @mock.patch("satpy.readers.slstr_l1b.xr") def test_radiance_calibration(self, xr_): """Test radiance calibration steps.""" - from satpy.readers.slstr_l1b import CHANCALIB_FACTORS + from satpy.readers.core.slstr import CHANCALIB_FACTORS xr_.open_dataset.return_value = self.fake_dataset ds_id = make_dataid(name="foo", calibration="radiance", @@ -315,32 +282,3 @@ def test_cal_rad(self): out_rad = NCSLSTR1B._cal_rad(rad, didx, solflux) np.testing.assert_allclose(out_rad, good_rad) - - -class TestSLSTRAngles(TestSLSTRL1B): - """Test the implementation of the angle reconstruction.""" - - @mock.patch("satpy.readers.slstr_l1b.xr.open_dataset") - def test_radiance_calibration(self, xr_): - """Test radiance calibration steps.""" - xr_.return_value = self.fake_dataset - - res = np.array([[350.17659522, 353.31335956, 356.1441152, 358.55896366, - 0.4400064, 1.79903533, 3.18424353, 5.28496166, 8.80226547], - [350.02717525, 353.48225908, 356.37905054, 358.72059252, - 0.5, 1.81840943, 3.23819125, 5.44552178, 9.13958444], - [349.30986294, 354.38594648, 357.53203205, 359.49732861, - 1.00001215, 2.61842911, 4.45389453, 6.49348929, 8.72979111], - [349.68116993, 354.62824808, 357.55405421, 359.31582013, - 0.74004914, 2.46431559, 4.44154365, 6.45655991, 8.30055946]]) - - ds_id = make_dataid(name="solar_azimuth_angle", view="nadir") - filename_info = {"mission_id": "S3A", "dataset_name": "solar_azimuth_angle", - "start_time": 0, "end_time": 0, - "stripe": "t", "view": "n", "baseline": 4} - - test = NCSLSTRAngles("somedir/geometry_tn.nc", filename_info, "c") - data = test.get_dataset(ds_id, dict(filename_info, **{"file_key": "solar_azimuth_tn"})) - assert data.attrs["units"] == "degrees" - assert data.shape == res.shape - np.testing.assert_allclose(data.values, res)