class PCR(GBmi): """CSDMS-compliant BMI implementation for GLOFRIM. Arguments: GBmi {class} -- Interface (abstract base class) for a model that implements the CSDMS BMI (Basic Model Interface). Raises: Warning -- Warning is raised if two-step model initialization is not correctly executed Warning -- Warning is raised if two-step model initialization is not correctly executed Exception -- Raised if specified time is smaller than time step or later than model end time ValueError -- Raised if model time step is not whole number of update interval Exception -- Raised if end time is already reached as no further update is possible IOError -- function requires LDD file; if not found, IOerror is raised IOError -- function requires landsmask file; if not found, IOerror is raised ValueError -- start time must be yyyy-mm-dd; if not, ValueError is raised ValueError -- end time must be yyyy-mm-dd; if not, ValueError is raised """ _name = 'PCR' _long_name = 'PCR-GLOBWB' _version = '2.0.3' _var_units = {'runoff': 'm/day', 'discharge': 'm3/s', 'cellArea': 'm2'} _input_var_names = ['cellArea'] _output_var_names = ['runoff', 'discharge'] _area_var_name = 'cellArea' _timeunit = 'days' _dt = timedelta(days=1) # NOTE: this is fixed in PCR def __init__(self, loglevel=logging.INFO, logger=None): # import PCR-GLOBWB with BMI functions from pcrglobwb_bmi_v203 import pcrglobwb_bmi as _bmi self._bmi = _bmi.pcrglobwbBMI() if logger: self.logger = logger.getChild(self._name) else: self.logger = setlogger(None, self._name, thelevel=loglevel) self._loglevel = loglevel self.initialized = False self.grid = None ### ### Model Control Functions ### def initialize_config(self, config_fn): """Initializing the model configuration file. Aligning GLOFRIM specs for coupled runs to be consistent with overall run settings. Arguments: config_fn {str} -- path to model configuration file (for PCR-GLOBWB: ini-file) Raises: Warning -- Warning is raised if two-step model initialization is not correctly executed """ # config settings if self.initialized: raise Warning( "model already initialized, it's therefore not longer possible to initialize the config" ) self._config_fn = abspath(config_fn) self._config = glib.configread( self._config_fn, encoding='utf-8', cf=ConfigParser(inline_comment_prefixes=('#'))) self._datefmt = "%Y-%m-%d" # model time self._startTime = self.get_start_time() self._endTime = self.get_end_time() self._t = self._startTime # model files self._outdir = abspath( self.get_attribute_value('globalOptions:outputDir')) self.logger.info('Config initialized') def initialize_model(self, **kwargs): """Initializes the model, i.e. loading all files and checking for consistency; can only be executed after the model-specific config-file was initialized. This is essential for a successful two-step model initialization and aligned model data. Raises: Warning -- Warning is raised if two-step model initialization is not correctly executed """ if not hasattr(self, '_config_fn'): raise Warning('run initialize_config before initialize_model') self.write_config( ) # write updated config to file as bmi does not allow direct access self._bmi.initialize(self._config_fn) # stop pcr double logging. pcr_log = logging.getLogger() # pcr_log.setLevel(self._loglevel) self.logger.info("remove PCR logger because it's making to much noise") pcr_log.handlers = pcr_log.handlers[1:] self.initialized = True self.logger.info('Model initialized') # reset model time to make sure it is consistent with the model self._startTime = self.get_start_time() self._endTime = self.get_end_time() self._t = self._startTime def initialize(self, config_fn): """Initializes the model following a two-step initialization procedure. First, the config-file is initialized and where necessary aligned with overall model settings (e.g. output-dir, start and end time). Second, the model is actually initialized. Arguments: config_fn {str} -- path to model configuration file (for PCR-GLOBWB: ini-file) """ self.initialize_config(config_fn) self.initialize_model() def update(self, dt=None): """Updating model for a certain time step interval (default: None). Checks whether model end time is already reached. Requires that model-specific time step is a whole number of update time step. Keyword Arguments: dt {int} -- update time step interval [s] at which information is exchanged between models (default: {None}) Raises: ValueError -- Raised if model time step is not whole number of update interval Exception -- Raised if end time is already reached as no further update is possible """ # dt in seconds. if not given model timestep is used if self._t >= self._endTime: raise Exception("endTime already reached, model not updated") if (dt is not None) and (dt != self._dt.total_seconds()): _dt = timedelta(seconds=dt) if not glib.check_dts_divmod(_dt, self._dt): msg = "Invalid value for dt in comparison to model dt. Make sure a whole number of model timesteps ({}) fit in the given dt ({})" raise ValueError(msg.format(self._dt, _dt)) self._bmi.update(_dt.days) self._t += _dt else: self._bmi.update(self._dt.days) self._t += self._dt self.logger.info('updated model to datetime {}'.format( self._t.strftime("%Y-%m-%d %H:%M"))) def update_until(self, t, dt=None): """Updates the model until a certain time t is reached with an update time step dt. Arguments: t {int} -- Model time until which the model is updated Keyword Arguments: dt {int} -- update time step interval [s] at which information is exchanged between models (default: {None}) Raises: Exception -- Raised if specified time is smaller than time step or later than model end time """ if (t < self._t) or t > self._endTime: raise Exception( "wrong time input: smaller than model time or larger than endTime" ) while self._t < t: self.update(dt=dt) def spinup(self): """PCR-GLOBWB specific function. Runs the in ini-file specified number of spin-up years. """ self._bmi.spinup() def finalize(self): """Finalizes the model, i.e. shuts down all operations and closes output files. """ self.logger.info('finalize bmi. Close logger.') self._bmi.finalize() closelogger(self.logger) ### ### Variable Information Functions ### def get_start_time(self): """Provides start time of PCR-GLOBWB Returns: date -- start time of PCR-GLOBWB """ if self.initialized: # date to datetime object startTime = datetime.combine(self._bmi.get_start_time(), datetime.min.time()) else: startTime = self.get_attribute_value('globalOptions:startTime') startTime = datetime.strptime(startTime, self._datefmt) self._startTime = startTime return self._startTime def get_current_time(self): """Provides current model time of PCR-GLOBWB Returns: [date] -- current model time of PCR-GLOBWB """ if self.initialized: return self._t else: return self.get_start_time() def get_end_time(self): """Provides end time of PCR-GLOBWB Returns: date -- end time of PCR-GLOBWB """ if self.initialized: # date to datetime object endTime = datetime.combine(self._bmi.get_end_time(), datetime.min.time()) else: endTime = self.get_attribute_value('globalOptions:endTime') endTime = datetime.strptime(endTime, self._datefmt) self._endTime = endTime return self._endTime def get_time_step(self): """Provides time step of PCR-GLOBWB Returns: date -- time step of PCR-GLOBWB """ return self._dt def get_time_units(self): """Provides time unit of PCR-GLOBWB. Returns: str -- time unit of model of PCR-GLOBWB """ return self._timeunit ### ### Variable Getter and Setter Functions ### def get_value(self, long_var_name, fill_value=-999, **kwargs): """Retrieval of all values of a certain exposed variable; entries with fill_value are replaced with NaN values Arguments: long_var_name {str} -- name of exposed PCR-GLOBWB variable Keyword Arguments: fill_value {int} -- fill_value used in PCR-GLOBWB (default: {-999}) Returns: array -- array with all entries of retrieved variable """ # additional fill_value argument required to translate pcr maps to numpy arrays var = np.asarray( self._bmi.get_var(long_var_name, missingValues=fill_value, **kwargs)) var = np.where(var == fill_value, np.nan, var) return var def get_value_at_indices(self, long_var_name, inds, fill_value=-999, **kwargs): """Retrieval of value at a specific index of a certain exposed variable; entries with fill_value are replaced with NaN values Arguments: long_var_name {str} -- name of exposed PCR-GLOBWB variable inds {int} -- Index pointing to entry within entire array of variable values Keyword Arguments: fill_value {int} -- fill_value used in PCR-GLOBWB (default: {-999}) Returns: float -- value at specific index of retrieved variable """ return self.get_value(long_var_name, fill_value=fill_value, **kwargs).flat[inds] def set_value(self, long_var_name, src, fill_value=-999, **kwargs): """Overwriting of all values of a certain exposed variable with provided new values; entries with NaN value are replaced with fill_value; provided new values must match shape of aim variable Arguments: long_var_name {str} -- name of exposed PCR-GLOBWB variable src {array} -- array with new values Keyword Arguments: fill_value {int} -- fill_value used in PCR-GLOBWB (default: {-999}) """ src = np.where(np.isnan(src), fill_value, src).astype(self.get_var_type(long_var_name)) self._bmi.set_var(long_var_name, src, missingValues=fill_value, **kwargs) def set_value_at_indices(self, long_var_name, inds, src, fill_value=-999, **kwargs): """Overwriting of value at specific entry of a certain exposed variable with provided new values; entries with NaN value are replaced with fill_value Arguments: long_var_name {str} -- name of exposed PCR-GLOBWB variable inds {int} -- Index pointing to entry within entire array of variable values src {array} -- array with new values Keyword Arguments: fill_value {int} -- fill_value used in PCR-GLOBWB (default: {-999}) """ val = self.get_value(long_var_name, fill_value=fill_value, **kwargs) val.flat[inds] = src self.set_value(long_var_name, val, **kwargs) def _get_tot_volume_in(self): # precipitation [m] P = np.nansum( self.get_value('precipitation') * self.get_value('cellArea')) return P def _get_tot_volume_out(self): # discharge [m3/s] Qout = np.nansum(self.get_value_at_indices( 'discharge', self.grid.pits)) * self._dt.total_seconds() # ET [m] ?? ET = np.nansum(self.get_value('actualET') * self.get_value('cellArea')) return Qout + ET ### ### Grid Information Functions ### def get_grid(self): """Retreving spatial information about model domain from PCR-GLOBWB landmask file using rasterio operations; also retrieving PCR-GLOBWB drainage direction (LDD) information Raises: IOError -- function requires LDD file; if not found, IOerror is raised IOError -- function requires landsmask file; if not found, IOerror is raised Returns: grid -- rasterio grid object """ if not hasattr(self, 'grid') or (self.grid is None): # ldd is used for coupling to routing / flood model _indir = abspath( self.get_attribute_value('globalOptions:inputDir')) _ldd_fn = glib.getabspath( self.get_attribute_value('routingOptions:lddMap'), _indir) if not isfile(_ldd_fn): raise IOError('ldd file not found {}'.format(_ldd_fn)) # landmask used for masking out coordinates outside mask if provided _lm_fn = glib.getabspath( self.get_attribute_value('globalOptions:landmask'), _indir) if isfile(_lm_fn): with rasterio.open(_lm_fn, 'r') as ds: mask = ds.read(1) == 1 mask_name = basename(_lm_fn) # use ldd for masking else: mask = None mask_name = basename(_ldd_fn) self.logger.info( 'Getting rgrid info based on {}; mask based on {}'.format( basename(_ldd_fn), mask_name)) with rasterio.open(_ldd_fn, 'r') as ds: if mask is None: mask = ds.read(1) != ds.nodata self.grid = RGrid(ds.transform, ds.height, ds.width, crs=ds.crs, mask=mask) # read file with pcr readmap self.logger.info('Getting drainage direction from {}'.format( basename(_ldd_fn))) self.grid.set_dd(_ldd_fn, ddtype='ldd') self.grid.get_pits() return self.grid ### ### set and get attribute / config ### def set_start_time(self, start_time): """Overwriting default model start time with user-specified start time; format of provided start time must be yyyy-mm-dd Arguments: start_time {date} -- user-specified start time Raises: ValueError -- start time must be yyyy-mm-dd; if not, ValueError is raised """ if isinstance(start_time, datetime): start_time = start_time.strftime(self._datefmt) try: self._startTime = datetime.strptime(start_time, self._datefmt) self._t = self._startTime except ValueError: raise ValueError('wrong date format, use "yyyy-mm-dd"') self.set_attribute_value('globalOptions:startTime', start_time) def set_end_time(self, end_time): """Overwriting default model end time with user-specified end time; format of provided end time must be yyyy-mm-dd Arguments: end_time {date} -- user-specified end time Raises: ValueError -- end time must be yyyy-mm-dd; if not, ValueError is raised """ if isinstance(end_time, datetime): end_time = end_time.strftime(self._datefmt) try: self._endTime = datetime.strptime(end_time, self._datefmt) except ValueError: raise ValueError('wrong date format, use "yyyy-mm-dd"') self.set_attribute_value('globalOptions:endTime', end_time) def set_out_dir(self, out_dir): """Setting output directory of PCR-GLOBWB; overwrites the default output directory Arguments: out_dir {str} -- path to output directory """ self.set_attribute_value('globalOptions:outputDir', abspath(out_dir)) def get_attribute_names(self): """Provides list with all model attribute names from model config file Returns: list -- list with model attribute names """ glib.configcheck(self, self.logger) return glib.configattr(self._config) def get_attribute_value(self, attribute_name): """Gets attribute value in in underlying model; attribute_name should use the following convention model_name.section_name:attribute_name Arguments: attribute_name {str} -- Name of BMI attribute Returns: float -- value of attribute """ glib.configcheck(self, self.logger) self.logger.debug("get_attribute_value: " + attribute_name) return glib.configget(self._config, attribute_name) def set_attribute_value(self, attribute_name, attribute_value): """sets attribute value in in underlying model; attribute_name should use the following convention model_name.section_name:attribute_name Arguments: attribute_name {str} -- Name of BMI attribute attribute_value {float} -- value to be est Returns: [str] -- [no clue] """ glib.configcheck(self, self.logger) self.logger.debug("set_attribute_value: " + attribute_value) return glib.configset(self._config, attribute_name, attribute_value) def write_config(self): """Write adapted config to file; just before initializing; only for models which do not allow for direct access to model config via bmi """ self._config_fn = glib.write_config(self, self._config, self._config_fn, self.logger)
class WFL(GBmi): """ csdms BMI implementation of the WFLOW BMI adaptor for GLOFRIM. """ _name = 'WFL' _long_name = 'wflow' _version = '' _var_units = {} _input_var_names = [] _output_var_names = [] _area_var_name = 'csize' _timeunit = 'seconds' def __init__(self, loglevel=logging.INFO, logger=None): # import original PCR bmi import wflow.wflow_bmi as _bmi self._bmi = _bmi.wflowbmi_csdms() if logger: self.logger = logger.getChild(self._name) else: self.logger = setlogger(None, self._name, thelevel=loglevel) self._loglevel = loglevel self.initialized = False self.grid = None """ Model Control Functions """ def initialize_config(self, config_fn): # config settings if self.initialized: raise Warning( "model already initialized, it's therefore not longer possible to initialize the config" ) self._config_fn = abspath(config_fn) self.logger.info('Read ini at {}'.format(self._config_fn)) self._bmi.initialize_config(str(self._config_fn), loglevel=self._loglevel) self._config = self._bmi.config self._datefmt = "%Y-%m-%d %H:%M:%S" if self.get_attribute_value('run:runlengthdetermination') == 'steps': raise Warning( 'set WFLOW run:runlengthdetermination to "intervals" in order to use the same time convention as in GLOFRIM' ) # model time self._dt = self.get_time_step() self._startTime = self.get_start_time() self._endTime = self.get_end_time() self._t = self._startTime # model files self.logger.info('Config initialized') namesroles = self._bmi.dynModel.wf_supplyVariableNamesAndRoles() for n, r, u in namesroles: if r == 0 or r == 3: #input or parameter self._input_var_names.append(n) elif r == 1 or r == 2: # output or state self._output_var_names.append(n) self._var_units[n] = u def initialize_model(self, **kwargs): if not hasattr(self, '_config_fn'): raise Warning('run initialize_config before initialize_model') self._bmi.initialize_model() self.initialized = True self.logger.info('Model initialized') # reset model time to make sure it is consistent with the model self._dt = self.get_time_step() self._startTime = self.get_start_time() self._endTime = self.get_end_time() self._t = self._startTime def initialize(self, config_fn): self.initialize_config(config_fn) self.initialize_model() def update(self, dt=None): # dt in seconds. if not given model timestep is used if self._t >= self._endTime: raise Exception("endTime already reached, model not updated") if (dt is not None) and (dt != self._dt.total_seconds()): dt = timedelta(seconds=dt) if not glib.check_dts_divmod(dt, self._dt): msg = "Invalid value for dt in comparison to model dt. Make sure a whole number of model timesteps ({}) fit in the given dt ({})" raise ValueError(msg.format(self._dt, dt)) tt = timedelta(0) while tt < dt: # loop required ad self._bmi.update(dt) does not work self._bmi.update() tt += self._dt else: self._bmi.update() self._t = self.get_current_time() self.logger.info('updated model to datetime {}'.format( self._t.strftime("%Y-%m-%d %H:%M"))) def update_until(self, t, dt=None): if (t < self._t) or t > self._endTime: raise Exception( "wrong time input: smaller than model time or larger than endTime" ) while self._t < t: self.update(dt=dt) def spinup(self): """PCR specific spinup function""" raise NotImplementedError() def finalize(self): self.logger.info('finalize bmi. Close logger.') self._bmi.finalize() closelogger(self.logger) """ Variable Information Functions """ def get_start_time(self): # if self.initialized: # startTime = datetime.utcfromtimestamp(self._bmi.get_start_time()) # else: # startTime = self.get_attribute_value('run:starttime') # startTime = datetime.strptime(startTime, self._datefmt) startTime = datetime.utcfromtimestamp(self._bmi.get_start_time()) self._startTime = startTime return self._startTime def get_current_time(self): if self.initialized: # return datetime.utcfromtimestamp(self._bmi.get_current_time()) else: return self.get_start_time() def get_end_time(self): # if self.initialized: # endTime = datetime.utcfromtimestamp(self._bmi.get_end_time()) # else: # endTime = self.get_attribute_value('run:endtime') # endTime = datetime.strptime(endTime, self._datefmt) endTime = datetime.utcfromtimestamp(self._bmi.get_end_time()) self._endTime = endTime return self._endTime def get_time_step(self): if not hasattr(self, '_dt'): self._dt = timedelta( **{self.get_time_units(): self._bmi.get_time_step()}) return self._dt def get_time_units(self): return self._timeunit """ Variable Getter and Setter Functions """ def get_value(self, long_var_name, fill_value=-999, **kwargs): # additional fill_value argument required to translate pcr maps to numpy arrays # NOTE that wflow flips the arrays upside down in the 'wf_supplyMapAsNumpy' function that is called by the bmi. # TODO: check if we can improve the wflow_bmi and change this var = np.asarray(self._bmi.get_value( long_var_name))[::-1, :] # flip array back to N->S var = np.where(var == fill_value, np.nan, var) return var def get_value_at_indices(self, long_var_name, inds, fill_value=-999, **kwargs): return self.get_value(long_var_name, fill_value=fill_value, **kwargs).flat[inds] def set_value(self, long_var_name, src, fill_value=-999): src = np.where(np.isnan(src), fill_value, src).astype(self.get_var_type(long_var_name)) # we need to flip the data again! see note in get_value command self._bmi.set_value(long_var_name, src[::-1, :]) def set_value_at_indices(self, long_var_name, inds, src, fill_value=-999): val = self.get_value(long_var_name, fill_value=fill_value) val.flat[inds] = src self.set_value(long_var_name, val) """ Grid Information Functions """ def get_grid(self, mapdir=None): if not hasattr(self, 'grid') or (self.grid is None): if mapdir is None: mapdir = join(dirname(self._config_fn), 'staticmaps') # ldd is used for coupling to routing / flood model _ldd_fn = join(mapdir, 'wflow_ldd.map') if not isfile(_ldd_fn): raise IOError('ldd file not found') # landmask used for masking out coordinates outside mask _lm_fn = join(mapdir, 'wflow_subcatch.map') if not isfile(_lm_fn): raise IOError('subcatch file not found') self.logger.info('Getting rgrid info based on {}'.format( basename(_lm_fn))) with rasterio.open(_lm_fn, 'r') as ds: # TODO: change transform instead of flipping the grid every time .. self.grid = RGrid(ds.transform, ds.height, ds.width, crs=ds.crs, mask=ds.read(1) >= 1) # river is used for the 1D cells _riv_fn = join(mapdir, 'wflow_river.map') if isfile(_riv_fn): with rasterio.open(_riv_fn, 'r') as ds: row, col = np.where(ds.read(1) == 1) x, y = self.grid.xy(row=row, col=col) inds = self.grid.ravel_multi_index(row, col) self.grid.set_1d(nodes=np.array(zip(x, y)), links=None, inds=inds) # read file with pcr readmap self.logger.info('Getting drainage direction from {}'.format( basename(_ldd_fn))) self.grid.set_dd(_ldd_fn, ddtype='ldd') return self.grid """ set and get attribute / config """ def set_start_time(self, start_time): if isinstance(start_time, str): start_time = datetime.strptime(start_time, self._datefmt) t = (start_time - datetime.utcfromtimestamp(0)) self._startTime = start_time self._t = start_time self._bmi.set_start_time(t.days * 86400 + t.seconds) def set_end_time(self, end_time): if isinstance(end_time, str): end_time = datetime.strptime(end_time, self._datefmt) t = (end_time - datetime.utcfromtimestamp(0)) self._endTime = end_time self._bmi.set_end_time(t.days * 86400 + t.seconds) def set_out_dir(self, out_dir): # NOTE this needs te be set somewhere pre initialize config # the file setup is hardcoded in wflow pass def get_attribute_names(self): glib.configcheck(self, self.logger) return glib.configattr(self._config) def get_attribute_value(self, attribute_name): glib.configcheck(self, self.logger) self.logger.debug("get_attribute_value: " + attribute_name) return self._bmi.get_attribute_value(attribute_name) def set_attribute_value(self, attribute_name, attribute_value): glib.configcheck(self, self.logger) self.logger.info("set_attribute_value {} -> {} ".format( attribute_name, attribute_value)) # update config glib.configset(self._config, attribute_name, attribute_value) # set wfl config return self._bmi.set_attribute_value(attribute_name, attribute_value) def write_config(self): """write adapted config to file. just before initializing only for models which do not allow for direct access to model config via bmi""" glib.write_config(self, self._config, self._config_fn, self.logger)