Define and use your own Mass-Balance Model#

In this notebook we define a Mass-Balance model, open the glacier geometry at the RGI date (represented by the flowlines) and put these two parts together to conduct a dynamic glacier model run.

We start with the already well-known initialisation of our glacier of choice:

import numpy as np
import matplotlib.pyplot as plt
from oggm import cfg, utils, workflow, tasks
from oggm.cfg import SEC_IN_YEAR

cfg.initialize(logging_level='WARNING')

# Local working directory (where OGGM will write its output)
WORKING_DIR = utils.gettempdir('OGGM_model_from_scratch')
cfg.PATHS['working_dir'] = WORKING_DIR

rgi_ids = ['RGI60-11.00897']

cfg.PARAMS['border'] = 80
base_url = 'https://cluster.klima.uni-bremen.de/~oggm/gdirs/oggm_v1.6/L3-L5_files/2023.1/elev_bands/W5E5'
gdirs = workflow.init_glacier_directories(rgi_ids, from_prepro_level=3, prepro_base_url=base_url)
gdir = gdirs[0]
2023-03-23 08:20:32: oggm.cfg: Reading default parameters from the OGGM `params.cfg` configuration file.
2023-03-23 08:20:32: oggm.cfg: Multiprocessing switched OFF according to the parameter file.
2023-03-23 08:20:32: oggm.cfg: Multiprocessing: using all available processors (N=8)
2023-03-23 08:20:32: oggm.workflow: init_glacier_directories from prepro level 3 on 1 glaciers.
2023-03-23 08:20:32: oggm.workflow: Execute entity tasks [gdir_from_prepro] on 1 glaciers

Define your own Mass-Balance Model#

Here is an example of how you can provide your own Mass-Balance Model to OGGM.

This is done using Object-Oriented Programming, which means our newly created class RandomLinearMassBalance can build upon the existing OGGM class MassBalanceModel. By doing so we can make sure that our new Mass-Balance Model has the principle structure that OGGM expects a Mass-Balance Model to look like.

Now we can define the behaviour of our Mass-Balance Model as we wish. At a minimum, we need to define get_annual_mb(), which should return the mass-balance value for given heights and at a given year.

So let’s have a look at what the code could look like:

# here we import the base class for all MassBalanceModels inside of OGGM
from oggm.core.massbalance import MassBalanceModel

# now we define that our new class RandomLinearMassBalance 
# should build upon OGGMs MassBalanceModel
class RandomLinearMassBalance(MassBalanceModel):
    """Mass-balance as a linear function of altitude with random ELA.
    
    The reference ELA is taken as the 40% percentile altitude of the glacier.
    It then varies randomly from year to year.
    This class implements the MassBalanceModel interface so that the
    dynamical model can use it. Even if you are not familiar with object
    oriented programming, I hope that the example below is simple enough.
    """

    # here we can define changeable parameters of our model, and also
    # define default values for them
    def __init__(self, gdir, ela_h, grad=3., sigma_ela=200., seed=None):
        """ Initialize.
        Parameters
        ----------
        gdir : oggm.GlacierDirectory
            the working glacier directory
        ela_h : float
            the average ELA
        grad: float
            Mass-balance gradient (unit: [mm w.e. yr-1 m-1])
        sigma_ela: float
            The standard deviation of the ELA (unit: [m])
        seed : int, optional
            Random seed used to initialize the pseudo-random number generator.
        Attributes
        ----------
        temp_bias : float, default 0
            A "temperature bias" doesn't makes much sense in the linear MB
            context, but we implemented a simple empirical rule:
            + 1K -> ELA + 150 m
        """
        super(RandomLinearMassBalance, self).__init__()
        self.valid_bounds = [-1e4, 2e4]  # in m
        self.grad = grad
        self.orig_ela_h = ela_h
        self.sigma_ela = sigma_ela
        self.hemisphere = 'nh'
        self.rng = np.random.RandomState(seed)
        self.ela_h_per_year = dict()  # empty dictionary

    # this is a unique attribute of our MassBalance model we are currently
    # defining
    def get_random_ela_h(self, year):
        """This generates a random ELA for the requested year.
        Since we do not know which years are going to be asked for we generate
        them on the go.
        """

        year = int(year)
        if year in self.ela_h_per_year:
            # Nothing to be done
            return self.ela_h_per_year[year]

        # Else we generate it for this year
        ela_h = self.orig_ela_h + self.rng.randn() * self.sigma_ela
        self.ela_h_per_year[year] = ela_h
        return ela_h

    # here we redefine an attribute all MassBalance model must provide,
    # as this is used for the communication during a dynamic run
    def get_annual_mb(self, heights, year=None, fl_id=None, fls=None):

        # Compute the mass-balance gradient
        ela_h = self.get_random_ela_h(year)
        mb = (np.asarray(heights) - ela_h) * self.grad

        # Convert to units of [m s-1] (meters of ice per second)
        return mb / SEC_IN_YEAR / cfg.PARAMS['ice_density']

Now that we have defined our MassBalanceModel class we can define a instance of it:

mb_model = RandomLinearMassBalance(gdir, 3000)

And explore some properties of our new Mass-Balance model:

mb_model.grad
3.0
mb_model.sigma_ela
200.0

Where are this values defined? Can you define a Mass Balance model with different values?

Click for the answer mb_model = RandomLinearMassBalance(gdir, grad=4, sigma_ela=150)

We also can ask for the Mass Balance profile for a specific year:

# first define the height range you are interested in
heights = np.arange(2000, 3500, 10)

# get the Mass Balance values for this heights for a specific year and plot them
mb_2000 = mb_model.get_annual_mb(heights, year=2000)
# OGGM internally works with SI units which are meters of ice per second,
# for interpretation we need to convert them into kg per year
mb_2000 = mb_2000 * SEC_IN_YEAR * cfg.PARAMS['ice_density']

# and finally we can plot it
plt.plot(mb_2000, heights)
plt.ylabel('Altitude (m)')
plt.xlabel('Mass Balance (kg yr-1)');
../_images/ca06ff6c888645ebaafb30e42c5f3742bbf35bd8146b5d9487e7efda9a923937.png
# or also can access other usful information which is available through the base class MassBalanceModel
mb_model.get_ela(year=2000)
3194.767637885181

Dynamic model run with user provided Mass Balance Model#

Now we connect our defined mass balance model with the geometry through the dynamic model.

The connection of individual parts is straightforward because we agreed on a way of communication by building our Mass-Balance Model upon the base class of OGGM.

# First we open our glacier geometry, which is already converted
# into the flowline representation 
fls = gdir.read_pickle('model_flowlines')

# We also need to define the year of our geomtery
y0 = gdir.rgi_date

# Then we import the dynamic solver we want to use
from oggm.core.flowline import SemiImplicitModel

# And bring the individual parts together
model = SemiImplicitModel(flowlines=fls, mb_model=mb_model, y0=y0)

# This model we now can use to conduct a dynamic glacier model run,
# here for example until 2100
ds = model.run_until_and_store(2300)

# And finally we can plot the results
f, (ax1, ax2, ax3) = plt.subplots(3,1,figsize=(10, 9))
ds.length_m.plot(ax=ax1)
ds.area_m2.plot(ax=ax2)
ds.volume_m3.plot(ax=ax3);
../_images/bfdf36f3345fdf4e9195687e163ec27e74dfc87ecc4ee4a11bfe459dfaa520f3.png