Source code for pycif.plugins.datastreams.fluxes.dalecbethy.write
import os
from logging import info
import numpy as np
import xarray as xr
from .....utils.check.errclass import CifError, CifKeyError
from .....utils.hdf5 import _hdf5_lock
# Units the Fortran reader validates against (see model_sources/dalecbethy/
# src/ncread_forcing.f90::ncread_dynamic_forcing).
_UNITS = {
"swrad": "W/m2",
"temperature": "Celsius",
"precipitation": "mm/h",
"lwdown": "W/m2",
"soil_temperature": "Celsius",
}
[docs]
def write(self, name, flx_file, flx, mode="a", metadata=None, **kwargs):
"""Write D&B meteorological forcing to a native ``dynforcing.nc`` file.
Builds/appends into the D&B-native forcing format expected by
``model_sources/dalecbethy/src/ncread_forcing.f90::
ncread_dynamic_forcing``: ``ng``/``time`` dimensions and one ``(ng,
time)`` variable per meteorological component, with the exact
``units`` attribute the Fortran reader checks for. The mandatory
``yyyymmddhh`` calendar variable is *not* written here (it is not a
per-component ``("meteo", <component>)`` mapper input): see
:func:`pycif.plugins.models.dalecbethy.io.inputs.make_forcing.make_meteo`,
which copies it as-is from the original forcing file.
Values are scattered back from the tracer's active sample points
(``self.domain.active``, a subset of the full ``self.domain.ng`` grid
cells) to their original ``ng`` row; grid cells outside
``self.domain.active`` are left as ``NaN``.
Args:
self: the fluxes Plugin (with a ``dalecbethy`` ``domain`` attached).
name (str or list[str]): name(s) of the component(s) to write.
flx_file (str): the ``dynforcing.nc`` file to write/append to.
flx (xarray.DataArray or dict[str, xarray.DataArray]): forcing data,
with dimensions ``(time, lev, lat, lon)`` (``lev``/``lat`` of
size 1), as returned by :func:`~.read.read`.
mode (str): ``"w"`` to overwrite, ``"a"`` to append.
metadata (dict, optional): if given, ``metadata["domain"]`` is used
instead of ``self.domain``.
**kwargs: unused, kept for interface compatibility.
Raises:
CifKeyError: if no domain information can be found.
CifError: if the domain has no ``active``/``ng`` sample-point
selection, or a component has no known expected ``units``.
"""
if isinstance(name, str):
flx = {name: flx}
name = [name]
if hasattr(self, "domain"):
domain = self.domain
elif isinstance(metadata, dict) and "domain" in metadata:
domain = metadata["domain"]
else:
raise CifKeyError("Could not find information about the domain")
if not hasattr(domain, "active") or not hasattr(domain, "ng"):
raise CifError(
"Writing D&B dynamic forcing requires a 'dalecbethy' domain "
"(with an 'active'/'ng' sample-point selection)."
)
active = domain.active
ng = domain.ng
time = flx[name[0]]["time"].values
if os.path.isfile(flx_file) and mode == "a":
with _hdf5_lock:
with xr.open_dataset(flx_file) as ds_existing:
if ds_existing.sizes.get("time") != time.size:
raise CifError(
f"Trying to append D&B forcing data with "
f"{time.size} time steps into '{flx_file}' which "
f"already has {ds_existing.sizes.get('time')}."
)
data_vars = {}
for k in name:
if k not in _UNITS:
raise CifError(
f"Unknown D&B dynamic forcing component '{k}': expected "
f"units are only known for {sorted(_UNITS)}."
)
squeezed = flx[k].squeeze(["lat", "lev"]).transpose("time", "lon")
full = np.full((time.size, ng), np.nan)
full[:, active] = squeezed.values
data_vars[k] = xr.DataArray(
full.T, dims=("ng", "time"),
attrs={"units": _UNITS[k], "long_name": f"{k} (pyCIF-generated)"}
)
ds = xr.Dataset(data_vars)
if not os.path.isfile(flx_file) or mode == "w":
info(f"writing D&B dynamic forcing {name} to '{flx_file}'")
with _hdf5_lock:
ds.to_netcdf(flx_file, mode="w")
else:
info(f"appending D&B dynamic forcing {name} to '{flx_file}'")
with _hdf5_lock:
ds.to_netcdf(flx_file, mode="a")