diff --git a/satpy/etc/readers/scatsat1_l2b.yaml b/satpy/etc/readers/scatsat1_l2b.yaml index 71ef94ee3d..461f70adbc 100644 --- a/satpy/etc/readers/scatsat1_l2b.yaml +++ b/satpy/etc/readers/scatsat1_l2b.yaml @@ -4,7 +4,7 @@ reader: long_name: Scatsat-1 Level 2b Wind field data in HDF5 format description: Generic Eumetsat Scatsat-1 L2B Wind field Reader reader: !!python/name:satpy.readers.core.yaml_reader.FileYAMLReader - status: defunct + status: beta supports_fsspec: false sensors: [scatterometer] default_datasets: @@ -12,35 +12,64 @@ reader: datasets: longitude: name: longitude - resolution: 25000 file_type: scatsat + file_key: "science_data/Longitude" + resolution: + 12500: {file_type: scatsat_12km} + 25000: {file_type: scatsat_25km} + 50000: {file_type: scatsat_50km} standard_name: longitude - units: degree + units: degrees_east + coordinates: [longitude, latitude] + fill_value: 65535 latitude: name: latitude - resolution: 25000 file_type: scatsat + file_key: "science_data/Latitude" + resolution: + 12500: {file_type: scatsat_12km} + 25000: {file_type: scatsat_25km} + 50000: {file_type: scatsat_50km} standard_name: latitude - units: degree + units: degrees_north + coordinates: [longitude, latitude] + fill_value: 32767 wind_speed: name: wind_speed sensor: Scatterometer - resolution: 25000 coordinates: [longitude, latitude] + resolution: + 12500: {file_type: scatsat_12km} + 25000: {file_type: scatsat_25km} + 50000: {file_type: scatsat_50km} file_type: scatsat + file_key: "science_data/Wind_speed_selection" standard_name: wind_speed + fill_value: 32767 wind_direction: name: wind_direction - resolution: 25000 + sensor: Scatterometer coordinates: [longitude, latitude] + resolution: + 12500: {file_type: scatsat_12km} + 25000: {file_type: scatsat_25km} + 50000: {file_type: scatsat_50km} file_type: scatsat + file_key: "science_data/Wind_direction_selection" standard_name: wind_direction - + fill_value: 65535 file_types: - scatsat: + scatsat_12km: + file_reader: !!python/name:satpy.readers.scatsat1_l2b.SCATSAT1L2BFileHandler + file_patterns: ['E06SCTL2B{start_date:%Y%j}_{start_orbit}_{end_orbit}_{direction}_12km_{prod_date}T{prod_time}_{version}.h5'] + scatsat_25km: + file_reader: !!python/name:satpy.readers.scatsat1_l2b.SCATSAT1L2BFileHandler + file_patterns: ['S1L2B{start_date:%Y%j}_{start_orbit}_{end_orbit}_{direction}_25km_{prod_date}T{prod_time}_{version}.h5', + 'E06SCTL2B{start_date:%Y%j}_{start_orbit}_{end_orbit}_{direction}_25km_{prod_date}T{prod_time}_{version}.h5'] + scatsat_50km: file_reader: !!python/name:satpy.readers.scatsat1_l2b.SCATSAT1L2BFileHandler - file_patterns: ['S1L2B{start_date:%Y%j}_{start_orbit}_{end_orbit}_{direction}_{cell_spacing}_{prod_date}T{prod_time}_{version}.h5'] + file_patterns: ['S1L2B{start_date:%Y%j}_{start_orbit}_{end_orbit}_{direction}_50km_{prod_date}T{prod_time}_{version}.h5'] diff --git a/satpy/readers/scatsat1_l2b.py b/satpy/readers/scatsat1_l2b.py index 39cc0fbb04..f4167fcedb 100644 --- a/satpy/readers/scatsat1_l2b.py +++ b/satpy/readers/scatsat1_l2b.py @@ -19,54 +19,47 @@ import datetime as dt -import h5py +import xarray as xr -from satpy.dataset import Dataset -from satpy.readers.core.file_handlers import BaseFileHandler +from satpy.readers.core.hdf5 import HDF5FileHandler -class SCATSAT1L2BFileHandler(BaseFileHandler): +class SCATSAT1L2BFileHandler(HDF5FileHandler): """File handler for ScatSat level 2 files, as distributed by Eumetsat in HDF5 format.""" - def __init__(self, filename, filename_info, filetype_info): - """Initialize the file handler.""" - super(SCATSAT1L2BFileHandler, self).__init__(filename, filename_info, filetype_info) - self.h5f = h5py.File(self.filename, "r") - h5data = self.h5f["science_data"] + @property + def start_time(self): + """Time for first observation.""" + return dt.datetime.strptime(self["science_data/attr/Range Beginning Date"], + "%Y-%jT%H:%M:%S.%f") - self.filename_info["start_time"] = dt.datetime.strptime( - h5data.attrs["Range Beginning Date"], "%Y-%jT%H:%M:%S.%f") - self.filename_info["end_time"] = dt.datetime.strptime( - h5data.attrs["Range Ending Date"], "%Y-%jT%H:%M:%S.%f") + @property + def end_time(self): + """Time for final observation.""" + return dt.datetime.strptime(self["science_data/attr/Range Ending Date"], + "%Y-%jT%H:%M:%S.%f") - self.lons = None - self.lats = None + @property + def platform_name(self): + """Get the Platform ShortName.""" + return self["science_data/attr/Satellite Name"] - self.wind_speed_scale = float(h5data.attrs["Wind Speed Selection Scale"]) - self.wind_direction_scale = float(h5data.attrs["Wind Direction Selection Scale"]) - self.latitude_scale = float(h5data.attrs["Latitude Scale"]) - self.longitude_scale = float(h5data.attrs["Longitude Scale"]) + def get_dataset(self, ds_id, ds_info): + """Get output data and metadata of specified dataset.""" + var_path = ds_info["file_key"] + data = self[var_path] - def get_dataset(self, key, info): - """Get the dataset.""" - h5data = self.h5f["science_data"] - stdname = info.get("standard_name") + data = data.where(data != ds_info.get("fill_value", 65535)) + if "Longitude" in var_path: + data = data * float(self["science_data/attr/Longitude Scale"]) + data = xr.where(data > 180, data - 360., data) + elif "Latitude" in var_path: + data = data * float(self["science_data/attr/Latitude Scale"]) + elif "Wind_speed_selection" in var_path: + data = data * float(self["science_data/attr/Wind Speed Selection Scale"]) + elif "Wind_direction_selection" in var_path: + data = data * float(self["science_data/attr/Wind Direction Selection Scale"]) - if stdname in ["latitude", "longitude"]: - - if self.lons is None or self.lats is None: - self.lons = h5data["Longitude"][:]*self.longitude_scale - self.lats = h5data["Latitude"][:]*self.latitude_scale - - if info["standard_name"] == "longitude": - return Dataset(self.lons, id=key, **info) - else: - return Dataset(self.lats, id=key, **info) - - if stdname in ["wind_speed"]: - windspeed = h5data["Wind_speed_selection"][:, :] * self.wind_speed_scale - return Dataset(windspeed, id=key, **info) - - if stdname in ["wind_direction"]: - wind_direction = h5data["Wind_direction_selection"][:, :] * self.wind_direction_scale - return Dataset(wind_direction, id=key, **info) + data.attrs.update({"platform_name": self.platform_name}) + data.attrs.update(ds_info) + return data diff --git a/satpy/tests/reader_tests/test_scatsat_l1b.py b/satpy/tests/reader_tests/test_scatsat_l1b.py new file mode 100644 index 0000000000..4e6e61f7bc --- /dev/null +++ b/satpy/tests/reader_tests/test_scatsat_l1b.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2025 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.scatsat1_l2b module.""" + +import os +import unittest +from unittest import mock + +import numpy as np + +from satpy.tests.reader_tests.test_hdf5_utils import FakeHDF5FileHandler +from satpy.tests.utils import convert_file_content_to_data_array + +DEFAULT_FILE_DTYPE = np.uint16 +DEFAULT_FILE_SHAPE = (12, 105) +DEFAULT_FILE_DATA = np.arange(DEFAULT_FILE_SHAPE[0] * DEFAULT_FILE_SHAPE[1], + dtype=DEFAULT_FILE_DTYPE).reshape(DEFAULT_FILE_SHAPE) +DEFAULT_FILE_FACTORS = np.array([2.0, 1.0], dtype=np.float32) +DEFAULT_LAT_DATA = np.linspace(45, 65, DEFAULT_FILE_SHAPE[1]).astype(DEFAULT_FILE_DTYPE) +DEFAULT_LAT_DATA = np.repeat([DEFAULT_LAT_DATA], DEFAULT_FILE_SHAPE[0], axis=0) +DEFAULT_LON_DATA = np.linspace(5, 45, DEFAULT_FILE_SHAPE[1]).astype(DEFAULT_FILE_DTYPE) +DEFAULT_LON_DATA = np.repeat([DEFAULT_LON_DATA], DEFAULT_FILE_SHAPE[0], axis=0) + + +class FakeHDF5FileHandler2(FakeHDF5FileHandler): + """Swap-in HDF5 File Handler.""" + + def get_test_content(self, filename, filename_info, filetype_info): + """Mimic reader input file content.""" + file_content = { + "science_data/attr/Range Beginning Date": "2025-139T04:52:21.469", + "science_data/attr/Range Ending Date": "2025-139T05:42:01.023", + "science_data/attr/Satellite Name": "EOS-06 ", + "science_data/attr/Longitude Scale": "0.01", + "science_data/attr/Latitude Scale": "0.01", + "science_data/attr/Wind Speed Selection Scale": "0.1", + "science_data/attr/Wind Direction Selection Scale": "1.0", + } + for wd in ["Wind_speed_selection", "Wind_direction_selection"]: + k = f"science_data/{wd}" + file_content[k] = DEFAULT_FILE_DATA[:, :] + file_content[k + "/shape"] = (DEFAULT_FILE_SHAPE[0], DEFAULT_FILE_SHAPE[1]) + + lon_k = "science_data/Longitude" + lat_k = "science_data/Latitude" + file_content[lon_k] = DEFAULT_LON_DATA + file_content[lon_k + "/shape"] = DEFAULT_FILE_SHAPE + file_content[lat_k] = DEFAULT_LAT_DATA + file_content[lat_k + "/shape"] = DEFAULT_FILE_SHAPE + + convert_file_content_to_data_array(file_content) + return file_content + + +class TestSCATSAT1L2BReader(unittest.TestCase): + """Test SCATSAT L2B Reader.""" + + yaml_file = "scatsat1_l2b.yaml" + + def setUp(self): + """Wrap HDF5 file handler with our own fake handler.""" + from satpy._config import config_search_paths + from satpy.readers.scatsat1_l2b import SCATSAT1L2BFileHandler + self.reader_configs = config_search_paths(os.path.join("readers", self.yaml_file)) + # http://stackoverflow.com/questions/12219967/how-to-mock-a-base-class-with-python-mock-library + self.p = mock.patch.object(SCATSAT1L2BFileHandler, "__bases__", (FakeHDF5FileHandler2,)) + self.fake_handler = self.p.start() + self.p.is_local = True + + def tearDown(self): + """Stop wrapping the HDF5 file handler.""" + self.p.stop() + + def test_init(self): + """Test basic init with no extra parameters.""" + from satpy.readers.core.loading import load_reader + r = load_reader(self.reader_configs) + loadables = r.select_files_from_pathnames([ + "E06SCTL2B2025139_13087_13088_SN_12km_2025-139T07-55-10_v1.0.4.h5", + ]) + assert len(loadables) == 1 + r.create_filehandlers(loadables) + # make sure we have some files + assert r.file_handlers + + def test_load_basic(self): + """Test loading of basic channels.""" + from satpy.readers.core.loading import load_reader + r = load_reader(self.reader_configs) + loadables = r.select_files_from_pathnames([ + "E06SCTL2B2025139_13087_13088_SN_12km_2025-139T07-55-10_v1.0.4.h5", + ]) + assert len(loadables) == 1 + r.create_filehandlers(loadables) + ds = r.load(["wind_speed", "wind_direction"]) + assert len(ds) == 2 + for d in ds.values(): + assert d.shape == (DEFAULT_FILE_SHAPE[0], int(DEFAULT_FILE_SHAPE[1])) + assert "area" in d.attrs + assert d.attrs["area"] is not None + assert d.attrs["area"].lons.shape == (DEFAULT_FILE_SHAPE[0], DEFAULT_FILE_SHAPE[1]) + assert d.attrs["area"].lats.shape == (DEFAULT_FILE_SHAPE[0], DEFAULT_FILE_SHAPE[1]) + assert d.attrs["sensor"] == "Scatterometer" + assert d.attrs["platform_name"] == "EOS-06 " + assert d.attrs["resolution"] == 12500 + + def test_load_basic_25km_resolution(self): + """Test loading of basic channels from 25km resolution data.""" + from satpy.readers.core.loading import load_reader + r = load_reader(self.reader_configs) + loadables = r.select_files_from_pathnames([ + "E06SCTL2B2025139_13087_13088_SN_25km_2025-139T07-55-10_v1.0.4.h5", + ]) + assert len(loadables) == 1 + r.create_filehandlers(loadables) + ds = r.load(["wind_speed", "wind_direction"]) + assert len(ds) == 2 + for d in ds.values(): + assert d.attrs["resolution"] == 25000 + + def test_load_wind_speed(self): + """Test loading of wind_speed.""" + from satpy.readers.core.loading import load_reader + r = load_reader(self.reader_configs) + loadables = r.select_files_from_pathnames([ + "E06SCTL2B2025139_13087_13088_SN_12km_2025-139T07-55-10_v1.0.4.h5", + ]) + assert len(loadables) == 1 + r.create_filehandlers(loadables) + ds = r.load([ + "wind_speed", + ]) + assert len(ds) == 1 + for d in ds.values(): + assert d.shape == DEFAULT_FILE_SHAPE + assert "area" in d.attrs + assert d.attrs["area"] is not None + assert d.attrs["area"].lons.shape == DEFAULT_FILE_SHAPE + assert d.attrs["area"].lats.shape == DEFAULT_FILE_SHAPE + + def test_properties(self): + """Test platform_name.""" + import datetime as dt + + from satpy.readers.core.loading import load_reader + filenames = [ + "E06SCTL2B2025139_13087_13088_SN_12km_2025-139T07-55-10_v1.0.4.h5", ] + + reader = load_reader(self.reader_configs) + files = reader.select_files_from_pathnames(filenames) + reader.create_filehandlers(files) + # Make sure we have some files + res = reader.load(["wind_speed"]) + assert res["wind_speed"].platform_name == "EOS-06 " + assert res["wind_speed"].start_time == dt.datetime(2025, 5, 19, 4, 52, 21, 469000) + assert res["wind_speed"].end_time == dt.datetime(2025, 5, 19, 5, 42, 1, 23000) + + def test_available_dataset_ids(self): + """Test available_dataset_ids method.""" + from satpy.dataset.dataid import DataID, default_id_keys_config + from satpy.readers.core.loading import load_reader + r = load_reader(self.reader_configs) + loadables = r.select_files_from_pathnames([ + "E06SCTL2B2025139_13087_13088_SN_12km_2025-139T07-55-10_v1.0.4.h5", + ]) + assert len(loadables) == 1 + r.create_filehandlers(loadables) + available_ids = r.available_dataset_ids + expected_keys = { + DataID(default_id_keys_config, name="longitude", resolution=12500, modifiers=()), + DataID(default_id_keys_config, name="latitude", resolution=12500, modifiers=()), + DataID(default_id_keys_config, name="wind_speed", resolution=12500, modifiers=()), + DataID(default_id_keys_config, name="wind_direction", resolution=12500, modifiers=()) + } + assert set(available_ids) == expected_keys