"""VnGAD gridded-Tx observation source (metadata-independent; no GRIB). Depends only on the observational Tx NetCDF, so it is safe to build now and is what target construction and (later) forecast-to-observation alignment read from. Originals are opened read-only and never modified. """ import os from .interfaces import ObservationSource _ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) DEFAULT_TX = os.path.join(_ROOT, "data", "VnGAD_daily-0.25_1961-2023_tasmax.nc") class VnGADObservations(ObservationSource): """Local (UTC+7) daily-max Tx on the 0.25 deg VnGAD grid.""" def __init__(self, path=DEFAULT_TX, var="tasmax"): import xarray as xr self.path = path self.var = var self._ds = xr.open_dataset(path) # lazy; values read on demand self.lat = self._ds["lat"].values self.lon = self._ds["lon"].values def dates(self): """Observation local-day axis as datetime64[D].""" return self._ds["time"].values.astype("datetime64[D]") def tx(self, start=None, end=None): """Tx (time, lat, lon) in degC for [start, end] (inclusive, by date).""" da = self._ds[self.var] if start is not None or end is not None: da = da.sel(time=slice(start, end)) return da def valid_mask(self, ref_years=(2003, 2022)): """Time-stable valid-data mask (lat, lon) over the reference period, built from ACTUAL valid data (not a hard-coded land mask).""" import numpy as np d = self.dates() yr = d.astype("datetime64[Y]").astype(int) + 1970 sel = (yr >= ref_years[0]) & (yr <= ref_years[1]) arr = self._ds[self.var].values[sel] return np.isfinite(arr).any(0)