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 VIIRS L2 Reader #2740

Merged
merged 24 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,4 @@ The following people have made contributions to this project:
- [Xin Zhang (zxdawn)](/~https://github.com/zxdawn)
- [Yufei Zhu (yufeizhu600)](/~https://github.com/yufeizhu600)
- [Youva Aoun (YouvaEUMex)](/~https://github.com/YouvaEUMex)
- [Will Sharpe (wjsharpe)](/~https://github.com/wjsharpe)
126 changes: 126 additions & 0 deletions satpy/etc/readers/viirs_l2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
reader:
name: viirs_l2
short_name: VIIRS L2
long_name: SNPP VIIRS Level 2 data in netCDF4 format
description: Generic NASA VIIRS L2 Reader
status: Alpha
supports_fsspec: false
reader: !!python/name:satpy.readers.yaml_reader.GEOFlippableFileYAMLReader
sensors: [viirs]

file_types:
cldprop_l2_viirs:
file_reader: !!python/name:satpy.readers.viirs_l2.VIIRSL2FileHandler
file_patterns:
- 'CLDPROP_L2_VIIRS_{spacecraft_name:s}.A{start_time:%Y%j.%H%M}.{collection:03d}.{production_time:%Y%j%H%M%S}.nc'
cldmsk_l2_viirs:
file_reader: !!python/name:satpy.readers.viirs_l2.VIIRSL2FileHandler
file_patterns:
- 'CLDMSK_L2_VIIRS_{spacecraft_name:s}.A{start_time:%Y%j.%H%M}.{collection:03d}.{production_time:%Y%j%H%M%S}.nc'
aerdb_l2_viirs:
file_reader: !!python/name:satpy.readers.viirs_l2.VIIRSL2FileHandler
file_patterns:
- 'AERDB_L2_VIIRS_{spacecraft_name:s}.A{start_time:%Y%j.%H%M}.{collection:03d}.{production_time:%Y%j%H%M%S}.nc'
- 'AERDB_L2_VIIRS_{spacecraft_name:s}.A{start_time:%Y%j.%H%M}.{collection:03d}.nrt.nc'
cldir_l2_viirs:
file_reader: !!python/name:satpy.readers.viirs_l2.VIIRSL2FileHandler
file_patterns:
- 'CLDIR_L2_VIIRS_{spacecraft_name:s}.A{start_time:%Y%j.%H%M}.{collection:03d}.{production_time:%Y%j%H%M%S}.hdf'
aerdt_l2_viirs:
file_reader: !!python/name:satpy.readers.viirs_l2.VIIRSL2FileHandler
file_patterns:
- 'AERDT_L2_VIIRS_{spacecraft_name:s}.A{start_time:%Y%j.%H%M}.{collection:03d}.{production_time:%Y%j%H%M%S}.nc'
fsnrad_l2_viirs:
file_reader: !!python/name:satpy.readers.viirs_l2.VIIRSL2FileHandler
file_patterns:
- 'FSNRAD_L2_VIIRS_CRIS_{spacecraft_name:s}.A{start_time:%Y%j.%H%M}.{collection:03d}.{production_time:%Y%j%H%M%S}.nc'
- 'FSNRAD_L2_VIIRS_CRIS_SS_{spacecraft_name:s}.A{start_time:%Y%j.%H%M}.{collection:03d}.{production_time:%Y%j%H%M%S}.nc'

datasets:
cld_lon:
name: cld_lon
resolution: 1000
file_type: [cldmsk_l2_viirs, cldprop_l2_viirs]
file_key: geolocation_data/longitude
units: degrees
standard_name: longitude
cld_lat:
name: cld_lat
resolution: 1000
file_type: [cldmsk_l2_viirs, cldprop_l2_viirs]
file_key: geolocation_data/latitude
units: degrees
standard_name: latitude
aerdb_lon:
name: aerdb_lon
resolution: 1000
file_type: [aerdb_l2_viirs]
file_key: Longitude
units: degrees
standard_name: longitude
aerdb_lat:
name: aerdb_lat
resolution: 1000
file_type: [aerdb_l2_viirs]
file_key: Latitude
units: degrees
standard_name: latitude
aerdt_lon:
name: aerdt_lon
resolution: 1000
file_type: [aerdt_l2_viirs]
file_key: longitude
units: degrees
standard_name: longitude
aerdt_lat:
name: aerdt_lat
resolution: 1000
file_type: [aerdt_l2_viirs]
file_key: latitude
units: degrees
standard_name: latitude

##################################
# Datasets in file cldmsk_l2_viirs
##################################
Clear_Sky_Confidence:
name: Clear_Sky_Confidence
long_name: VIIRS Clear Sky Confidence
units: "1"
coordinates: [cld_lon, cld_lat]
file_key: geophysical_data/Clear_Sky_Confidence
file_type: cldmsk_l2_viirs
standard_name: clear_sky_confidence

###################################
# Datasets in file cldprop_l2_viirs
###################################
Cloud_Top_Height:
name: Cloud_Top_Height
long_name: Cloud Top Height from NOAA CLAVR-x AWG algorithm
units: m
coordinates: [cld_lon,cld_lat]
file_key: geophysical_data/Cloud_Top_Height
file_type: cldprop_l2_viirs
standard_name: cloud_top_height

##########################################
# Datasets in files aerdb_l2_viirs
##########################################
Angstrom_Exponent_Land_Ocean_Best_Estimate:
name: Angstrom_Exponent_Land_Ocean_Best_Estimate
long_name: Deep Blue/SOAR Angstrom exponent over land and ocean
units: "1"
coordinates: [aerdb_lon,aerdb_lat]
file_key: Angstrom_Exponent_Land_Ocean_Best_Estimate
file_type: [aerdb_l2_viirs]
standard_name: angstrom_exponent_land_ocean_best_estimate

Aerosol_Optical_Thickness_550_Land_Ocean:
name: Aerosol_Optical_Thickness_550_Land_Ocean_Best_Estimate
long_name: Deep Blue/SOAR aerosol optical thickness at 550 nm over land and ocean
units: "1"
coordinates: [aerdb_lon,aerdb_lat]
file_key: Aerosol_Optical_Thickness_550_Land_Ocean_Best_Estimate
file_type: [aerdb_l2_viirs]
standard_name: aerosol_optical_thickness_550_land_ocean
176 changes: 176 additions & 0 deletions satpy/readers/viirs_l2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
"""Interface to VIIRS L2 format.

This reader implements the support of L2 files generated using the VIIRS instrument on SNPP and NOAA satellite files.
The intent of this reader is to be able to reproduce images from L2 layers in NASA worldview with identical colormaps.

Currently a subset of four of these layers are supported
1. Deep Blue Aerosol Angstrom Exponent (Land and Ocean)
2. Clear Sky Confidence
3. Cloud Top Height
4. Deep Blue Aerosol Optical Thickness (Land and Ocean)
"""
import logging
from datetime import datetime

import numpy as np

from satpy.readers.netcdf_utils import NetCDF4FileHandler

LOG = logging.getLogger(__name__)


class VIIRSL2FileHandler(NetCDF4FileHandler):
"""NetCDF File Handler for VIIRS L2 Products."""
def _parse_datetime(self, datestr):
"""Parse datetime."""
return datetime.strptime(datestr, "%Y-%m-%dT%H:%M:%S.000Z")

@property
def start_time(self):
"""Get start time."""
return self._parse_datetime(self["/attr/time_coverage_start"])

@property
def end_time(self):
"""Get end time."""
return self._parse_datetime(self["/attr/time_coverage_end"])

@property
def start_orbit_number(self):
"""Get start orbit number."""
try:
return int(self["/attr/orbit_number"])
except KeyError:
return int(self["/attr/OrbitNumber"])

Check warning on line 44 in satpy/readers/viirs_l2.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/viirs_l2.py#L43-L44

Added lines #L43 - L44 were not covered by tests

@property
def end_orbit_number(self):
"""Get end orbit number."""
try:
return int(self["/attr/orbit_number"])
except KeyError:
return int(self["/attr/OrbitNumber"])

Check warning on line 52 in satpy/readers/viirs_l2.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/viirs_l2.py#L51-L52

Added lines #L51 - L52 were not covered by tests

@property
def platform_name(self):
"""Get platform name."""
try:
res = self.get("/attr/platform", self.filename_info["platform_shortname"])
except KeyError:
res = "Unknown"

return {
"JPSS-1": "NOAA-20",
"NP": "Suomi-NPP",
"J1": "NOAA-20",
"J2": "NOAA-21",
"JPSS-2": "NOAA-21",
}.get(res, res)

@property
def sensor_name(self):
"""Get sensor name."""
return self["/attr/instrument"].lower()

def _get_dataset_file_units(self, ds_info, var_path):
file_units = ds_info.get("units")
if file_units is None:
file_units = self.get(var_path + "/attr/units")

Check warning on line 78 in satpy/readers/viirs_l2.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/viirs_l2.py#L78

Added line #L78 was not covered by tests
if file_units == "none" or file_units == "None":
file_units = "1"

Check warning on line 80 in satpy/readers/viirs_l2.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/viirs_l2.py#L80

Added line #L80 was not covered by tests
return file_units
wjsharpe marked this conversation as resolved.
Show resolved Hide resolved

def _get_dataset_valid_range(self, ds_info, var_path):
valid_min = self.get(var_path + "/attr/valid_min")
valid_max = self.get(var_path + "/attr/valid_max")
if not valid_min and not valid_max:
valid_range = self.get(var_path + "/attr/valid_range")
if valid_range is not None:
valid_min = valid_range[0]
valid_max = valid_range[1]

Check warning on line 90 in satpy/readers/viirs_l2.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/viirs_l2.py#L89-L90

Added lines #L89 - L90 were not covered by tests
scale_factor = self.get(var_path + "/attr/scale_factor")
scale_offset = self.get(var_path + "/attr/add_offset")
return valid_min, valid_max, scale_factor, scale_offset

def get_metadata(self, dataset_id, ds_info):
"""Get metadata."""
var_path = ds_info.get("file_key", ds_info["name"])
file_units = self._get_dataset_file_units(ds_info, var_path)

# Get extra metadata
i = getattr(self[var_path], "attrs", {})
i.update(ds_info)
i.update(dataset_id.to_dict())
i.update(
{
"file_units": file_units,
"platform_name": self.platform_name,
"sensor": self.sensor_name,
"start_orbit": self.start_orbit_number,
"end_orbit": self.end_orbit_number,
}
)
i.update(dataset_id.to_dict())
return i

def adjust_scaling_factors(self, factors, file_units, output_units):
"""Adjust scaling factors."""
if factors is None or factors[0] is None:
factors = [1, 0]
if file_units == output_units:
LOG.debug("File units and output units are the same (%s)", file_units)
return factors
factors = np.array(factors)
return factors

Check warning on line 124 in satpy/readers/viirs_l2.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/viirs_l2.py#L123-L124

Added lines #L123 - L124 were not covered by tests

def available_datasets(self, configured_datasets=None):
"""Generate dataset info and their availablity.

See
:meth:`satpy.readers.file_handlers.BaseFileHandler.available_datasets`
for details.

"""
for is_avail, ds_info in configured_datasets or []:
if is_avail is not None:
yield is_avail, ds_info
continue

Check warning on line 137 in satpy/readers/viirs_l2.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/viirs_l2.py#L136-L137

Added lines #L136 - L137 were not covered by tests
ft_matches = self.file_type_matches(ds_info["file_type"])
if ft_matches is None:
yield None, ds_info
continue
var_path = ds_info.get("file_key", ds_info["name"])
yield var_path in self, ds_info

def get_dataset(self, ds_id, ds_info):
"""Get DataArray for specified dataset."""
var_path = ds_info.get("file_key", ds_info["name"])
metadata = self.get_metadata(ds_id, ds_info)
(
valid_min,
valid_max,
scale_factor,
scale_offset,
) = self._get_dataset_valid_range(ds_info, var_path)
data = self[var_path]
# For aerdb Longitude and Latitude datasets have coordinates
# This check is needed to work with yaml_reader
if "long_name" in metadata and metadata["long_name"] == "Longitude":
data.coords["Latitude"].attrs["standard_name"] = "latitude"

Check warning on line 159 in satpy/readers/viirs_l2.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/viirs_l2.py#L159

Added line #L159 was not covered by tests
elif "long_name" in metadata and metadata["long_name"] == "Latitude":
data.coords["Longitude"].attrs["standard_name"] = "longitude"

Check warning on line 161 in satpy/readers/viirs_l2.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/viirs_l2.py#L161

Added line #L161 was not covered by tests

data.attrs.update(metadata)
if valid_min is not None and valid_max is not None:
data = data.where((data >= valid_min) & (data <= valid_max))

Check warning on line 165 in satpy/readers/viirs_l2.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/viirs_l2.py#L165

Added line #L165 was not covered by tests
factors = (scale_factor, scale_offset)
factors = self.adjust_scaling_factors(
factors, metadata["file_units"], ds_info.get("units")
)
if factors[0] != 1 or factors[1] != 0:
data *= factors[0]
data += factors[1]

Check warning on line 172 in satpy/readers/viirs_l2.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/viirs_l2.py#L171-L172

Added lines #L171 - L172 were not covered by tests
# rename dimensions to correspond to satpy's 'y' and 'x' standard
if "number_of_lines" in data.dims:
data = data.rename({"number_of_lines": "y", "number_of_pixels": "x"})

Check warning on line 175 in satpy/readers/viirs_l2.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/viirs_l2.py#L175

Added line #L175 was not covered by tests
return data

Check warning on line 176 in satpy/readers/viirs_l2.py

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

❌ New issue: Complex Method

VIIRSL2FileHandler.get_dataset has a cyclomatic complexity of 10, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
Loading
Loading