From fcbae549dd85353d282004f956860978fa8c7d08 Mon Sep 17 00:00:00 2001 From: Simon Proud Date: Thu, 24 Aug 2023 21:30:56 +0100 Subject: [PATCH 01/15] Initial commit for Himawari L2 NOAA enterprise cloud products. --- satpy/etc/readers/ahi_l2_nc.yaml | 21 ++++++ satpy/readers/ahi_l2_nc.py | 112 +++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 satpy/etc/readers/ahi_l2_nc.yaml create mode 100644 satpy/readers/ahi_l2_nc.py diff --git a/satpy/etc/readers/ahi_l2_nc.yaml b/satpy/etc/readers/ahi_l2_nc.yaml new file mode 100644 index 0000000000..b1f0461838 --- /dev/null +++ b/satpy/etc/readers/ahi_l2_nc.yaml @@ -0,0 +1,21 @@ +reader: + name: ahi_l2_nc + short_name: AHI L2 NetCDF4 + long_name: Himawari-8/9 AHI Level 2 products in netCDF4 format from NOAA enterprise + status: Beta + supports_fsspec: true + sensors: ['ahi'] + reader: !!python/name:satpy.readers.yaml_reader.FileYAMLReader + + +file_types: + ahi_l2_cloudmask: + file_reader: !!python/name:satpy.readers.ahi_l2_nc.HIML2NCFileHandler + file_patterns: + - '{sensor:3s}-{product:_4s}_{version:4s}_{platform:3s}_s{start_time:%Y%m%d%H%M%S%f}_e{end_time:%Y%m%d%H%M%S%f}_c{creation_time:%Y%m%d%H%M%S%f}.nc' + +datasets: + cloudmask: + name: cloudmask + file_key: CloudMask + file_type: [ ahi_l2_cloudmask ] \ No newline at end of file diff --git a/satpy/readers/ahi_l2_nc.py b/satpy/readers/ahi_l2_nc.py new file mode 100644 index 0000000000..68c4f55a38 --- /dev/null +++ b/satpy/readers/ahi_l2_nc.py @@ -0,0 +1,112 @@ +#!/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 . +"""Reader for Himawari L2 cloud products from NOAA's big data programme.""" + +import logging +from datetime import datetime + +import xarray as xr + +from satpy._compat import cached_property +from satpy.readers._geos_area import get_area_definition, get_area_extent +from satpy.readers.file_handlers import BaseFileHandler +from satpy.utils import get_legacy_chunk_size + +logger = logging.getLogger(__name__) + +CHUNK_SIZE = get_legacy_chunk_size() + +EXPECTED_DATA_AREA = 'Full Disk' + + +class HIML2NCFileHandler(BaseFileHandler): + """File handler for Himawari L2 NOAA enterprise data in netCDF format.""" + + def __init__(self, filename, filename_info, filetype_info, geo_data=None): + """Initialize the reader.""" + super(HIML2NCFileHandler, self).__init__(filename, filename_info, + filetype_info) + self.nc = xr.open_dataset(self.filename, + decode_cf=True, + mask_and_scale=False, + chunks={'xc': CHUNK_SIZE, 'yc': CHUNK_SIZE}) + + # Check that file is a full disk scene, we don't know the area for anything else + if self.nc.attrs['cdm_data_type'] != EXPECTED_DATA_AREA: + raise ValueError('File is not a full disk scene') + + self.sensor = self.nc.attrs['instrument_name'].lower() + self.nlines = self.nc.dims['Columns'] + self.ncols = self.nc.dims['Rows'] + self.platform_name = self.nc.attrs['satellite_name'] + self.platform_shortname = filename_info['platform'] + self._meta = None + + @property + def start_time(self): + """Start timestamp of the dataset.""" + dt = self.nc.attrs['time_coverage_start'] + return datetime.strptime(dt, '%Y-%m-%dT%H:%M:%SZ') + + @property + def end_time(self): + """End timestamp of the dataset.""" + dt = self.nc.attrs['time_coverage_end'] + return datetime.strptime(dt, '%Y-%m-%dT%H:%M:%SZ') + + def get_dataset(self, key, info): + """Load a dataset.""" + var = info['file_key'] + logger.debug('Reading in get_dataset %s.', var) + variable = self.nc[var] + variable.attrs.update(key.to_dict()) + return variable + + @cached_property + def area(self): + """Get AreaDefinition representing this file's data.""" + return self._get_area_def() + + def get_area_def(self, dsid): + """Get the area definition.""" + del dsid + return self.area + + def _get_area_def(self): + logger.warning('This product misses metadata required to produce an appropriate area definition.' + 'Assuming standard Himawari-8/9 full disk projection.') + pdict = {} + pdict['cfac'] = 20466275 + pdict['lfac'] = 20466275 + pdict['coff'] = 2750.5 + pdict['loff'] = 2750.5 + pdict['a'] = 6378137.0 + pdict['h'] = 35785863.0 + pdict['b'] = 6356752.3 + pdict['ssp_lon'] = 140.7 + pdict['nlines'] = self.nlines + pdict['ncols'] = self.ncols + pdict['scandir'] = 'N2S' + + aex = get_area_extent(pdict) + + pdict['a_name'] = 'Himawari_Area' + pdict['a_desc'] = "AHI Full Disk area" + pdict['p_id'] = f'geos{self.platform_shortname}' + + return get_area_definition(pdict, aex) \ No newline at end of file From fa491a6816cf218f0e65a7cb1dd3c1e47d46f6fa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 24 Aug 2023 21:01:44 +0000 Subject: [PATCH 02/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- satpy/etc/readers/ahi_l2_nc.yaml | 2 +- satpy/readers/ahi_l2_nc.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/satpy/etc/readers/ahi_l2_nc.yaml b/satpy/etc/readers/ahi_l2_nc.yaml index b1f0461838..04d0571f79 100644 --- a/satpy/etc/readers/ahi_l2_nc.yaml +++ b/satpy/etc/readers/ahi_l2_nc.yaml @@ -18,4 +18,4 @@ datasets: cloudmask: name: cloudmask file_key: CloudMask - file_type: [ ahi_l2_cloudmask ] \ No newline at end of file + file_type: [ ahi_l2_cloudmask ] diff --git a/satpy/readers/ahi_l2_nc.py b/satpy/readers/ahi_l2_nc.py index 68c4f55a38..76823017d3 100644 --- a/satpy/readers/ahi_l2_nc.py +++ b/satpy/readers/ahi_l2_nc.py @@ -109,4 +109,4 @@ def _get_area_def(self): pdict['a_desc'] = "AHI Full Disk area" pdict['p_id'] = f'geos{self.platform_shortname}' - return get_area_definition(pdict, aex) \ No newline at end of file + return get_area_definition(pdict, aex) From 865c7f634c05e1aa2dd3147da0228b766cc655b4 Mon Sep 17 00:00:00 2001 From: Simon Proud Date: Fri, 25 Aug 2023 08:49:28 +0100 Subject: [PATCH 03/15] Correct typo in AHI L2 code. --- satpy/readers/ahi_l2_nc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/satpy/readers/ahi_l2_nc.py b/satpy/readers/ahi_l2_nc.py index 68c4f55a38..f795eec748 100644 --- a/satpy/readers/ahi_l2_nc.py +++ b/satpy/readers/ahi_l2_nc.py @@ -106,7 +106,7 @@ def _get_area_def(self): aex = get_area_extent(pdict) pdict['a_name'] = 'Himawari_Area' - pdict['a_desc'] = "AHI Full Disk area" + pdict['a_desc'] = "AHI Full Disk area" pdict['p_id'] = f'geos{self.platform_shortname}' return get_area_definition(pdict, aex) \ No newline at end of file From ad89b9a4ad9a514ab8f2cb2ad593a0c0858eac59 Mon Sep 17 00:00:00 2001 From: Simon Proud Date: Fri, 25 Aug 2023 08:54:46 +0100 Subject: [PATCH 04/15] Add basic check to ensure we're working on a full disk scene. --- satpy/readers/ahi_l2_nc.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/satpy/readers/ahi_l2_nc.py b/satpy/readers/ahi_l2_nc.py index 3167896e66..3db9d1528c 100644 --- a/satpy/readers/ahi_l2_nc.py +++ b/satpy/readers/ahi_l2_nc.py @@ -90,18 +90,13 @@ def get_area_def(self, dsid): def _get_area_def(self): logger.warning('This product misses metadata required to produce an appropriate area definition.' 'Assuming standard Himawari-8/9 full disk projection.') - pdict = {} - pdict['cfac'] = 20466275 - pdict['lfac'] = 20466275 - pdict['coff'] = 2750.5 - pdict['loff'] = 2750.5 - pdict['a'] = 6378137.0 - pdict['h'] = 35785863.0 - pdict['b'] = 6356752.3 - pdict['ssp_lon'] = 140.7 - pdict['nlines'] = self.nlines - pdict['ncols'] = self.ncols - pdict['scandir'] = 'N2S' + + # Basic check to ensure we're processing a full disk (2km) scene. + if self.nlines != 5500 or self.ncols != 5500: + raise ValueError("Input L2 file is not a full disk Himawari scene. Only full disk data is supported.") + + pdict = {'cfac': 20466275, 'lfac': 20466275, 'coff': 2750.5, 'loff': 2750.5, 'a': 6378137.0, 'h': 35785863.0, + 'b': 6356752.3, 'ssp_lon': 140.7, 'nlines': self.nlines, 'ncols': self.ncols, 'scandir': 'N2S'} aex = get_area_extent(pdict) From 81bb4c8b2b45c36cc6b372504c6205754fb4bdce Mon Sep 17 00:00:00 2001 From: Simon Proud Date: Fri, 25 Aug 2023 11:47:52 +0100 Subject: [PATCH 05/15] Add tests for AHI L2 reader. --- satpy/tests/reader_tests/test_ahi_l2_nc.py | 110 +++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 satpy/tests/reader_tests/test_ahi_l2_nc.py diff --git a/satpy/tests/reader_tests/test_ahi_l2_nc.py b/satpy/tests/reader_tests/test_ahi_l2_nc.py new file mode 100644 index 0000000000..68f8c3a420 --- /dev/null +++ b/satpy/tests/reader_tests/test_ahi_l2_nc.py @@ -0,0 +1,110 @@ +"""Tests for the Himawari L2 netCDF reader.""" + +from datetime import datetime + +import h5netcdf +import numpy as np +import pytest + +from satpy.readers.ahi_l2_nc import HIML2NCFileHandler +from satpy.tests.utils import make_dataid + +rng = np.random.default_rng() +clmk_data = rng.integers(0, 3, (5500, 5500), dtype=np.uint16) +cprob_data = rng.uniform(0, 1, (5500, 5500)) + +start_time = datetime(2023, 8, 24, 5, 40, 21) +end_time = datetime(2023, 8, 24, 5, 49, 40) + +dimensions = {'Columns': 5500, 'Rows': 5500} + +exp_ext = (-5499999.9012, -5499999.9012, 5499999.9012, 5499999.9012) + +global_attrs = {"time_coverage_start": start_time.strftime("%Y-%m-%dT%H:%M:%SZ"), + "time_coverage_end": end_time.strftime("%Y-%m-%dT%H:%M:%SZ"), + "instrument_name": "AHI", + "satellite_name": "Himawari-9", + "cdm_data_type": "Full Disk", + } + +badarea_attrs = global_attrs.copy() +badarea_attrs['cdm_data_type'] = 'bad_area' + + +def ahil2_filehandler(fname, platform='h09'): + """Instantiate a Filehandler.""" + fileinfo = {'platform': platform} + filetype = None + fh = HIML2NCFileHandler(fname, fileinfo, filetype) + return fh + + +@pytest.fixture(scope="session") +def himl2_filename(tmp_path_factory): + """Create a fake himawari l2 file.""" + fname = f'{tmp_path_factory.mktemp("data")}/AHI-CMSK_v1r1_h09_s202308240540213_e202308240549407_c202308240557548.nc' + with h5netcdf.File(fname, mode="w") as h5f: + h5f.dimensions = dimensions + h5f.attrs.update(global_attrs) + var = h5f.create_variable("CloudMask", ("Rows", "Columns"), np.uint16, chunks=(200, 200)) + var[:] = clmk_data + + return fname + + +@pytest.fixture(scope="session") +def himl2_filename_bad(tmp_path_factory): + """Create a fake himawari l2 file.""" + fname = f'{tmp_path_factory.mktemp("data")}/AHI-CMSK_v1r1_h09_s202308240540213_e202308240549407_c202308240557548.nc' + with h5netcdf.File(fname, mode="w") as h5f: + h5f.dimensions = dimensions + h5f.attrs.update(badarea_attrs) + var = h5f.create_variable("CloudMask", ("Rows", "Columns"), np.uint16, chunks=(200, 200)) + var[:] = clmk_data + + return fname + + +def test_startend(himl2_filename): + """Test start and end times are set correctly.""" + fh = ahil2_filehandler(himl2_filename) + assert fh.start_time == start_time + assert fh.end_time == end_time + + +def test_ahi_l2_area_def(himl2_filename, caplog): + """Test reader handles area definition correctly.""" + warntxt = "This product misses metadata" + ps = '+proj=geos +lon_0=140.7 +h=35785863 +x_0=0 +y_0=0 +a=6378137 +rf=298.257024882273 +units=m +no_defs +type=crs' + + # Check case where input data is correct size. + fh = ahil2_filehandler(himl2_filename) + clmk_id = make_dataid(name="cloudmask") + area_def = fh.get_area_def(clmk_id) + assert area_def.width == dimensions['Columns'] + assert area_def.height == dimensions['Rows'] + assert np.allclose(area_def.area_extent, exp_ext) + assert area_def.proj4_string == ps + assert warntxt in caplog.text + + # Check case where input data is incorrect size. + with pytest.raises(ValueError): + fh = ahil2_filehandler(himl2_filename) + fh.nlines = 3000 + fh.get_area_def(clmk_id) + + +def test_bad_area_name(himl2_filename_bad): + """Check case where area name is not correct.""" + global_attrs['cdm_data_type'] = 'bad_area' + with pytest.raises(ValueError): + ahil2_filehandler(himl2_filename_bad) + global_attrs['cdm_data_type'] = 'Full Disk' + + +def test_load_data(himl2_filename): + """Test that data is loaded successfully.""" + fh = ahil2_filehandler(himl2_filename) + clmk_id = make_dataid(name="cloudmask") + clmk = fh.get_dataset(clmk_id, {'file_key': 'CloudMask'}) + assert np.allclose(clmk.data, clmk_data) From c7ce9735a6d48806f1d3de1cd19ce117776dea0d Mon Sep 17 00:00:00 2001 From: Simon Proud Date: Fri, 25 Aug 2023 11:48:40 +0100 Subject: [PATCH 06/15] Remove unneeded variable from AHI L2 NC reader. --- satpy/readers/ahi_l2_nc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/satpy/readers/ahi_l2_nc.py b/satpy/readers/ahi_l2_nc.py index 3db9d1528c..3675f4f419 100644 --- a/satpy/readers/ahi_l2_nc.py +++ b/satpy/readers/ahi_l2_nc.py @@ -37,7 +37,7 @@ class HIML2NCFileHandler(BaseFileHandler): """File handler for Himawari L2 NOAA enterprise data in netCDF format.""" - def __init__(self, filename, filename_info, filetype_info, geo_data=None): + def __init__(self, filename, filename_info, filetype_info): """Initialize the reader.""" super(HIML2NCFileHandler, self).__init__(filename, filename_info, filetype_info) @@ -91,7 +91,7 @@ def _get_area_def(self): logger.warning('This product misses metadata required to produce an appropriate area definition.' 'Assuming standard Himawari-8/9 full disk projection.') - # Basic check to ensure we're processing a full disk (2km) scene. + # Basic check to ensure we're processing a full disk (2km) scene.n if self.nlines != 5500 or self.ncols != 5500: raise ValueError("Input L2 file is not a full disk Himawari scene. Only full disk data is supported.") From 71476306cda0cd82345752ea4355c64445fd2bca Mon Sep 17 00:00:00 2001 From: Simon Proud Date: Fri, 25 Aug 2023 12:30:35 +0100 Subject: [PATCH 07/15] Add additional AHI L2 netcdf datasets to the YAML file. --- satpy/etc/readers/ahi_l2_nc.yaml | 164 ++++++++++++++++++++++++++++++- 1 file changed, 159 insertions(+), 5 deletions(-) diff --git a/satpy/etc/readers/ahi_l2_nc.yaml b/satpy/etc/readers/ahi_l2_nc.yaml index 04d0571f79..955d41bbcd 100644 --- a/satpy/etc/readers/ahi_l2_nc.yaml +++ b/satpy/etc/readers/ahi_l2_nc.yaml @@ -9,13 +9,167 @@ reader: file_types: - ahi_l2_cloudmask: + ahi_l2_mask: file_reader: !!python/name:satpy.readers.ahi_l2_nc.HIML2NCFileHandler file_patterns: - - '{sensor:3s}-{product:_4s}_{version:4s}_{platform:3s}_s{start_time:%Y%m%d%H%M%S%f}_e{end_time:%Y%m%d%H%M%S%f}_c{creation_time:%Y%m%d%H%M%S%f}.nc' + - '{sensor:3s}-CMSK_{version:4s}_{platform:3s}_s{start_time:%Y%m%d%H%M%S%f}_e{end_time:%Y%m%d%H%M%S%f}_c{creation_time:%Y%m%d%H%M%S%f}.nc' + + ahi_l2_type: + file_reader: !!python/name:satpy.readers.ahi_l2_nc.HIML2NCFileHandler + file_patterns: + - '{sensor:3s}-CPHS_{version:4s}_{platform:3s}_s{start_time:%Y%m%d%H%M%S%f}_e{end_time:%Y%m%d%H%M%S%f}_c{creation_time:%Y%m%d%H%M%S%f}.nc' + + ahi_l2_height: + file_reader: !!python/name:satpy.readers.ahi_l2_nc.HIML2NCFileHandler + file_patterns: + - '{sensor:3s}-CHGT_{version:4s}_{platform:3s}_s{start_time:%Y%m%d%H%M%S%f}_e{end_time:%Y%m%d%H%M%S%f}_c{creation_time:%Y%m%d%H%M%S%f}.nc' datasets: - cloudmask: - name: cloudmask + # Products from the cloud mask files + cloud_mask: + name: cloud_mask file_key: CloudMask - file_type: [ ahi_l2_cloudmask ] + file_type: [ ahi_l2_mask ] + + cloud_mask_binary: + name: cloud_mask_binary + file_key: CloudMaskBinary + file_type: [ ahi_l2_mask ] + + cloud_probability: + name: cloud_probability + file_key: CloudProbability + file_type: [ ahi_l2_mask ] + + ice_cloud_probability: + name: ice_cloud_probability + file_key: IceCloudProbability + file_type: [ ahi_l2_mask ] + + phase_uncertainty: + name: phase_uncertainty + file_key: PhaseUncertainty + file_type: [ ahi_l2_mask ] + + dust_mask: + name: dust_mask + file_key: Dust_Mask + file_type: [ ahi_l2_mask ] + + fire_mask: + name: fire_mask + file_key: Fire_Mask + file_type: [ ahi_l2_mask ] + + smoke_mask: + name: smoke_mask + file_key: Smoke_Mask + file_type: [ ahi_l2_mask ] + + # Products from the cloud phase / type files + cloud_phase: + name: cloud_phase + file_key: CloudPhase + file_type: [ ahi_l2_type ] + + cloud_phase_flag: + name: cloud_phase_flag + file_key: CloudPhaseFlag + file_type: [ ahi_l2_type ] + + cloud_type: + name: cloud_type + file_key: CloudType + file_type: [ ahi_l2_type ] + + # Products from the cloud height files + cloud_optical_depth: + name: cloud_optical_depth + file_key: CldOptDpth + file_type: [ ahi_l2_height ] + + cloud_top_emissivity: + name: cloud_top_emissivity + file_key: CldTopEmss + file_type: [ ahi_l2_height ] + + cloud_top_pressure: + name: cloud_top_pressure + file_key: CldTopPres + file_type: [ ahi_l2_height ] + + cloud_top_pressure_low: + name: cloud_top_pressure_low + file_key: CldTopPresLow + file_type: [ ahi_l2_height ] + + cloud_top_temperature: + name: cloud_top_temperature + file_key: CldTopTemp + file_type: [ ahi_l2_height ] + + cloud_top_temperature_low: + name: cloud_top_temperature_low + file_key: CldTopTempLow + file_type: [ ahi_l2_height ] + + cloud_height_quality: + name: cloud_height_quality + file_key: CloudHgtQF + file_type: [ ahi_l2_height ] + + retrieval_cost: + name: retrieval_cost + file_key: Cost + file_type: [ ahi_l2_height ] + + inversion_flag: + name: inversion_flag + file_key: InverFlag + file_type: [ ahi_l2_height ] + + latitude_parallax_corrected: + name: latitude_parallax_corrected + file_key: Latitude_Pc + file_type: [ ahi_l2_height ] + + longitude_parallax_corrected: + name: longitude_parallax_corrected + file_key: Longitude_Pc + file_type: [ ahi_l2_height ] + + cloud_top_pressure_error: + name: cloud_top_pressure_error + file_key: PcError + file_type: [ ahi_l2_height ] + + processing_order: + name: processing_order + file_key: ProcOrder + file_type: [ ahi_l2_height ] + + shadow_mask: + name: shadow_mask + file_key: Shadow_Mask + file_type: [ ahi_l2_height ] + + cloud_top_temperature_error: + name: cloud_top_temperature_error + file_key: TcError + file_type: [ ahi_l2_height ] + + cloud_top_height_error: + name: cloud_top_height_error + file_key: ZcError + file_type: [ ahi_l2_height ] + + # Datasets in all three file types + latitude: + name: latitude + file_key: Latitude + file_type: [ ahi_l2_height, ahi_l2_type, ahi_l2_mask ] + + longitude: + name: longitude + file_key: Longitude + file_type: [ ahi_l2_height, ahi_l2_type, ahi_l2_mask ] From 46a2f2919901a8432e60e8d914fcfcd2b07b3e7c Mon Sep 17 00:00:00 2001 From: Simon Proud Date: Fri, 25 Aug 2023 12:35:57 +0100 Subject: [PATCH 08/15] Add some extra documentation to the AHI L2 nc reader. --- satpy/readers/ahi_l2_nc.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/satpy/readers/ahi_l2_nc.py b/satpy/readers/ahi_l2_nc.py index 3675f4f419..51d73af11f 100644 --- a/satpy/readers/ahi_l2_nc.py +++ b/satpy/readers/ahi_l2_nc.py @@ -15,7 +15,28 @@ # # You should have received a copy of the GNU General Public License along with # satpy. If not, see . -"""Reader for Himawari L2 cloud products from NOAA's big data programme.""" +"""Reader for Himawari L2 cloud products from NOAA's big data programme. + +These products are generated by the NOAA enterprise cloud suite and have filenames like: +AHI-CMSK_v1r1_h09_s202308240540213_e202308240549407_c202308240557548.nc + +The second letter grouping (CMSK above) indicates the product type: + CMSK - Cloud mask + CHGT - Cloud height + CPHS - Cloud type and phase +These products are generated from the AHI sensor on Himawari-8 and Himawari-9, and are +produced at the native instrument resolution for the IR channels (2km at nadir). + +NOTE: This reader is currently only compatible with full disk scenes. Unlike level 1 himawari +data, the netCDF files do not contain the required metadata to produce an appropriate area +definition for the data contents, and hence the area definition is hardcoded into the reader. + +A warning is displayed to the user highlighting this. The assumed area definition is a full +disk image at the nominal subsatellite longitude of 140.7 degrees East. + +All the simple data products are supported here, but multidimensional products are not yet +supported. These include the CldHgtFlag and the CloudMaskPacked variables. +""" import logging from datetime import datetime From 09051c8dca2344305e06290d576693c4d538a0b3 Mon Sep 17 00:00:00 2001 From: Simon Proud Date: Fri, 25 Aug 2023 12:46:49 +0100 Subject: [PATCH 09/15] Update area definition warning message for AHI L2 NC reader. --- satpy/readers/ahi_l2_nc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/satpy/readers/ahi_l2_nc.py b/satpy/readers/ahi_l2_nc.py index 51d73af11f..a238f1bb73 100644 --- a/satpy/readers/ahi_l2_nc.py +++ b/satpy/readers/ahi_l2_nc.py @@ -109,8 +109,8 @@ def get_area_def(self, dsid): return self.area def _get_area_def(self): - logger.warning('This product misses metadata required to produce an appropriate area definition.' - 'Assuming standard Himawari-8/9 full disk projection.') + logger.warning('The AHI L2 cloud products do not have the metadata required to produce an area definition.' + ' Assuming standard Himawari-8/9 full disk projection.') # Basic check to ensure we're processing a full disk (2km) scene.n if self.nlines != 5500 or self.ncols != 5500: From 4a99b36e85baf34fbb7daab8679bfce50a694ccb Mon Sep 17 00:00:00 2001 From: Simon Proud Date: Fri, 25 Aug 2023 13:20:06 +0100 Subject: [PATCH 10/15] Fir AHI L2 NC tests for warning message. --- satpy/tests/reader_tests/test_ahi_l2_nc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/satpy/tests/reader_tests/test_ahi_l2_nc.py b/satpy/tests/reader_tests/test_ahi_l2_nc.py index 68f8c3a420..84b3c667d7 100644 --- a/satpy/tests/reader_tests/test_ahi_l2_nc.py +++ b/satpy/tests/reader_tests/test_ahi_l2_nc.py @@ -74,7 +74,7 @@ def test_startend(himl2_filename): def test_ahi_l2_area_def(himl2_filename, caplog): """Test reader handles area definition correctly.""" - warntxt = "This product misses metadata" + warntxt = "The AHI L2 cloud products do not have the metadata" ps = '+proj=geos +lon_0=140.7 +h=35785863 +x_0=0 +y_0=0 +a=6378137 +rf=298.257024882273 +units=m +no_defs +type=crs' # Check case where input data is correct size. From c447480cb0444e506c219b3d654f83768cbcb0a0 Mon Sep 17 00:00:00 2001 From: Simon Proud Date: Fri, 25 Aug 2023 16:35:16 +0100 Subject: [PATCH 11/15] Fix AHI L2 NC documentation. --- satpy/readers/ahi_l2_nc.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/satpy/readers/ahi_l2_nc.py b/satpy/readers/ahi_l2_nc.py index a238f1bb73..e44c882898 100644 --- a/satpy/readers/ahi_l2_nc.py +++ b/satpy/readers/ahi_l2_nc.py @@ -22,8 +22,11 @@ The second letter grouping (CMSK above) indicates the product type: CMSK - Cloud mask + CHGT - Cloud height + CPHS - Cloud type and phase + These products are generated from the AHI sensor on Himawari-8 and Himawari-9, and are produced at the native instrument resolution for the IR channels (2km at nadir). From 344beac625d2a9f7a54c2dd797f8c8a164682f5b Mon Sep 17 00:00:00 2001 From: Simon Proud Date: Wed, 6 Sep 2023 17:07:10 +0100 Subject: [PATCH 12/15] Fix AHI L2 nc coordinate names --- satpy/readers/ahi_l2_nc.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/satpy/readers/ahi_l2_nc.py b/satpy/readers/ahi_l2_nc.py index e44c882898..ef3b7611aa 100644 --- a/satpy/readers/ahi_l2_nc.py +++ b/satpy/readers/ahi_l2_nc.py @@ -98,6 +98,13 @@ def get_dataset(self, key, info): var = info['file_key'] logger.debug('Reading in get_dataset %s.', var) variable = self.nc[var] + + # Data has 'Latitude' and 'Longitude' coords, these must be replaced. + variable = variable.rename({'Rows': 'y', 'Columns': 'x'}) + + variable = variable.drop('Latitude') + variable = variable.drop('Longitude') + variable.attrs.update(key.to_dict()) return variable From cce6349ae1bd0da06b8d58f8f0e6b496f7bcf503 Mon Sep 17 00:00:00 2001 From: Simon Proud Date: Wed, 6 Sep 2023 17:09:26 +0100 Subject: [PATCH 13/15] Update AHI tests for new coord names. --- satpy/tests/reader_tests/test_ahi_l2_nc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/satpy/tests/reader_tests/test_ahi_l2_nc.py b/satpy/tests/reader_tests/test_ahi_l2_nc.py index 84b3c667d7..d2e9c24489 100644 --- a/satpy/tests/reader_tests/test_ahi_l2_nc.py +++ b/satpy/tests/reader_tests/test_ahi_l2_nc.py @@ -16,7 +16,7 @@ start_time = datetime(2023, 8, 24, 5, 40, 21) end_time = datetime(2023, 8, 24, 5, 49, 40) -dimensions = {'Columns': 5500, 'Rows': 5500} +dimensions = {'X': 5500, 'Y': 5500} exp_ext = (-5499999.9012, -5499999.9012, 5499999.9012, 5499999.9012) @@ -46,7 +46,7 @@ def himl2_filename(tmp_path_factory): with h5netcdf.File(fname, mode="w") as h5f: h5f.dimensions = dimensions h5f.attrs.update(global_attrs) - var = h5f.create_variable("CloudMask", ("Rows", "Columns"), np.uint16, chunks=(200, 200)) + var = h5f.create_variable("CloudMask", ("Y", "X"), np.uint16, chunks=(200, 200)) var[:] = clmk_data return fname @@ -59,7 +59,7 @@ def himl2_filename_bad(tmp_path_factory): with h5netcdf.File(fname, mode="w") as h5f: h5f.dimensions = dimensions h5f.attrs.update(badarea_attrs) - var = h5f.create_variable("CloudMask", ("Rows", "Columns"), np.uint16, chunks=(200, 200)) + var = h5f.create_variable("CloudMask", ("Y", "X"), np.uint16, chunks=(200, 200)) var[:] = clmk_data return fname From 55cba4f24904bef98f56f21f17cf777568575be1 Mon Sep 17 00:00:00 2001 From: Simon Proud Date: Mon, 9 Oct 2023 20:56:50 +0100 Subject: [PATCH 14/15] Update AHI L2 NOAA tests. --- satpy/tests/reader_tests/test_ahi_l2_nc.py | 27 +++++++++++----------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/satpy/tests/reader_tests/test_ahi_l2_nc.py b/satpy/tests/reader_tests/test_ahi_l2_nc.py index d2e9c24489..ff2b5a3d53 100644 --- a/satpy/tests/reader_tests/test_ahi_l2_nc.py +++ b/satpy/tests/reader_tests/test_ahi_l2_nc.py @@ -2,9 +2,9 @@ from datetime import datetime -import h5netcdf import numpy as np import pytest +import xarray as xr from satpy.readers.ahi_l2_nc import HIML2NCFileHandler from satpy.tests.utils import make_dataid @@ -12,11 +12,13 @@ rng = np.random.default_rng() clmk_data = rng.integers(0, 3, (5500, 5500), dtype=np.uint16) cprob_data = rng.uniform(0, 1, (5500, 5500)) +lat_data = rng.uniform(-90, 90, (5500, 5500)) +lon_data = rng.uniform(-180, 180, (5500, 5500)) start_time = datetime(2023, 8, 24, 5, 40, 21) end_time = datetime(2023, 8, 24, 5, 49, 40) -dimensions = {'X': 5500, 'Y': 5500} +dimensions = {'Columns': 5500, 'Rows': 5500} exp_ext = (-5499999.9012, -5499999.9012, 5499999.9012, 5499999.9012) @@ -43,12 +45,11 @@ def ahil2_filehandler(fname, platform='h09'): def himl2_filename(tmp_path_factory): """Create a fake himawari l2 file.""" fname = f'{tmp_path_factory.mktemp("data")}/AHI-CMSK_v1r1_h09_s202308240540213_e202308240549407_c202308240557548.nc' - with h5netcdf.File(fname, mode="w") as h5f: - h5f.dimensions = dimensions - h5f.attrs.update(global_attrs) - var = h5f.create_variable("CloudMask", ("Y", "X"), np.uint16, chunks=(200, 200)) - var[:] = clmk_data - + ds = xr.Dataset({'CloudMask': (['Rows', 'Columns'], clmk_data)}, + coords={'Latitude': (['Rows', 'Columns'], lat_data), + 'Longitude': (['Rows', 'Columns'], lon_data)}, + attrs=global_attrs) + ds.to_netcdf(fname) return fname @@ -56,11 +57,11 @@ def himl2_filename(tmp_path_factory): def himl2_filename_bad(tmp_path_factory): """Create a fake himawari l2 file.""" fname = f'{tmp_path_factory.mktemp("data")}/AHI-CMSK_v1r1_h09_s202308240540213_e202308240549407_c202308240557548.nc' - with h5netcdf.File(fname, mode="w") as h5f: - h5f.dimensions = dimensions - h5f.attrs.update(badarea_attrs) - var = h5f.create_variable("CloudMask", ("Y", "X"), np.uint16, chunks=(200, 200)) - var[:] = clmk_data + ds = xr.Dataset({'CloudMask': (['Rows', 'Columns'], clmk_data)}, + coords={'Latitude': (['Rows', 'Columns'], lat_data), + 'Longitude': (['Rows', 'Columns'], lon_data)}, + attrs=badarea_attrs) + ds.to_netcdf(fname) return fname From 8cbe04a411ed3cffdf5aa5309623fefef486b860 Mon Sep 17 00:00:00 2001 From: Simon Proud Date: Tue, 10 Oct 2023 20:07:26 +0100 Subject: [PATCH 15/15] Minor updates to AHI L2 reader and the tests. --- satpy/readers/ahi_l2_nc.py | 16 +++++++--------- satpy/tests/reader_tests/test_ahi_l2_nc.py | 2 -- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/satpy/readers/ahi_l2_nc.py b/satpy/readers/ahi_l2_nc.py index ef3b7611aa..5159931819 100644 --- a/satpy/readers/ahi_l2_nc.py +++ b/satpy/readers/ahi_l2_nc.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright (c) 2018 Satpy developers +# Copyright (c) 2023 Satpy developers # # This file is part of satpy. # @@ -17,6 +17,8 @@ # satpy. If not, see . """Reader for Himawari L2 cloud products from NOAA's big data programme. +For more information about the data, see: . + These products are generated by the NOAA enterprise cloud suite and have filenames like: AHI-CMSK_v1r1_h09_s202308240540213_e202308240549407_c202308240557548.nc @@ -49,12 +51,9 @@ from satpy._compat import cached_property from satpy.readers._geos_area import get_area_definition, get_area_extent from satpy.readers.file_handlers import BaseFileHandler -from satpy.utils import get_legacy_chunk_size logger = logging.getLogger(__name__) -CHUNK_SIZE = get_legacy_chunk_size() - EXPECTED_DATA_AREA = 'Full Disk' @@ -63,12 +62,11 @@ class HIML2NCFileHandler(BaseFileHandler): def __init__(self, filename, filename_info, filetype_info): """Initialize the reader.""" - super(HIML2NCFileHandler, self).__init__(filename, filename_info, - filetype_info) + super().__init__(filename, filename_info, filetype_info) self.nc = xr.open_dataset(self.filename, decode_cf=True, mask_and_scale=False, - chunks={'xc': CHUNK_SIZE, 'yc': CHUNK_SIZE}) + chunks={"xc": "auto", "yc": "auto"}) # Check that file is a full disk scene, we don't know the area for anything else if self.nc.attrs['cdm_data_type'] != EXPECTED_DATA_AREA: @@ -119,8 +117,8 @@ def get_area_def(self, dsid): return self.area def _get_area_def(self): - logger.warning('The AHI L2 cloud products do not have the metadata required to produce an area definition.' - ' Assuming standard Himawari-8/9 full disk projection.') + logger.info('The AHI L2 cloud products do not have the metadata required to produce an area definition.' + ' Assuming standard Himawari-8/9 full disk projection.') # Basic check to ensure we're processing a full disk (2km) scene.n if self.nlines != 5500 or self.ncols != 5500: diff --git a/satpy/tests/reader_tests/test_ahi_l2_nc.py b/satpy/tests/reader_tests/test_ahi_l2_nc.py index ff2b5a3d53..39de4e1053 100644 --- a/satpy/tests/reader_tests/test_ahi_l2_nc.py +++ b/satpy/tests/reader_tests/test_ahi_l2_nc.py @@ -75,7 +75,6 @@ def test_startend(himl2_filename): def test_ahi_l2_area_def(himl2_filename, caplog): """Test reader handles area definition correctly.""" - warntxt = "The AHI L2 cloud products do not have the metadata" ps = '+proj=geos +lon_0=140.7 +h=35785863 +x_0=0 +y_0=0 +a=6378137 +rf=298.257024882273 +units=m +no_defs +type=crs' # Check case where input data is correct size. @@ -86,7 +85,6 @@ def test_ahi_l2_area_def(himl2_filename, caplog): assert area_def.height == dimensions['Rows'] assert np.allclose(area_def.area_extent, exp_ext) assert area_def.proj4_string == ps - assert warntxt in caplog.text # Check case where input data is incorrect size. with pytest.raises(ValueError):