Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to choose start time to MSI SAFE reader #2776

Merged
merged 18 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 18 additions & 18 deletions satpy/etc/readers/msi_safe.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
reader:
name: msi_safe
short_name: MSI SAFE
long_name: Sentinel-2 A and B MSI data in SAFE format
long_name: Sentinel-2 A and B MSI data in SAFE format, supporting L1C format only.
description: SAFE Reader for MSI data (Sentinel-2)
status: Nominal
supports_fsspec: false
Expand All @@ -10,16 +10,16 @@ reader:
reader: !!python/name:satpy.readers.yaml_reader.FileYAMLReader

file_types:
safe_granule:
safe_granule_l1c:
file_reader: !!python/name:satpy.readers.msi_safe.SAFEMSIL1C
file_patterns: ['{fmission_id:3s}_MSIL1C_{observation_time:%Y%m%dT%H%M%S}_N{fprocessing_baseline_number:4d}_R{relative_orbit_number:3d}_T{dtile_number:5s}_{dproduct_discriminator:%Y%m%dT%H%M%S}.SAFE/GRANULE/L1C_T{gtile_number:5s}_A{absolute_orbit_number:6d}_{gfile_discriminator:%Y%m%dT%H%M%S}/IMG_DATA/T{tile_number:5s}_{file_discriminator:%Y%m%dT%H%M%S}_{band_name:3s}.jp2']
file_patterns: ['{fmission_id:3s}_MSI{proclevel:3s}_{observation_time:%Y%m%dT%H%M%S}_N{fprocessing_baseline_number:4d}_R{relative_orbit_number:3d}_T{dtile_number:5s}_{dproduct_discriminator:%Y%m%dT%H%M%S}.SAFE/GRANULE/L1C_T{gtile_number:5s}_A{absolute_orbit_number:6d}_{gfile_discriminator:%Y%m%dT%H%M%S}/IMG_DATA/T{tile_number:5s}_{file_discriminator:%Y%m%dT%H%M%S}_{band_name:3s}.jp2']
requires: [safe_metadata, safe_tile_metadata]
safe_tile_metadata:
file_reader: !!python/name:satpy.readers.msi_safe.SAFEMSITileMDXML
file_patterns: ['{fmission_id:3s}_MSIL1C_{observation_time:%Y%m%dT%H%M%S}_N{fprocessing_baseline_number:4d}_R{relative_orbit_number:3d}_T{dtile_number:5s}_{dproduct_discriminator:%Y%m%dT%H%M%S}.SAFE/GRANULE/L1C_T{gtile_number:5s}_A{absolute_orbit_number:6d}_{gfile_discriminator:%Y%m%dT%H%M%S}/MTD_TL.xml']
file_patterns: ['{fmission_id:3s}_MSI{proclevel:3s}_{observation_time:%Y%m%dT%H%M%S}_N{fprocessing_baseline_number:4d}_R{relative_orbit_number:3d}_T{dtile_number:5s}_{dproduct_discriminator:%Y%m%dT%H%M%S}.SAFE/GRANULE/L1C_T{gtile_number:5s}_A{absolute_orbit_number:6d}_{gfile_discriminator:%Y%m%dT%H%M%S}/MTD_TL.xml']
safe_metadata:
file_reader: !!python/name:satpy.readers.msi_safe.SAFEMSIMDXML
file_patterns: ['{fmission_id:3s}_MSIL1C_{observation_time:%Y%m%dT%H%M%S}_N{fprocessing_baseline_number:4d}_R{relative_orbit_number:3d}_T{dtile_number:5s}_{dproduct_discriminator:%Y%m%dT%H%M%S}.SAFE/MTD_MSIL1C.xml']
file_patterns: ['{fmission_id:3s}_MSI{proclevel:3s}_{observation_time:%Y%m%dT%H%M%S}_N{fprocessing_baseline_number:4d}_R{relative_orbit_number:3d}_T{dtile_number:5s}_{dproduct_discriminator:%Y%m%dT%H%M%S}.SAFE/MTD_MSIL1C.xml']


