Source code for pywatershed.parameters.prms_parameters

import json
from copy import deepcopy

import numpy as np

from ..base import meta
from ..base.parameters import Parameters
from ..constants import fileish, ft2_per_acre, inches_per_foot, ndoy
from ..utils.prms5_file_util import PrmsFile

# TODO:
# PRMS uses "ndays"for the number of days in "year" defined as 366.
# I have changed this for pywatershed because it is ambiguous and have
# adopted ndoy instead, which is somewhat better but not perfect.
# I ndays is in a parameter file, it may cause errors that we should
# probably trap.

prms_dim_names = (
    "nhru",
    "nsegment",
    "ncascade",
    "ncascdgw",
    "nssr",
    "ngw",
    "npoigages",
    "nobs",
    "ndeplval",
    "ndepl",
    "nmonth",
    "ndoy",
    "scalar",
)


class JSONParameterEncoder(json.JSONEncoder):
    """
    Simple encoder to cast numpy objects to json-friendly formats
    """

    def default(self, obj):
        if isinstance(obj, np.integer):
            return int(obj)
        elif isinstance(obj, np.floating):
            return float(obj)
        elif isinstance(obj, np.ndarray):
            return obj.tolist()
        else:
            return super(JSONParameterEncoder, self).default(obj)


def _json_load(json_filename):
    with open(json_filename) as ff:
        pars = json.load(ff)

    # need to convert lists to numpy arrays
    for k, v in pars.items():
        if isinstance(v, list):
            pars[k] = np.array(v)
    return pars


[docs] class PrmsParameters(Parameters): """A parameter class with methods for native PRMS files. See Also -------- pywatershed.Parameters """
[docs] def __init__( self, dims: dict = None, coords: dict = None, data_vars: dict = None, metadata: dict = None, encoding: dict = None, validate: bool = True, copy: bool = True, ) -> None: if dims is None: dims = {} if coords is None: coords = {} if data_vars is None: data_vars = {} if metadata is None: metadata = {} if encoding is None: encoding = {} super().__init__( dims=dims, coords=coords, data_vars=data_vars, metadata=metadata, encoding=encoding, validate=validate, copy=copy, ) return
[docs] def parameters_to_json(self, json_filename) -> None: """write the parameters dictionary out to a json file""" with open(json_filename, "w") as ff: json.dump( {**self.dims, **self.parameters}, ff, indent=4, cls=JSONParameterEncoder, ) return None
@staticmethod def _from_dict(param_dict: dict) -> "PrmsParameters": """Load parameters from a dictionary of just parameters""" return PrmsParameters._after_load(param_dict)
[docs] @staticmethod def load_from_json(json_filename: fileish) -> "PrmsParameters": """Load parameters from a json file. Args: json_filename: json file path """ pars = _json_load(json_filename) params = PrmsParameters._process_file_input(pars) return params
[docs] @staticmethod def load(parameter_file: fileish) -> "PrmsParameters": """Load parameters from a PRMS parameter file Args: parameter_file: parameter file path Returns: PrmsParameters: full PRMS parameter dictionary """ data = PrmsFile(parameter_file, "parameter").get_data() params = PrmsParameters._process_file_input( data["parameter"]["parameters"], # data["parameter"]["parameter_dimensions"], ) return params
[docs] def to_netcdf(self, filename, use_xr=False) -> None: """Write PrmsParameters to a netcdf file""" self.data_vars if use_xr: self.to_xr_ds().to_netcdf(filename) else: self.to_nc4_ds(filename) return
@staticmethod def _process_file_input( parameter_dict: dict, parameter_dimensions_dict: dict = None, ) -> "PrmsParameters": # move dims from params to dims if parameter_dimensions_dict is None: parameter_dimensions_dict = {} dims = {} for dd in prms_dim_names: if dd in parameter_dict.keys(): dims[dd] = parameter_dict.pop(dd) if dd in parameter_dimensions_dict.keys(): _ = parameter_dimensions_dict.pop(dd) # add implied dimensions dims["ndoy"] = ndoy dims["nmonth"] = 12 # build dimension metadata from data if len(parameter_dimensions_dict) == 0: for key, value in parameter_dict.items(): # errors in the next line, see prms_dim_names at top param_dim_names = meta.get_params(key)[key]["dims"] parameter_dimensions_dict[key] = {"dims": param_dim_names} common_params = set(param_dim_names) & set(dims) if not len(common_params): parameter_dimensions_dict[key] = { "dims": tuple(["unknown"]) } continue param_dims = {kk: dims[kk] for kk in common_params} if isinstance(value, int): parameter_dimensions_dict[key] = None elif isinstance(value, np.ndarray): shape = value.shape temp_dims = [] for isize in shape: found_dim = False for dim_key, dim_value in param_dims.items(): if dim_value == isize: found_dim = True temp_dims.append(dim_key) break if isize == 1 and not found_dim: found_dim = True temp_dims.append("scalar") if not found_dim: raise ValueError( "Unable to identify dimension name for" f"parameter {key} with length {isize}" ) parameter_dimensions_dict[key] = {"dims": tuple(temp_dims)} # build coords, only some dims have coords that are not indexes coords = {} # edge case of dims that have only index or implied coords # this is justified, as this data should probably be in the parameter # files. netcdf neccisitates this to even carry the dimension data. # alternative would be to have a scalar, but in fact this is a # dimension of the data. coords["doy"] = np.arange(dims["ndoy"], dtype="int32") + 1 for cc in ["nhm_id", "nhm_seg", "poi_gage_id", "doy"]: coord_to_dim = { "nhm_id": "nhru", "nhm_seg": "nsegment", "poi_gage_id": "npoigages", "doy": "ndoy", } dim_name = coord_to_dim[cc] if dim_name not in dims: continue dim_val = dims[dim_name] if cc in parameter_dict.keys(): coords[cc] = parameter_dict.pop(cc) else: coord_to_dim = {"nhm_id": "nhru", "nhm_seg": "nsegment"} coords[cc] = np.arange(1, dim_val + 1) parameter_dimensions_dict[cc] = {"dims": (dim_name,)} # Derived parameter converting inches to cubic feet on hrus. parameter_dict["hru_in_to_cf"] = ( parameter_dict["hru_area"] * ft2_per_acre / (inches_per_foot) ) parameter_dimensions_dict["hru_in_to_cf"] = {"dims": ("nhru",)} for key, value in parameter_dimensions_dict.items(): key_meta = deepcopy(meta.get_params(key)[key]) _ = key_meta.pop("dims") parameter_dimensions_dict[key]["attrs"] = key_meta parameter_dimensions_dict["global"] = { "Description": "Parameter data for PRMS" } prms_params = PrmsParameters( dims=dims, coords=coords, data_vars=parameter_dict, metadata=parameter_dimensions_dict, validate=True, ) return prms_params