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 EMIT emit_l1b reader #2592

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
154 changes: 154 additions & 0 deletions satpy/etc/readers/emit_l1b.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
reader:
name: emit_l1b
short_name: EMIT L1B NetCDF4
long_name: Earth Surface Mineral Dust Source Investigation (EMIT) Level 1B data in netCDF4 format
description: >
Reader for EMIT Level 1B files in NetCDF4 format.
status: Beta
supports_fsspec: false
sensors: [emit]
reader: !!python/name:satpy.readers.yaml_reader.FileYAMLReader

file_types:
emit_l1b_rad:
file_reader: !!python/name:satpy.readers.emit_l1b.EMITL1BFileHandler
file_patterns: ['{sensor:4s}_{level:3s}_RAD_{product_version:s}_{start_time:%Y%m%dT%H%M%S}_{orbit:s}_{scene:s}.nc']
requires: [emit_l1b_obs]
emit_l1b_obs:
file_reader: !!python/name:satpy.readers.emit_l1b.EMITL1BFileHandler
file_patterns: ['{sensor:4s}_{level:3s}_OBS_{product_version:s}_{start_time:%Y%m%dT%H%M%S}_{orbit:s}_{scene:s}.nc']

datasets:
elev:
name: elev
file_type: [emit_l1b_rad, emit_l1b_obs]
standard_name: elev
long_name: surface elevation
units: m
nc_group: location
_FillValue: -9999.0

glt_x:
name: glt_x
file_type: [emit_l1b_rad, emit_l1b_obs]
standard_name: glt_x
long_name: GLT sample Lookup
units: pixel location
nc_group: location
_FillValue: -9999

glt_y:
name: glt_y
file_type: [emit_l1b_rad, emit_l1b_obs]
standard_name: glt_y
long_name: GLT sample Lookup
units: pixel location
nc_group: location
_FillValue: -9999

radiance:
name: radiance
file_type: [emit_l1b_rad]
standard_name: radiance
units: uW cm-2 sr-1 nm-1
nc_group:
_FillValue: -9999.0

sza:
name: sza
file_type: [emit_l1b_obs]
standard_name: solar_zenith_angle
long_name: To-sun zenith (0 to 90 degrees from zenith)
units: degree
nc_group:
_FillValue: -9999.0

saa:
name: saa
file_type: [emit_l1b_obs]
standard_name: solar_azimuth_angle
long_name: To-sun azimuth (0 to 360 degrees CW from N)
units: degree
nc_group:
_FillValue: -9999.0

vza:
name: vza
file_type: [emit_l1b_obs]
standard_name: sensor_zenith_angle
long_name: To-sensor zenith (0 to 90 degrees from zenith)
units: degree
nc_group:
_FillValue: -9999.0

vaa:
name: vaa
file_type: [emit_l1b_obs]
standard_name: sensor_azimuth_angle
long_name: To-sensor azimuth (0 to 360 degrees CW from N)
units: degree
nc_group:
_FillValue: -9999.0

path_length:
name: path_length
file_type: [emit_l1b_obs]
standard_name: sensor_height
long_name: Path length (sensor-to-ground in meters)
units: m
nc_group:
_FillValue: -9999.0

solar_phase:
name: solar_phase
file_type: [emit_l1b_obs]
standard_name: solar_phase
long_name: Solar phase (degrees between to-sensor and to-sun vectors in principal plane)
units: degree
nc_group:
_FillValue: -9999.0

surface_slope:
name: surface_slope
file_type: [emit_l1b_obs]
standard_name: surface_slope
long_name: Slope (local surface slope as derived from DEM in degrees)
units: degree
nc_group:
_FillValue: -9999.0

surface_aspect:
name: surface_aspect
file_type: [emit_l1b_obs]
standard_name: surface_aspect
long_name: Aspect (local surface aspect 0 to 360 degrees clockwise from N)
units: degree
nc_group:
_FillValue: -9999.0