datasets:
Expand All @@ -39,7 +39,7 @@ datasets:
counts:
standard_name: counts
units: "1"
file_type: safe_granule
file_type: safe_granule_l1c

B02:
name: B02
Expand All @@ -56,7 +56,7 @@ datasets:
counts:
standard_name: counts
units: "1"
file_type: safe_granule
file_type: safe_granule_l1c

B03:
name: B03
Expand All @@ -73,7 +73,7 @@ datasets:
counts:
standard_name: counts
units: "1"
file_type: safe_granule
file_type: safe_granule_l1c

B04:
name: B04
Expand All @@ -90,7 +90,7 @@ datasets:
counts:
standard_name: counts
units: "1"
file_type: safe_granule
file_type: safe_granule_l1c

B05:
name: B05
Expand All @@ -107,7 +107,7 @@ datasets:
counts:
standard_name: counts
units: "1"
file_type: safe_granule
file_type: safe_granule_l1c

B06:
name: B06
Expand All @@ -124,7 +124,7 @@ datasets:
counts:
standard_name: counts
units: "1"
file_type: safe_granule
file_type: safe_granule_l1c

B07:
name: B07
Expand All @@ -141,7 +141,7 @@ datasets:
counts:
standard_name: counts
units: "1"
file_type: safe_granule
file_type: safe_granule_l1c

B08:
name: B08
Expand All @@ -158,7 +158,7 @@ datasets:
counts:
standard_name: counts
units: "1"
file_type: safe_granule
file_type: safe_granule_l1c

B8A:
name: B8A
Expand All @@ -175,7 +175,7 @@ datasets:
counts:
standard_name: counts
units: "1"
file_type: safe_granule
file_type: safe_granule_l1c

B09:
name: B09
Expand All @@ -192,7 +192,7 @@ datasets:
counts:
standard_name: counts
units: "1"
file_type: safe_granule
file_type: safe_granule_l1c

B10:
name: B10
Expand All @@ -209,7 +209,7 @@ datasets:
counts:
standard_name: counts
units: "1"
file_type: safe_granule
file_type: safe_granule_l1c

B11:
name: B11
Expand All @@ -226,7 +226,7 @@ datasets:
counts:
standard_name: counts
units: "1"
file_type: safe_granule
file_type: safe_granule_l1c

B12:
name: B12
Expand All @@ -243,7 +243,7 @@ datasets:
counts:
standard_name: counts
units: "1"
file_type: safe_granule
file_type: safe_granule_l1c


solar_zenith_angle:
Expand Down
13 changes: 10 additions & 3 deletions satpy/readers/msi_safe.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@
reader_kwargs={'mask_saturated': False})
scene.load(['B01'])

L1B format description for the files read here:
L1C format description for the files read here:

https://sentinels.copernicus.eu/documents/247904/0/Sentinel-2-product-specifications-document-V14-9.pdf/

