Source code for imod.msw.meteo_grid

import csv
from pathlib import Path
from typing import Optional, Union

import numpy as np
import pandas as pd
import xarray as xr

import imod
from imod.msw.pkgbase import MetaSwapPackage
from imod.msw.timeutil import to_metaswap_timeformat


[docs] class MeteoGrid(MetaSwapPackage): """ This contains the meteorological grid data. Grids are written to ESRI ASCII files. The meteorological data requires a time coordinate. Next to a MeteoGrid instance, instances of PrecipitationMapping and EvapotranspirationMapping are required as well to specify meteorological information to MetaSWAP. This class is responsible for `mete_grid.inp`. Parameters ---------- precipitation: array of floats (xr.DataArray) Contains the precipitation grids in mm/d. A time coordinate is required. evapotranspiration: array of floats (xr.DataArray) Contains the evapotranspiration grids in mm/d. A time coordinate is required. """ _file_name = "mete_grid.inp" _meteo_dirname = "meteo_grids"
[docs] def __init__(self, precipitation: xr.DataArray, evapotranspiration: xr.DataArray): super().__init__() self.dataset["precipitation"] = precipitation self.dataset["evapotranspiration"] = evapotranspiration self._pkgcheck()
def write_free_format_file(self, path: Union[str, Path], dataframe: pd.DataFrame): """ Write free format file. The mete_grid.inp file is free format. """ columns = list(self.dataset.data_vars) dataframe.loc[:, columns] = '"' + dataframe[columns] + '"' # Add required columns, which we will not use. # These are only used when WOFOST is used # TODO: Add support for temperature to allow WOFOST support wofost_columns = [ "minimum_day_temperature", "maximum_day_temperature", "mean_temperature", ] dataframe.loc[:, wofost_columns] = '"NoValue"' self.check_string_lengths(dataframe) dataframe.to_csv( path, header=False, quoting=csv.QUOTE_NONE, float_format="%.4f", index=False ) def _compose_filename( self, d: dict, directory: Path, pattern: Optional[str] = None ): """ Construct a filename, following the iMOD conventions. Parameters ---------- d : dict dict of parts (time, layer) for filename. pattern : string or re.pattern Format to create pattern for. Returns ------- str Absolute path. """ return str(directory / imod.util.path.compose(d, pattern)) def _is_grid(self, varname: str): coords = self.dataset[varname].coords if "y" not in coords and "x" not in coords: return False else: return True def _compose_dataframe(self, times: np.ndarray): dataframe = pd.DataFrame(index=times) year, time_since_start_year = to_metaswap_timeformat(times) dataframe["time_since_start_year"] = time_since_start_year dataframe["year"] = year # Data dir is always relative to model dir, so don't use model directory # here data_dir = Path(".") / self._meteo_dirname for varname in self.dataset.data_vars: # If grid, we have to add the filename of the .asc to be written if self._is_grid(varname): dataframe[varname] = [ self._compose_filename( {"time": time, "name": varname, "extension": ".asc"}, directory=data_dir, ) for time in times ] else: dataframe[varname] = self.dataset[varname].values.astype(str) return dataframe def check_string_lengths(self, dataframe: pd.DataFrame): """ Check if strings lengths do not exceed 256 characters. With absolute paths this might be an issue. """ # Because two quote marks are added later. character_limit = 254 columns = list(self.dataset.data_vars) str_too_long = [ np.any(dataframe[varname].str.len() > character_limit) for varname in columns ] if any(str_too_long): indexes_true = np.where(str_too_long)[0] too_long_columns = list(np.array(columns)[indexes_true]) raise ValueError( f"Encountered strings longer than 256 characters in columns: {too_long_columns}" ) def write(self, directory: Union[str, Path], *args): """ Write mete_grid.inp and accompanying ASCII grid files. Parameters ---------- directory: str or Path directory to write file in. """ directory = Path(directory) times = self.dataset["time"].values dataframe = self._compose_dataframe(times) self.write_free_format_file(directory / self._file_name, dataframe) # Write grid data to ESRI ASCII files for varname in self.dataset.data_vars: if self._is_grid(varname): path = (directory / self._meteo_dirname / varname).with_suffix(".asc") imod.rasterio.save(path, self.dataset[varname], nodata=-9999.0) def _pkgcheck(self): for varname in self.dataset.data_vars: coords = self.dataset[varname].coords if "time" not in coords: raise ValueError(f"No 'time' coordinate included in {varname}") allowed_dims = ["time", "y", "x"] excess_dims = set(self.dataset[varname].dims) - set(allowed_dims) if len(excess_dims) > 0: raise ValueError( f"Received excess dims {excess_dims} in {self.__class__} for " f"{varname}, please provide data with {allowed_dims}" )