Source code for pycif.plugins.models.dalecbethy.io.outputs2native

import glob
import os
from logging import debug, info

import xarray as xr
import numpy as np

from .....utils.check.errclass import CifKeyError


# D&B output variable names (see src/ncwrite_output.f90) for each pyCIF
# output component declared in ini_mapper.py / __init__.py::output_components
_DB_VARNAME = {
    "NEE": "nee",
}


[docs] def outputs2native( self, data2dump, input_type, di, df, runsubdir, mode="fwd", onlyinit=False, check_transforms=False, **kwargs ): """Read D&B NetCDF diagnostics back into pyCIF objects. D&B writes its diagnostics to ``diagout/dalec-bethy_{hourly,daily}-output_<yyyymmdd_start>-<yyyymmdd_end>.nc`` (see ``src/ncwrite_output.f90``), on the ``nsp`` sample-point dimension (grid-cell x active-PFT combinations, see ``src/dimensions.f90``). The stream (``hourly``/``daily``) to read is controlled by ``self.output_resolution``. The output file also carries, on that same ``nsp`` dimension, the ``gidx`` (1-based grid-cell index, D&B's ``map_sample2grid``) and ``pft_fraction`` (D&B's ``fracv``) static fields. These are used here to recombine the PFT-level sample points of each grid cell into a single per-cell value (a ``pft_fraction``-weighted average), so the result lines up with the pyCIF ``domain`` built by ``pycif.plugins.domains.dalecbethy.read_domain`` (``domain.active``, on the same grid-cell indexing as ``gidx``). Args: self: the dalecbethy model Plugin. data2dump (dict): output data structure to fill for every component/tracer declared in the mapper. input_type (str): the type of model outputs to be processed (redundant with the components of ``data2dump``). di, df (datetime.datetime): start/end date of the present sub-simulation. runsubdir (str): path to the present sub-simulation work directory. mode (str): running mode; one of "fwd", "tl" and "adj". Returns: dict: a dictionary with structure the components/tracers to be extracted, with each component's per-grid-cell ``xarray.DataArray`` (PFT sample points recombined onto ``self.domain``'s grid cells). """ if mode not in ("fwd",): debug(f"dalecbethy outputs2native: nothing to do for mode={mode!r}") return {} stream = "hourly" if self.output_resolution == "hourly" else "daily" pattern = os.path.join( runsubdir, "diagout", f"dalec-bethy_{stream}-output_*.nc") matches = sorted(glob.glob(pattern)) if not matches: debug(f"No D&B {stream} output file found matching {pattern}") return {} ncfile = matches[0] info(f"Reading D&B output from {ncfile}") domain = self.domain active = domain.active dataout = {} with xr.open_dataset(ncfile) as ds: for key in ("gidx", "pft_fraction"): if key not in ds: raise CifKeyError( f"mandatory variable {key!r} not found in {ncfile}; " f"cannot recombine D&B's sample points back onto the " f"pyCIF domain's grid cells." ) # 0-based grid-cell index of each sample point (D&B writes it # 1-based, see src/static_fields.f90::map_sample2grid), and the # PFT area fraction to weight it by when recombining sample points # sharing the same grid cell (src/static_fields.f90::fracv). gidx = ds["gidx"].values.astype(int) - 1 weight = ds["pft_fraction"].values denom = np.zeros(domain.ng) np.add.at(denom, gidx, weight) for trid in data2dump: if trid[0] != input_type: continue varname = _DB_VARNAME.get(trid[1]) if varname is None or varname not in ds: debug( f"Variable for component {trid} (D&B name " f"{varname!r}) not found in {ncfile}; skipping." ) continue dates = ds.time.values nee_sp = ds[varname].values # (time, nsp) numer = np.zeros((dates.size, domain.ng)) np.add.at(numer, (slice(None), gidx), weight[np.newaxis, :] * nee_sp) with np.errstate(invalid="ignore"): nee_cell = numer / denom[np.newaxis, :] xmod = xr.DataArray( nee_cell[:, active][:, np.newaxis, np.newaxis, :], coords={"time": dates}, dims=("time", "lev", "lat", "lon"), ) dataout[trid] = {"spec": xmod} return dataout