"""

import logging
from datetime import datetime

import dask.array as da
import defusedxml.ElementTree as ET
Expand Down Expand Up @@ -63,13 +64,14 @@ def __init__(self, filename, filename_info, filetype_info, mda, tile_mda, mask_s
super(SAFEMSIL1C, self).__init__(filename, filename_info,
filetype_info)
del mask_saturated
self._start_time = filename_info["observation_time"]
self._end_time = filename_info["observation_time"]
self._channel = filename_info["band_name"]
self._tile_mda = tile_mda
self._mda = mda
self.platform_name = PLATFORMS[filename_info["fmission_id"]]

self._start_time = self._tile_mda.start_time()
self._end_time = filename_info["observation_time"]

def get_dataset(self, key, info):
"""Load a dataset."""
if self._channel != key["name"]:
Expand Down Expand Up @@ -269,6 +271,11 @@ def _shape(self, resolution):
cols = int(self.geocoding.find('Size[@resolution="' + str(resolution) + '"]/NCOLS').text)
return cols, rows

def start_time(self):
"""Get the observation time from the tile metadata."""
timestr = self.root.find(".//SENSING_TIME").text
return datetime.strptime(timestr, "%Y-%m-%dT%H:%M:%S.%fZ")

@staticmethod
def _do_interp(minterp, xcoord, ycoord):
interp_points2 = np.vstack((ycoord.ravel(), xcoord.ravel()))
Expand Down
22 changes: 21 additions & 1 deletion satpy/tests/reader_tests/test_msi_safe.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# satpy. If not, see <http://www.gnu.org/licenses/>.
"""Module for testing the satpy.readers.msi_safe module."""
import unittest.mock as mock
from datetime import datetime
from io import BytesIO, StringIO

import numpy as np
Expand All @@ -25,6 +26,10 @@

from satpy.tests.utils import make_dataid

# Datetimes used for checking start time is correctly set.
fname_dt = datetime(2020, 10, 1, 18, 35, 41)
tilemd_dt = datetime(2020, 10, 1, 16, 34, 23, 153611)

mtd_tile_xml = b"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<n1:Level-1C_Tile_ID xmlns:n1="https://psd-14.sentinel2.eo.esa.int/PSD/S2_PDI_Level-1C_Tile_Metadata.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://psd-14.sentinel2.eo.esa.int/PSD/S2_PDI_Level-1C_Tile_Metadata.xsd /gpfs/dpc/app/s2ipf/FORMAT_METADATA_TILE_L1C/02.14.00/scripts/../../../schemas/02.17.00/PSD/S2_PDI_Level-1C_Tile_Metadata.xsd">

Expand Down Expand Up @@ -873,6 +878,10 @@ def setup_method(self):
self.old_xml_fh = SAFEMSIMDXML(StringIO(mtd_l1c_old_xml), filename_info, mock.MagicMock())
self.xml_fh = SAFEMSIMDXML(StringIO(mtd_l1c_xml), filename_info, mock.MagicMock(), mask_saturated=True)

def test_start_time(self):
"""Ensure start time is read correctly from XML."""
assert self.xml_tile_fh.start_time() == tilemd_dt

def test_satellite_zenith_array(self):
"""Test reading the satellite zenith array."""
info = dict(xml_tag="Viewing_Incidence_Angles_Grids", xml_item="Zenith")
Expand Down Expand Up @@ -980,10 +989,11 @@ class TestSAFEMSIL1C:
def setup_method(self):
"""Set up the test."""
from satpy.readers.msi_safe import SAFEMSITileMDXML
self.filename_info = dict(observation_time=None, fmission_id="S2A", band_name="B01", dtile_number=None)
self.filename_info = dict(observation_time=fname_dt, fmission_id="S2A", band_name="B01", dtile_number=None)
self.fake_data = xr.Dataset({"band_data": xr.DataArray([[[0, 1], [65534, 65535]]], dims=["band", "x", "y"])})
self.tile_mda = mock.create_autospec(SAFEMSITileMDXML)(BytesIO(mtd_tile_xml),
self.filename_info, mock.MagicMock())
self.tile_mda.start_time.return_value = tilemd_dt

@pytest.mark.parametrize(("mask_saturated", "calibration", "expected"),
[(True, "reflectance", [[np.nan, 0.01 - 10], [645.34, np.inf]]),
Expand All @@ -1001,3 +1011,13 @@ def test_calibration_and_masking(self, mask_saturated, calibration, expected):
with mock.patch("xarray.open_dataset", return_value=self.fake_data):
res = self.jp2_fh.get_dataset(make_dataid(name="B01", calibration=calibration), info=dict())
np.testing.assert_allclose(res, expected)


def test_start_time(self):
"""Test that the correct start time is returned."""
from satpy.readers.msi_safe import SAFEMSIL1C, SAFEMSIMDXML

mda = SAFEMSIMDXML(StringIO(mtd_l1c_xml), self.filename_info, mock.MagicMock())
self.jp2_fh = SAFEMSIL1C("somefile", self.filename_info, mock.MagicMock(),
mda, self.tile_mda)
assert tilemd_dt == self.jp2_fh.start_time