cosine_i:
name: cosine_i
file_type: [emit_l1b_obs]
standard_name: cosine_i
long_name: Cosine(i) (apparent local illumination factor based on DEM slope and aspect and to sun vector)
units: degree
nc_group:
_FillValue: -9999.0

time:
name: time
file_type: [emit_l1b_obs]
standard_name: time
long_name: UTC Time (decimal hours for mid-line pixels)
units:
nc_group:
_FillValue: -9999.0

earth_sun_distance:
name: earth_sun_distance
file_type: [emit_l1b_obs]
standard_name: earth_sun_distance
long_name: Earth-sun distance (AU)
units: AU
nc_group:
_FillValue: -9999.0
176 changes: 176 additions & 0 deletions satpy/readers/emit_l1b.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 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 <http://www.gnu.org/licenses/>.

"""Reader for the EMIT L1B NetCDF data."""

import logging
from datetime import datetime

import numpy as np
from pyresample.geometry import SwathDefinition
from satpy.readers.netcdf_utils import NetCDF4FileHandler
from satpy.utils import get_legacy_chunk_size

logger = logging.getLogger(__name__)
CHUNK_SIZE = get_legacy_chunk_size()

DATE_FMT = "%Y-%m-%dT%H:%M:%S%z"


class EMITL1BFileHandler(NetCDF4FileHandler):
"""File handler for EMIT L1B netCDF files."""

def __init__(self, filename, filename_info, filetype_info, *req_fhs):
"""Prepare the class for dataset reading."""
super().__init__(filename, filename_info, filetype_info)
self.area = None

Check warning on line 41 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L40-L41

Added lines #L40 - L41 were not covered by tests

self._load_bands()

Check warning on line 43 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L43

Added line #L43 was not covered by tests

def _load_bands(self):
"""Load bands data and attributes."""
if self.filetype_info["file_type"] == "emit_l1b_rad":
bands_name = "wavelengths"
elif self.filetype_info["file_type"] == "emit_l1b_obs":
bands_name = "observation_bands"

Check warning on line 50 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L47-L50

Added lines #L47 - L50 were not covered by tests

self.bands = self[f"sensor_band_parameters/{bands_name}"].rename("bands")

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

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L52

Added line #L52 was not covered by tests

# add other parameters as coords of bands
if self.filetype_info["file_type"] == "emit_l1b_rad":

Check warning on line 55 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L55

Added line #L55 was not covered by tests
# add wavelength
self.bands.coords["fwhm"] = self["sensor_band_parameters/fwhm"]

Check warning on line 57 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L57

Added line #L57 was not covered by tests

@property
def start_time(self):
"""Get observation start time."""
return datetime.strptime(self["/attr/time_coverage_start"], DATE_FMT).replace(tzinfo=None)

Check warning on line 62 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L62

Added line #L62 was not covered by tests

@property
def end_time(self):
"""Get observation end time."""
return datetime.strptime(self["/attr/time_coverage_end"], DATE_FMT).replace(tzinfo=None)

Check warning on line 67 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L67

Added line #L67 was not covered by tests

@property
def platform_name(self):
"""Get platform name."""
return self["/attr/platform"]

Check warning on line 72 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L72

Added line #L72 was not covered by tests

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

Check warning on line 77 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L77

Added line #L77 was not covered by tests

@property
def spatial_ref(self):
"""Get spatial_ref."""
return self["/attr/spatial_ref"]

Check warning on line 82 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L82

Added line #L82 was not covered by tests

@property
def geotransform(self):
"""Get geotransform."""
return self["/attr/geotransform"]

Check warning on line 87 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L87

Added line #L87 was not covered by tests

@property
def attrs(self):
"""Return attributes."""
return {

Check warning on line 92 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L92

Added line #L92 was not covered by tests
"filename": self.filename,
"start_time": self.start_time,
"end_time": self.end_time,
"platform_name": self.platform_name,
"sensor": self.sensor,
"geotransform": self.geotransform,
"spatial_ref": self.spatial_ref,
}

