Skip to content

nesdis_eps_viirs

NESDIS EPS VIIRS Reader

NESDISEPSVIIRSReader

Bases: GriddedReader

Reader for NESDIS EPS VIIRS (Enterprise Processing System) AOT data. Available on NOAA STAR FTP.

Source code in monetio/readers/nesdis_eps_viirs.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@register_reader("nesdis_eps_viirs")
class NESDISEPSVIIRSReader(GriddedReader):
    """
    Reader for NESDIS EPS VIIRS (Enterprise Processing System) AOT data.
    Available on NOAA STAR FTP.
    """

    def open_dataset(
        self,
        files: str | list[str] = None,
        dates: pd.DatetimeIndex | list[datetime.datetime] | datetime.datetime | str = None,
        **kwargs,
    ) -> xr.Dataset:
        """
        Reads NESDIS EPS VIIRS data.

        Parameters
        ----------
        files : Union[str, List[str]], optional
            File path(s) or URL(s).
        dates : Union[pd.DatetimeIndex, List[datetime], datetime, str], optional
            Dates to retrieve. If files is None, this is used to build URLs.
        **kwargs : dict
            Additional arguments passed to XarrayDriver.open.

        Returns
        -------
        xr.Dataset
            The NESDIS EPS VIIRS dataset.
        """
        if files is None:
            if dates is None:
                raise ValueError("Either 'files' or 'dates' must be provided.")
            files = self.build_urls(dates)

        if "preprocess" not in kwargs:
            kwargs["preprocess"] = nesdis_eps_viirs_preprocess

        if "engine" not in kwargs:
            kwargs["engine"] = "h5netcdf"

        ds = super().open_dataset(files, **kwargs)

        # Update history
        ds = update_history(ds, "Read NESDIS EPS VIIRS data.")

        return ds

    def build_urls(
        self, dates: pd.DatetimeIndex | list[datetime.datetime] | datetime.datetime | str
    ) -> list[str]:
        """
        Build FTP URLs for NESDIS EPS VIIRS data based on dates.

        Parameters
        ----------
        dates : Union[pd.DatetimeIndex, List[datetime], datetime, str]
            Dates to retrieve.

        Returns
        -------
        List[str]
            List of FTP URLs.
        """
        if isinstance(dates, str | datetime.datetime | pd.Timestamp):
            dates = pd.DatetimeIndex([pd.to_datetime(dates)])
        else:
            dates = pd.to_datetime(dates)

        server = "ftp.star.nesdis.noaa.gov"
        base_dir = "/pub/smcd/VIIRS_Aerosol/npp.viirs.aerosol.data/epsaot550"

        urls = []
        for d in dates:
            year = d.strftime("%Y")
            yyyymmdd = d.strftime("%Y%m%d")
            # Example: ftp://ftp.star.nesdis.noaa.gov/pub/smcd/VIIRS_Aerosol/npp.viirs.aerosol.data/epsaot550/2023/npp_eaot_ip_gridded_0.25_20230101.high.nc
            url = f"ftp://{server}{base_dir}/{year}/npp_eaot_ip_gridded_0.25_{yyyymmdd}.high.nc"
            urls.append(url)
        return urls

build_urls(dates)

Build FTP URLs for NESDIS EPS VIIRS data based on dates.

Parameters:

Name Type Description Default
dates Union[DatetimeIndex, List[datetime], datetime, str]

Dates to retrieve.

required

Returns:

Type Description
List[str]

List of FTP URLs.

Source code in monetio/readers/nesdis_eps_viirs.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def build_urls(
    self, dates: pd.DatetimeIndex | list[datetime.datetime] | datetime.datetime | str
) -> list[str]:
    """
    Build FTP URLs for NESDIS EPS VIIRS data based on dates.

    Parameters
    ----------
    dates : Union[pd.DatetimeIndex, List[datetime], datetime, str]
        Dates to retrieve.

    Returns
    -------
    List[str]
        List of FTP URLs.
    """
    if isinstance(dates, str | datetime.datetime | pd.Timestamp):
        dates = pd.DatetimeIndex([pd.to_datetime(dates)])
    else:
        dates = pd.to_datetime(dates)

    server = "ftp.star.nesdis.noaa.gov"
    base_dir = "/pub/smcd/VIIRS_Aerosol/npp.viirs.aerosol.data/epsaot550"

    urls = []
    for d in dates:
        year = d.strftime("%Y")
        yyyymmdd = d.strftime("%Y%m%d")
        # Example: ftp://ftp.star.nesdis.noaa.gov/pub/smcd/VIIRS_Aerosol/npp.viirs.aerosol.data/epsaot550/2023/npp_eaot_ip_gridded_0.25_20230101.high.nc
        url = f"ftp://{server}{base_dir}/{year}/npp_eaot_ip_gridded_0.25_{yyyymmdd}.high.nc"
        urls.append(url)
    return urls

