Exemple #1
0
 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
Exemple #2
0
    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
Exemple #3
0
 def get_grid(self):
     if not hasattr(self, 'grid') or (self.grid is None):
         # dem file used for rgrid and mask of 2D domain
         _dem_fn = glib.getabspath(str(self.get_attribute_value('DEMfile')),
                                   self._mapdir)
         if not isfile(_dem_fn): raise IOError('DEMfile file not found')
         self.logger.info('Getting rgrid info based on {}'.format(
             basename(_dem_fn)))
         with rasterio.open(_dem_fn, 'r') as ds:
             self.grid = RGrid(ds.transform,
                               ds.height,
                               ds.width,
                               crs=ds.crs,
                               mask=ds.read(1) != ds.nodata)
         # riv width file used for "1D coords"
         _width_fn = glib.getabspath(
             str(self.get_attribute_value('SGCwidth')), self._mapdir)
         if not isfile(_width_fn): raise IOError('SGCwidth file not found')
         with rasterio.open(_width_fn, 'r') as ds:
             row, col = np.where(ds.read(1) > 0)
             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)
     return self.grid
Exemple #4
0
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)
Exemple #5
0
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)
Exemple #6
0
class LFP(GBmi):
    """
    Glofrim implementation of the LFP BMI adaptor.
    """
    _name = 'LFP'
    _long_name = 'LISFlood-FP'
    _version = '5.9'
    _var_units = {'SGCQin': 'm3/s', 'dA': 'm2', 'H': 'm'}
    _input_var_names = ['SGCQin', 'dA']
    _output_var_names = ['SGCQin', 'H']
    _area_var_name = 'dA'
    _timeunit = 'seconds'

    def __init__(self, engine, loglevel=logging.INFO, logger=None):
        self._bmi = _bmi(engine=engine)
        if logger:
            self.logger = logger.getChild(self._name)
        else:
            self.logger = setlogger(None, self._name, thelevel=loglevel)
        self.initialized = False
        self.grid = None

    """
    Model Control Functions
    """

    def initialize_config(self, config_fn):
        if self.initialized:
            raise Warning(
                "model already initialized, it's therefore not longer possible to initialize the config"
            )
        # config settings
        self._config_fn = abspath(config_fn)
        self._config = glib.configread(self._config_fn,
                                       encoding='utf-8',
                                       cf=ParConfigParser())
        self._datefmt = "%Y-%m-%d"
        # 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
        _root = dirname(self._config_fn)
        # mapdir where nextxy data is found
        self._mapdir = dirname(
            glib.getabspath(str(self.get_attribute_value('DEMfile')), _root))
        self._outdir = glib.getabspath(
            str(self.get_attribute_value('dirroot')), _root)
        self.logger.info('Config initialized')

    def initialize_model(self, **kwargs):
        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)
        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):
        if not hasattr(self, '_config'):
            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)
            # because of the adaptive timestep scheme do not check the dt value
            # 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))
        else:
            dt = self._dt
        t_next = self.get_current_time() + dt
        i = 0
        while self._t < t_next:
            self._bmi.update()
            self._t = self.get_current_time()
            i += 1
        self.logger.info(
            'updated model to datetime {} in {:d} iterations'.format(
                self._t.strftime("%Y-%m-%d %H:%M:%S"), i))

    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)

    # not defined in CMF
    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):
        refdate = self.get_attribute_value('refdate')
        refdate = datetime.strptime(refdate, self._datefmt)
        if self.initialized:
            TStart = self._bmi.get_start_time()
        else:
            TStart = 0.
        startTime = refdate + timedelta(**{self.get_time_units(): TStart})
        self._startTime = startTime
        return self._startTime

    def get_current_time(self):
        if self.initialized:
            curtime = timedelta(
                **{self.get_time_units(): self._bmi.get_current_time()})
            return self._startTime + curtime
        else:
            return self.get_start_time()

    def get_end_time(self):
        if self.initialized:
            # TODO end time after initialization is not correct
            TStop = self._bmi.get_end_time()
            # pass
        else:
            TStop = float(self.get_attribute_value('sim_time'))
        endTime = self.get_start_time() + timedelta(
            **{self.get_time_units(): TStop})
        self._endTime = endTime
        return self._endTime

    def get_time_step(self):
        if self.initialized:
            dt = self._bmi.get_time_step()
        else:
            dt = float(self.get_attribute_value('initial_tstep'))
        self._dt = timedelta(**{self.get_time_units(): dt})
        return self._dt

    def get_time_units(self):
        return self._timeunit

    """
    Variable Getter and Setter Functions
    """

    def get_value(self, long_var_name, **kwargs):
        return np.asarray(self._bmi.get_var(long_var_name)).copy()

    def get_value_at_indices(self, long_var_name, inds, **kwargs):
        return self.get_value(long_var_name).flat[inds]

    def set_value(self, long_var_name, src, **kwargs):
        # LFP does not have a set_var function, but used the get_var function with an extra argument
        self._bmi.get_var(long_var_name)[:] = src.astype(
            self.get_var_type(long_var_name))

    def set_value_at_indices(self, long_var_name, inds, src, **kwargs):
        val = self.get_value(long_var_name)
        val.flat[inds] = src
        self.set_value(long_var_name, val)

    """
    Grid Information Functions
    """

    def get_grid(self):
        if not hasattr(self, 'grid') or (self.grid is None):
            # dem file used for rgrid and mask of 2D domain
            _dem_fn = glib.getabspath(str(self.get_attribute_value('DEMfile')),
                                      self._mapdir)
            if not isfile(_dem_fn): raise IOError('DEMfile file not found')
            self.logger.info('Getting rgrid info based on {}'.format(
                basename(_dem_fn)))
            with rasterio.open(_dem_fn, 'r') as ds:
                self.grid = RGrid(ds.transform,
                                  ds.height,
                                  ds.width,
                                  crs=ds.crs,
                                  mask=ds.read(1) != ds.nodata)
            # riv width file used for "1D coords"
            _width_fn = glib.getabspath(
                str(self.get_attribute_value('SGCwidth')), self._mapdir)
            if not isfile(_width_fn): raise IOError('SGCwidth file not found')
            with rasterio.open(_width_fn, 'r') as ds:
                row, col = np.where(ds.read(1) > 0)
                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)
        return self.grid

    """
    set and get attribute / config 
    """

    def set_start_time(self, start_time):
        if isinstance(start_time, datetime):
            refdate = start_time.strftime(self._datefmt)
        elif isinstance(start_time, str):
            try:
                refdate = start_time  # str
                start_time = datetime.strptime(start_time,
                                               self._datefmt)  # check format
            except ValueError:
                raise ValueError('wrong date format, use "yyyy-mm-dd"')
        else:
            raise ValueError('wrong start_date datatype')
        self._startTime = start_time
        self._t = start_time
        self.set_attribute_value('refdate', refdate)

    def set_end_time(self, end_time):
        if isinstance(end_time, str):
            try:
                end_time = datetime.strptime(end_time, self._datefmt)
            except ValueError:
                raise ValueError('wrong end_date format, use "yyyy-mm-dd"')
        if not isinstance(end_time, datetime):
            raise ValueError('wrong end_date datatype')
        refdate = self.get_start_time()
        assert end_time > refdate
        TStop = (end_time -
                 refdate).seconds + (end_time - refdate).days * 86400
        TStop = '{:.0f}'.format(TStop)
        self._endTime = end_time
        self.set_attribute_value('sim_time', TStop)

    def set_out_dir(self, out_dir):
        self.set_attribute_value('dirroot',
                                 relpath(out_dir, dirname(self._config_fn)))
        self._outdir = abspath(out_dir)

    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)
        # always use "general" as config header; as file has no config header this is hard-coded
        if ':' not in attribute_name:
            attribute_name = 'general:{}'.format(attribute_name)
        else:
            attribute_name = 'general:{}'.format(attribute_name.split(':')[1])
        self.logger.debug("get_attribute_value: {}".format(attribute_name))
        return glib.configget(self._config, attribute_name)

    def set_attribute_value(self, attribute_name, attribute_value):
        glib.configcheck(self, self.logger)
        # always use "general" as config header; as file has no config header this is hard-coded
        if ':' not in attribute_name:
            attribute_name = 'general:{}'.format(attribute_name)
        else:
            attribute_name = 'general:{}'.format(attribute_name.split(':')[1])
        self.logger.debug("set_attribute_value: {} -> {}".format(
            attribute_name, attribute_value))
        return glib.configset(self._config, attribute_name,
                              str(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)