def _standardize_dims(self, dataset):
"""Standardize dims to y, x."""
if "downtrack" in dataset.dims:
dataset = dataset.rename({"downtrack": "y"})
if "crosstrack" in dataset.dims:
dataset = dataset.rename({"crosstrack": "x"})
if dataset.dims[0] == "x":
dataset = dataset.transpose("y", "x")
if "bands" in dataset.dims:
dataset.coords["bands"] = self.bands
dataset = dataset.transpose("bands", ...)

Check warning on line 112 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L104-L112

Added lines #L104 - L112 were not covered by tests

return dataset

Check warning on line 114 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L114

Added line #L114 was not covered by tests

def get_metadata(self, dataset, ds_info):
"""Get metadata."""
metadata = {}
metadata.update(dataset.attrs)
metadata.update(ds_info)
metadata.update(self.attrs)

Check warning on line 121 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L118-L121

Added lines #L118 - L121 were not covered by tests

return metadata

Check warning on line 123 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L123

Added line #L123 was not covered by tests

def get_dataset(self, dataset_id, ds_info):
"""Get dataset."""
name = dataset_id["name"]
file_type = self.filetype_info["file_type"]

Check warning on line 128 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L127-L128

Added lines #L127 - L128 were not covered by tests

if ds_info["nc_group"] is None:
var_path = name

Check warning on line 131 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L130-L131

Added lines #L130 - L131 were not covered by tests
else:
var_path = ds_info["nc_group"] + "/" + name

Check warning on line 133 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L133

Added line #L133 was not covered by tests

if file_type == "emit_l1b_obs":

Check warning on line 135 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L135

Added line #L135 was not covered by tests
# because the "obs" DataArray includes the varname as the "bands" dim
# we need to reset the var_path to "obs"
var_path = 'obs'

Check warning on line 138 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L138

Added line #L138 was not covered by tests

logger.debug(f"Reading in file to get dataset with path {var_path}.")
dataset = self[var_path]

Check warning on line 141 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L140-L141

Added lines #L140 - L141 were not covered by tests

dataset.attrs = self.get_metadata(dataset, ds_info)
fill_value = dataset.attrs.get('_FillValue', np.float32(np.nan))

Check warning on line 144 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L143-L144

Added lines #L143 - L144 were not covered by tests

# preserve integer data types if possible
if np.issubdtype(dataset.dtype, np.integer):
new_fill = fill_value

Check warning on line 148 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L147-L148

Added lines #L147 - L148 were not covered by tests
else:
new_fill = np.float32(np.nan)
dataset.attrs.pop('_FillValue', None)

Check warning on line 151 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L150-L151

Added lines #L150 - L151 were not covered by tests

# mask data by fill_value
good_mask = dataset != fill_value
dataset = dataset.where(good_mask, new_fill)

Check warning on line 155 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L154-L155

Added lines #L154 - L155 were not covered by tests

# standardize dims
dataset = self._standardize_dims(dataset)

Check warning on line 158 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L158

Added line #L158 was not covered by tests

# only load the variable by selecting bands dim
if file_type == "emit_l1b_obs":
dataset = dataset.sel(bands=ds_info['long_name']).rename(name)

Check warning on line 162 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L161-L162

Added lines #L161 - L162 were not covered by tests

# add area
self.get_lonlats()
dataset.attrs['area'] = self.area

Check warning on line 166 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L165-L166

Added lines #L165 - L166 were not covered by tests

return dataset

Check warning on line 168 in satpy/readers/emit_l1b.py

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L168

Added line #L168 was not covered by tests

def get_lonlats(self):
"""Get lonlats."""
if self.area is None:
lons = self['location/lon']
lats = self['location/lat']
self.area = SwathDefinition(lons, lats)
self.area.name = '_'.join([self.sensor, str(self.start_time)])

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

View check run for this annotation

Codecov / codecov/patch

satpy/readers/emit_l1b.py#L172-L176

Added lines #L172 - L176 were not covered by tests
Loading