open_dataset(files=None, dates=None, **kwargs)

Reads NESDIS EPS VIIRS data.

Parameters:

Name Type Description Default
files Union[str, List[str]]

File path(s) or URL(s).

None
dates Union[DatetimeIndex, List[datetime], datetime, str]

Dates to retrieve. If files is None, this is used to build URLs.

None
**kwargs dict

Additional arguments passed to XarrayDriver.open.

{}

Returns:

Type Description
Dataset

The NESDIS EPS VIIRS dataset.

Source code in monetio/readers/nesdis_eps_viirs.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def open_dataset(
    self,
    files: str | list[str] = None,
    dates: pd.DatetimeIndex | list[datetime.datetime] | datetime.datetime | str = None,
    **kwargs,
) -> xr.Dataset:
    """
    Reads NESDIS EPS VIIRS data.

    Parameters
    ----------
    files : Union[str, List[str]], optional
        File path(s) or URL(s).
    dates : Union[pd.DatetimeIndex, List[datetime], datetime, str], optional
        Dates to retrieve. If files is None, this is used to build URLs.
    **kwargs : dict
        Additional arguments passed to XarrayDriver.open.

    Returns
    -------
    xr.Dataset
        The NESDIS EPS VIIRS dataset.
    """
    if files is None:
        if dates is None:
            raise ValueError("Either 'files' or 'dates' must be provided.")
        files = self.build_urls(dates)

    if "preprocess" not in kwargs:
        kwargs["preprocess"] = nesdis_eps_viirs_preprocess

    if "engine" not in kwargs:
        kwargs["engine"] = "h5netcdf"

    ds = super().open_dataset(files, **kwargs)

    # Update history
    ds = update_history(ds, "Read NESDIS EPS VIIRS data.")

    return ds

nesdis_eps_viirs_preprocess(ds)

Preprocess NESDIS EPS VIIRS dataset: assign coordinates and standardize.

Parameters:

Name Type Description Default
ds Dataset

Input dataset.

required

Returns:

Type Description
Dataset

Processed dataset.

Source code in monetio/readers/nesdis_eps_viirs.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
def nesdis_eps_viirs_preprocess(ds: xr.Dataset) -> xr.Dataset:
    """
    Preprocess NESDIS EPS VIIRS dataset: assign coordinates and standardize.

    Parameters
    ----------
    ds : xr.Dataset
        Input dataset.

    Returns
    -------
    xr.Dataset
        Processed dataset.
    """
    # 1. Standardize dimensions and coordinates
    ds = standardize_satellite_coords(ds)

    # 2. Identify grid size and generate coordinates if needed
    # EPS files typically have nlat=720, nlon=1440
    if "latitude" not in ds.variables:
        nlat = ds.sizes.get("y", 720)
        nlon = ds.sizes.get("x", 1440)

        lon_min = -179.875
        lon_max = -1.0 * lon_min
        lat_min = -89.875
        lat_max = -1.0 * lat_min
        lons = np.linspace(lon_min, lon_max, nlon)
        # EPS uses descending latitudes (lat_max to lat_min)
        lats = np.linspace(lat_max, lat_min, nlat)

        # Lazy coordinate generation using xr.broadcast
        lon1d = xr.DataArray(lons, dims=("x",), name="longitude")
        lat1d = xr.DataArray(lats, dims=("y",), name="latitude")
        lat2d, lon2d = xr.broadcast(lat1d, lon1d)

        ds = ds.assign_coords(
            latitude=lat2d.assign_attrs({"units": "degrees_north", "standard_name": "latitude"}),
            longitude=lon2d.assign_attrs({"units": "degrees_east", "standard_name": "longitude"}),
        )

    # 3. Handle Time
    if "time" not in ds.coords:
        ds = add_time_coord(ds, time_attr="time_coverage_start")
    if "time" not in ds.coords:
        ds = add_time_coord(ds, time_attr="DATE")

    # 4. Final cleaning and standardization
    if "aot_ip_out" in ds.data_vars:
        ds = ds.rename({"aot_ip_out": "aod_550"})
        # Mask invalid values (e.g., negative)
        ds["aod_550"] = ds["aod_550"].where(ds["aod_550"] >= 0)
        ds["aod_550"].attrs.update(
            {
                "long_name": "Aerosol Optical Thickness at 550nm",
                "units": "1",
                "standard_name": "atmosphere_optical_thickness_due_to_ambient_aerosol",
            }
        )

    return ds