Source code for hyplan.winds.providers.gmao
"""GMAO GEOS-FP near-real-time wind field provider."""
from __future__ import annotations
import datetime
from typing import List, Optional, Tuple
import numpy as np
from ..gridded import _GriddedWindField
_GMAO_FP_URL = (
"dap2://opendap.nccs.nasa.gov/dods/GEOS-5/fp/0.25_deg/assim/inst3_3d_asm_Np"
)
# Epoch for GMAO time encoding: "days since 1-1-1 00:00:0.0"
_GMAO_EPOCH = np.datetime64("0001-01-01T00:00:00", "ns")
[docs]
class GMAOWindField(_GriddedWindField):
"""GMAO GEOS-FP near-real-time wind field for operational planning.
Fetches 3-hourly instantaneous U/V winds on pressure levels from
NCCS OPeNDAP server via the pydap DAP2 protocol. Typically covers
the last ~30 days of analysis plus short-range forecasts. No
credentials required.
Args:
lat_min: Southern latitude bound (degrees).
lat_max: Northern latitude bound (degrees).
lon_min: Western longitude bound (degrees).
lon_max: Eastern longitude bound (degrees).
time_start: Start of time window (UTC).
time_end: End of time window (UTC).
pressure_min_hpa: Top pressure level to fetch (hPa). Default 50.
pressure_max_hpa: Bottom pressure level to fetch (hPa). Default 1000.
url: Override the default GEOS-FP OPeNDAP URL.
"""
[docs]
def __init__(self, *args, url: Optional[str] = None, **kwargs):
self._base_url = url or _GMAO_FP_URL
super().__init__(*args, **kwargs)
def _build_urls(self) -> List[str]:
"""Single URL — GEOS-FP is served as a single aggregated dataset."""
return [self._base_url]
def _open_dataset(self, url: str):
"""Open via pydap engine with decode_times=False.
The NCCS OPeNDAP server's time variable uses a non-standard
epoch ("days since 1-1-1 00:00:0.0") that netCDF4 cannot decode.
Using pydap with DAP2 protocol avoids this issue.
"""
return self._xr.open_dataset(url, engine="pydap", decode_times=False)
def _var_names(self) -> Tuple[str, str]:
"""GEOS-FP uses lowercase u/v."""
return ("u", "v")
@staticmethod
def _datetime_to_gmao_days(dt: datetime.datetime) -> float:
"""Convert a datetime to GMAO 'days since 1-1-1' float."""
# proleptic Gregorian ordinal: Jan 1, year 1 = ordinal 1
naive = dt.replace(tzinfo=None) if dt.tzinfo else dt
ordinal = naive.toordinal() # 1-based (Jan 1, 0001 = 1)
frac = (naive.hour * 3600 + naive.minute * 60 + naive.second) / 86400.0
return float(ordinal) + frac
def _time_slice(self, time_coords: np.ndarray) -> slice:
"""Subset the aggregated GMAO time dimension to the request window."""
lo = self._datetime_to_gmao_days(self._time_start)
hi = self._datetime_to_gmao_days(self._time_end)
return self._index_range(time_coords, lo, hi)
def _decode_time(self, raw_time: np.ndarray) -> np.ndarray:
"""Convert GMAO 'days since 1-1-1' floats to datetime64[ns]."""
# raw_time values are fractional days since 0001-01-01.
# Subtract 1 because the epoch day itself is day 1, not day 0.
days: np.ndarray = (raw_time - 1).astype("timedelta64[D]")
frac_ns = ((raw_time - 1 - np.floor(raw_time - 1)) * 86400e9).astype(
"timedelta64[ns]"
)
return _GMAO_EPOCH + days + frac_ns # type: ignore[no-any-return]