"""Mass balance models - next generation"""
# Built ins
import logging
import os
import inspect
from datetime import date, timedelta
import calendar
from functools import partial
# External libs
import cftime
import numpy as np
import xarray as xr
import pandas as pd
from numpy.typing import ArrayLike
from scipy.interpolate import interp1d
from scipy import optimize
# Locals
import oggm.cfg as cfg
from oggm.cfg import SEC_IN_YEAR, SEC_IN_DAY
from oggm.utils import (SuperclassMeta, get_geodetic_mb_dataframe,
floatyear_to_date, date_to_floatyear, get_demo_file,
float_years_timeseries, ncDataset, get_temp_bias_dataframe,
clip_min, clip_max, clip_array, clip_scalar,
weighted_average_1d, lazy_property, set_array_type, rmsd,
get_days_of_year, get_seconds_of_year, get_days_of_month,
get_seconds_of_month, )
from oggm.exceptions import (InvalidWorkflowError, InvalidParamsError,
MassBalanceCalibrationError)
from oggm import entity_task, __version__
# Module logger
log = logging.getLogger(__name__)
# Climate relevant global params - not optimised
MB_GLOBAL_PARAMS = ['temp_default_gradient',
'temp_all_solid',
'temp_all_liq',
'temp_melt']
[docs]
class MassBalanceModel(object, metaclass=SuperclassMeta):
"""Interface and common logic for all mass balance models used in OGGM.
All mass balance models should implement this interface.
Attributes
----------
valid_bounds : [float, float]
The altitudinal bounds where the MassBalanceModel is valid. This is
necessary for automated ELA search.
hemisphere : str, {'nh', 'sh'}
Used for certain methods - if the hydrological year is requested.
ice_density : float, default: ``cfg.PARAMS['ice_density']``
Density of ice
use_leap_years : bool, default: False
If the calendar should use leap years
"""
[docs]
def __init__(self, gdir=None, use_leap_years=False):
""" Initialize."""
self.valid_bounds = None
self.hemisphere = None
if gdir is None:
self.ice_density = cfg.PARAMS['ice_density']
else:
self.ice_density = gdir.settings['ice_density']
self.gdir = gdir
self.use_leap_years = use_leap_years
def __repr__(self):
"""String Representation of the mass balance model"""
summary = ['<oggm.MassBalanceModel>']
summary += [' Class: ' + self.__class__.__name__]
summary += [' Attributes:']
# Add all scalar attributes
for k, v in self.__dict__.items():
if np.isscalar(v) and not k.startswith('_'):
nbform = ' - {}: {}'
summary += [nbform.format(k, v)]
return '\n'.join(summary) + '\n'
def reset_state(self):
"""Reset any internal state of the model.
This might not be needed by most models, but some models have an
internal state (e.g. a snow cover history) which can be reset
this way.
"""
pass
def days_in_month(self, year):
"""Get the number of days of a month, with or without leap years,
depending on self.use_leap_years.
Parameters
----------
year: float
The year (in the "floating year" convention).
Returns
-------
int
The number of days of the month
"""
return get_days_of_month(year, use_leap_years=self.use_leap_years)
def sec_in_month(self, year):
"""Get the seconds of the month, with or without leap years, depending
on self.use_leap_years.
Parameters
----------
year: float
The year (in the "floating year" convention).
Returns
-------
int
The seconds of the month
"""
return get_seconds_of_month(year, use_leap_years=self.use_leap_years)
def days_in_year(self, year):
"""Get the number of days of a year, with or without leap years,
depending on self.use_leap_years.
Parameters
----------
year: float
The year (in the "floating year" convention).
Returns
-------
int
The number of days of the year
"""
return get_days_of_year(year, use_leap_years=self.use_leap_years)
def sec_in_year(self, year):
"""Get the seconds of the year, with or without leap years, depending
on self.use_leap_years.
Parameters
----------
year: float
The year (in the "floating year" convention).
Returns
-------
int
The seconds of the year
"""
return get_seconds_of_year(year, use_leap_years=self.use_leap_years)
[docs]
def get_daily_mb(self, heights, year=None, fl_id=None, fls=None):
"""Daily mass balance at given altitude(s) for a moment in time.
Units: [m s-1], or meters of ice per second
Note: `year` is optional because some simpler models have no time
component.
Parameters
----------
heights: ndarray
the altitudes at which the mass balance will be computed
year: float, optional
the time (in the "floating year" convention)
fl_id: float, optional
the index of the flowline in the fls array (might be ignored
by some MB models)
fls: list of flowline instances, optional
the flowlines array, in case the MB model implementation needs
to know details about the glacier geometry at the moment the
MB model is called
Returns
-------
the mass balance (same dim as `heights`) (units: [m s-1])
"""
raise NotImplementedError()
[docs]
def get_monthly_mb(self, heights, year=None, fl_id=None, fls=None):
"""Monthly mass balance at given altitude(s) for a moment in time.
Units: [m s-1], or meters of ice per second
Note: `year` is optional because some simpler models have no time
component.
Parameters
----------
heights: ndarray
the altitudes at which the mass balance will be computed
year: float, optional
the time (in the "floating year" convention)
fl_id: float, optional
the index of the flowline in the fls array (might be ignored
by some MB models)
fls: list of flowline instances, optional
the flowlines array, in case the MB model implementation needs
to know details about the glacier geometry at the moment the
MB model is called
Returns
-------
the mass balance (same dim as `heights`) (units: [m s-1])
"""
raise NotImplementedError()
[docs]
def get_annual_mb(self, heights, year=None, fl_id=None, fls=None):
"""Like `self.get_monthly_mb()`, but for annual MB.
For some simpler mass balance models ``get_monthly_mb()` and
`get_annual_mb()`` can be equivalent.
Units: [m s-1], or meters of ice per second
Note: `year` is optional because some simpler models have no time
component.
Parameters
----------
heights: ndarray
the altitudes at which the mass balance will be computed
year: float, optional
the time (in the "floating year" convention)
fl_id: float, optional
the index of the flowline in the fls array (might be ignored
by some MB models)
fls: list of flowline instances, optional
the flowlines array, in case the MB model implementation needs
to know details about the glacier geometry at the moment the
MB model is called
Returns
-------
the mass balance (same dim as `heights`) (units: [m s-1])
"""
raise NotImplementedError()
[docs]
def get_specific_mb(self, heights=None, widths=None, fls=None, year=None,
time_resolution='annual'):
"""Specific mass balance for a given glacier geometry.
Units depends on time_resolution:
- 'annual': [mm w.e. yr-1], or millimeter water equivalent per year.
- 'monthly': [mm w.e. month-1], or millimeter water equivalent per month.
- 'daily': [mm w.e. day-1], or millimeter water equivalent per day.
Parameters
----------
heights : array_like, default None
Altitudes at which the mass balance will be computed.
Overridden by ``fls`` if provided.
widths : array_like, default None
Widths of the flowline (necessary for the weighted average).
Overridden by ``fls`` if provided.
fls : list[oggm.Flowline], default None
List of flowline instances. Alternative to heights and
widths, and overrides them if provided.
year : array_like[float] or float, default None
Year, or a range of years in "floating year" convention.
time_resolution : str
The resolution of the provided "floating year". Options are
'annual', 'monthly' or 'daily'. Default is 'annual'.
Returns
-------
np.ndarray
Specific mass balance (units: mm w.e. yr-1).
"""
stack = []
year = np.atleast_1d(year)
if time_resolution == 'annual':
mb_function = self.get_annual_mb
# mm w.e. yr-1
unit_conversion = self.sec_in_year
elif time_resolution == 'monthly':
mb_function = self.get_monthly_mb
# mm w.e. month-1
unit_conversion = self.sec_in_month
elif time_resolution == 'daily':
mb_function = self.get_daily_mb
# mm w.e. day-1
def unit_conversion(x):
return SEC_IN_DAY
else:
raise ValueError(f"time_resolution {time_resolution} not supported. "
"Options are 'annual', 'monthly' or 'daily'.")
for mb_yr in year:
if fls is not None:
mbs = []
widths = []
for i, fl in enumerate(fls):
_widths = fl.widths
try:
# For rect and parabola don't compute spec mb
_widths = np.where(fl.thick > 0, _widths, 0)
except AttributeError:
pass
widths.append(_widths)
mbs.append(
mb_function(fl.surface_h, fls=fls, fl_id=i, year=mb_yr)
)
# 2x faster than np.append
widths = np.concatenate(widths, axis=0)
mbs = np.concatenate(mbs, axis=0)
mbs = weighted_average_1d(mbs, widths)
else:
mbs = mb_function(heights, year=mb_yr)
mbs = weighted_average_1d(mbs, widths)
mbs *= unit_conversion(mb_yr)
stack.append(mbs)
return set_array_type(stack) * self.ice_density
[docs]
def get_ela(self, year=None, **kwargs):
"""Get the equilibrium line altitude for a given year.
Parameters
----------
year : array_like[float] or float, default None
Year, or a range of years in "floating year" convention.
**kwargs
Any other keyword argument accepted by ``self.get_annual_mb``.
Returns
-------
float or np.ndarray:
The equilibrium line altitude (ELA) in m.
"""
stack = []
year = np.atleast_1d(year)
for mb_year in year:
if self.valid_bounds is None:
raise ValueError('attribute `valid_bounds` needs to be '
'set for the ELA computation.')
# Check for invalid ELAs
b0, b1 = self.valid_bounds
if (np.any(~np.isfinite(
self.get_annual_mb([b0, b1], year=mb_year, **kwargs))) or
(self.get_annual_mb([b0], year=mb_year, **kwargs)[0] > 0) or
(self.get_annual_mb([b1], year=mb_year, **kwargs)[0] < 0)):
stack.append(np.nan)
else:
year_length = self.sec_in_year(year=mb_year)
def to_minimize(x):
return (self.get_annual_mb([x], year=mb_year, **kwargs)[0] *
year_length * self.ice_density)
stack.append(optimize.brentq(to_minimize, *self.valid_bounds,
xtol=0.1))
return set_array_type(stack)
def is_year_valid(self, year):
"""Checks if a given date year be simulated by this model.
Parameters
----------
year : float, optional
the time (in the "floating year" convention)
Returns
-------
True if this year can be simulated by the model
"""
raise NotImplementedError()
[docs]
class ScalarMassBalance(MassBalanceModel):
"""Constant mass balance, everywhere."""
[docs]
def __init__(self, mb=0., use_leap_years=False):
""" Initialize.
Parameters
----------
mb : float
Fix the mass balance to a certain value (unit: [mm w.e. yr-1])
use_leap_years : bool
If the calendar should use leap years
"""
super(ScalarMassBalance, self).__init__(use_leap_years=use_leap_years)
self.hemisphere = 'nh'
self.valid_bounds = [-2e4, 2e4] # in m
self._mb = mb
def get_monthly_mb(self, heights, **kwargs):
mb = np.asarray(heights) * 0 + self._mb
return mb / SEC_IN_YEAR / self.ice_density
def get_annual_mb(self, heights, **kwargs):
mb = np.asarray(heights) * 0 + self._mb
return mb / SEC_IN_YEAR / self.ice_density
def is_year_valid(self, year):
return True
[docs]
class LinearMassBalance(MassBalanceModel):
"""Constant mass balance as a linear function of altitude.
Attributes
----------
ela_h: float
the equilibrium line altitude (units: [m])
grad: float
the mass balance gradient (unit: [mm w.e. yr-1 m-1])
max_mb: float
Cap the mass balance to a certain value (unit: [mm w.e. yr-1])
temp_bias
"""
[docs]
def __init__(self, ela_h, grad=3., max_mb=None, use_leap_years=False):
""" Initialize.
Parameters
----------
ela_h: float
Equilibrium line altitude (units: [m])
grad: float
Mass balance gradient (unit: [mm w.e. yr-1 m-1])
max_mb: float
Cap the mass balance to a certain value (unit: [mm w.e. yr-1])
use_leap_years : bool
If the calendar should use leap years
"""
super(LinearMassBalance, self).__init__(use_leap_years=use_leap_years)
self.hemisphere = 'nh'
self.valid_bounds = [-1e4, 2e4] # in m
self.orig_ela_h = ela_h
self.ela_h = ela_h
self.grad = grad
self.max_mb = max_mb
self._temp_bias = 0
@property
def temp_bias(self):
"""Change the ELA following a simple rule: + 1K -> ELA + 150 m
A "temperature bias" doesn't makes much sense in the linear MB
context, but we implemented a simple empirical rule:
+ 1K -> ELA + 150 m
"""
return self._temp_bias
@temp_bias.setter
def temp_bias(self, value):
self.ela_h = self.orig_ela_h + value * 150
self._temp_bias = value
def get_monthly_mb(self, heights, **kwargs):
mb = (np.asarray(heights) - self.ela_h) * self.grad
if self.max_mb is not None:
clip_max(mb, self.max_mb, out=mb)
return mb / SEC_IN_YEAR / self.ice_density
def get_annual_mb(self, heights, **kwargs):
return self.get_monthly_mb(heights, **kwargs)
def is_year_valid(self, year):
return True
[docs]
class MonthlyTIModel(MassBalanceModel):
"""Monthly temperature index model."""
[docs]
def __init__(
self,
gdir,
filename: str = 'climate_historical',
input_filesuffix: str = '',
settings_filesuffix: str = '',
fl_id: int = None,
melt_f: float = None,
temp_bias: float = None,
prcp_fac: float = None,
bias: float = 0.0,
temp_melt: float = None,
ys: int = None,
ye: int = None,
repeat: bool = False,
check_calib_params: bool = True,
check_climate_data: bool = True,
use_leap_years: bool = False,
):
"""Monthly temperature index model.
Parameters
----------
gdir : GlacierDirectory
The glacier directory.
filename : str, default 'climate_historical'
Set to a different BASENAME if you want to use alternative
climate data.
input_filesuffix : str, optional
Append a suffix to the climate input filename (useful for
GCM runs).
settings_filesuffix : str, optional
Append a suffix to the settings file (useful for sensitivity
runs).
fl_id : int, optional
If this flowline has been calibrated alone and has specific
model parameters.
melt_f : float, optional
The value of the melt factor you want to use, here the unit
is kg m-2 day-1 K-1. Defaults to the calibrated value.
temp_bias : float, optional
The value of the temperature bias. Defaults to the
calibrated value.
prcp_fac : float, optional
The value of the precipitation factor. Defaults to the
calibrated value.
bias : float, default 0.0
The value of the calibration bias [mm we yr-1]. Defaults to
the calibrated value. Note that this bias is *subtracted*
from the computed MB. Indeed:
BIAS = MODEL_MB - REFERENCE_MB.
temp_melt : float or None, default None
The threshold for the air temperature above which ice melt is
assumed to occur (-1°C the default for monthly mb models). If None
settings['temp_melt'] is used.
ys : int, optional
The start of the climate period where the MB model is valid.
Defaults to the period with available data.
ye : int, optional
The end of the climate period where the MB model is valid.
Defaults to the period with available data.
repeat : bool, default False
Whether the climate period given by [ys, ye] should be repeated
indefinitely in a circular way
check_calib_params : bool, default True
OGGM will try hard not to use wrongly calibrated parameters
by checking the global parameters used during calibration
and the ones you are using at run time. If they don't
match, it will raise an error. Set to ``False`` to suppress
this check.
check_climate_data : bool, default True
If True the climate input data is checked if it is provided in total
years and that the length matches.
use_leap_years : bool, default False
If the calendar should use leap years
"""
self.settings_filesuffix = settings_filesuffix
gdir.settings_filesuffix = settings_filesuffix
super(MonthlyTIModel, self).__init__(gdir=gdir,
use_leap_years=use_leap_years)
self.valid_bounds = [-1e4, 2e4] # in m
self.fl_id = fl_id # which flowline are we the model of?
self.filename = filename
self.input_filesuffix = input_filesuffix
if melt_f is None: # This prevents class methods
melt_f = self.calib_params['melt_f']
if temp_bias is None:
temp_bias = self.calib_params['temp_bias']
if prcp_fac is None:
prcp_fac = self.calib_params['prcp_fac']
# Check the climate related params to the GlacierDir to make sure
if check_calib_params:
mb_calib = self.calib_params['mb_global_params']
for k, v in mb_calib.items():
if v != self.gdir.settings[k]:
msg = ('You seem to use different mass balance parameters '
'than used for the calibration: '
f"you use gdir.settings['{k}']={gdir.settings[k]} while "
f"it was calibrated with gdir.settings['{k}']={v}. "
'Set `check_calib_params=False` to ignore this '
'warning.')
raise InvalidWorkflowError(msg)
src = self.calib_params['baseline_climate_source']
src_calib = gdir.get_climate_info(
filename=self.filename, input_filesuffix=self.input_filesuffix
)['baseline_climate_source']
if src != src_calib:
msg = (f'You seem to have calibrated with the {src} '
f"climate data while this gdir was calibrated with "
f"{src_calib}. Set `check_calib_params=False` to "
f"ignore this warning.")
raise InvalidWorkflowError(msg)
self.melt_f = melt_f
self.bias = bias
# Global parameters
self.temp_all_solid = gdir.settings['temp_all_solid']
self.temp_all_liq = gdir.settings['temp_all_liq']
if temp_melt is None:
self.temp_melt = gdir.settings['temp_melt']
else:
gdir.settings['temp_melt'] = temp_melt
self.temp_melt = temp_melt
# check if valid prcp_fac is used
if prcp_fac <= 0:
raise InvalidParamsError('prcp_fac has to be above zero!')
self.temp_default_gradient = gdir.settings['temp_default_gradient']
# Public attrs
self.hemisphere = gdir.hemisphere
self.repeat = repeat
# Private attrs
# to allow prcp_fac to be changed after instantiation
# prescribe the prcp_fac as it is instantiated
self._prcp_fac = prcp_fac
# same for temp bias
self._temp_bias = temp_bias
# Read climate file
fpath = gdir.get_filepath(filename, filesuffix=input_filesuffix)
with ncDataset(fpath, mode='r') as nc:
time = nc.variables["time"]
time = cftime.num2date(time[:], time.units, calendar=time.calendar)
# only use defined years
years = np.array(list(map(lambda x: x.year, time)))
pok = slice(None) # take all is default (optim)
if ys is not None:
pok = years >= ys
if ye is not None:
try:
pok = pok & (years <= ye)
except TypeError:
pok = years <= ye
self.years = years[pok]
self.months = np.array(list(map(lambda x: x.month, time)))[pok]
self.days = np.array(list(map(lambda x: x.day, time)))[pok]
if check_climate_data:
# check for full years, this is overwritten for daily
self._check_for_full_years()
# Read timeseries and correct it
self.temp = nc.variables["temp"][pok].astype(np.float64) + self._temp_bias
self.prcp = nc.variables["prcp"][pok].astype(np.float64) * self._prcp_fac
grad = self.prcp * 0 + self.temp_default_gradient
self.grad = grad
self.ref_hgt = nc.ref_hgt
self.climate_source = nc.climate_source
self.ys = self.years[0]
self.ye = self.years[-1]
self.ys_float = date_to_floatyear(self.years[0], self.months[0],
self.days[0])
self.ye_float = date_to_floatyear(self.years[-1], self.months[-1],
self.days[-1])
def __repr__(self):
"""String Representation of the mass balance model"""
summary = ['<oggm.MassBalanceModel>']
summary += [' Class: ' + self.__class__.__name__]
summary += [' Attributes:']
# Add all scalar attributes
done = []
for k in ['hemisphere', 'climate_source', 'melt_f', 'prcp_fac', 'temp_bias', 'bias']:
done.append(k)
v = self.__getattribute__(k)
if k == 'climate_source':
if v.endswith('.nc'):
v = os.path.basename(v)
nofloat = ['hemisphere', 'climate_source']
nbform = ' - {}: {}' if k in nofloat else ' - {}: {:.02f}'
summary += [nbform.format(k, v)]
for k, v in self.__dict__.items():
if np.isscalar(v) and not k.startswith('_') and k not in done:
nbform = ' - {}: {}'
summary += [nbform.format(k, v)]
return '\n'.join(summary) + '\n'
# adds the possibility of changing prcp_fac
# after instantiation with properly changing the prcp time series
@property
def prcp_fac(self):
"""Precipitation factor (default: gdir.settings['prcp_fac'])
Called factor to make clear that it is a multiplicative factor in
contrast to the additive temperature bias
"""
return self._prcp_fac
@prcp_fac.setter
def prcp_fac(self, new_prcp_fac):
# just to check that no invalid prcp_factors are used
if np.any(np.asarray(new_prcp_fac) <= 0):
raise InvalidParamsError('prcp_fac has to be above zero!')
if len(np.atleast_1d(new_prcp_fac)) == 12:
# OK so that's monthly stuff
new_prcp_fac = np.tile(new_prcp_fac, len(self.prcp) // 12)
self.prcp *= new_prcp_fac / self._prcp_fac
self._prcp_fac = new_prcp_fac
@property
def temp_bias(self):
"""Add a temperature bias to the time series"""
return self._temp_bias
@temp_bias.setter
def temp_bias(self, new_temp_bias):
if len(np.atleast_1d(new_temp_bias)) == 12:
# OK so that's monthly stuff
new_temp_bias = np.tile(new_temp_bias, len(self.temp) // 12)
self.temp += new_temp_bias - self._temp_bias
self._temp_bias = new_temp_bias
@lazy_property
def calib_params(self):
if self.fl_id is None:
return self.gdir.settings
else:
fp_fl_settings = self.gdir.get_filepath('settings',
filesuffix=f'_fl{self.fl_id}')
if os.path.exists(fp_fl_settings):
self.gdir.settings_filesuffix = f'_fl{self.fl_id}'
out = self.gdir.settings
if self.settings_filesuffix:
raise InvalidWorkflowError('settings_filesuffix cannot be '
'used with multiple flowlines')
return out
else:
return self.gdir.settings
def _check_for_full_years(self):
# We check for full calendar years
if self.years[0] != self.years[-1]:
nr_of_months = (self.years[-1] - self.years[0] + 1) * 12
else:
nr_of_months = 12
len_data_ok = len(self.years) == nr_of_months
months_ok = (self.months[0] == 1) or (self.months[-1] == 12)
if not months_ok or not len_data_ok:
raise InvalidWorkflowError(
"We now work exclusively with full calendar years. Check "
"provided climate data! \n Your current selection: "
f"{self.months[0]:02d}.{self.years[0]:04d} - "
f"{self.months[-1]:02d}.{self.years[-1]:04d}\n"
f"Your data has {len(self.years)} timestamps, but we expect "
f"{nr_of_months}."
)
def is_year_valid(self, year: int) -> bool:
"""Check if a year is within the climate period.
Returns
-------
bool
True if the year is within the climate period.
"""
return self.ys_float <= year <= self.ye_float
def validate_year(self, year: int) -> int:
"""Get and validate if a year is outside the data's time range.
Raises
------
ValueError
If the year is outside of the data's climate period.
"""
if self.repeat:
year = self.ys + (year - self.ys) % (self.ye - self.ys + 1)
if not self.is_year_valid(year): # this is overloaded by subclasses
raise ValueError(
f'year {year} out of the valid time bounds: '
f'[{self.ys_float}, {self.ye_float}]'
)
return year
def _get_tempformelt(self, temp):
tempformelt = temp - self.temp_melt
clip_min(tempformelt, 0, out=tempformelt)
return tempformelt
def _get_prcpsol(self, prcp, temp):
fac = 1 - (temp - self.temp_all_solid) / (self.temp_all_liq - self.temp_all_solid)
return prcp * clip_array(fac, 0, 1)
def _get_climate_for_index(self, heights: ArrayLike, pok: ArrayLike
) -> tuple:
"""Returns climate information at provided heights and time indexes.
If only one time index is provided also the climate information is
returned in 1D, otherwise in 2D.
Parameters
----------
heights : array_like
the heights of interest in meters
pok : np.ndarray
the time indexes of interest
Returns
-------
tuple[np.ndarray]
Temperature, melt temperatures, total precipitation, and
solid precipitation for each height pixel.
"""
# Read already (temperature bias and precipitation factor corrected!)
itemp = self.temp[pok]
iprcp = self.prcp[pok]
igrad = self.grad[pok]
# For each height pixel:
heights = np.asarray(heights) # sometimes heights are passed as lists
npix = len(heights)
if np.size(pok) == 1:
# fast path for 1D data (one time index)
# Compute temp and tempformelt (temperature above melting threshold)
temp = itemp + igrad * (heights - self.ref_hgt)
tempformelt = self._get_tempformelt(temp)
# Compute solid precipitation from total precipitation
prcp = np.ones(npix) * iprcp
prcpsol = self._get_prcpsol(prcp, temp)
return temp, tempformelt, prcp, prcpsol
# otherwise we need to handle 2D data
# Compute temp and tempformelt (temperature above melting threshold)
grad_temp = np.atleast_2d(igrad).repeat(npix, 0)
grad_temp *= (heights.repeat(len(pok)).reshape(grad_temp.shape) -
self.ref_hgt)
temp2d = np.atleast_2d(itemp).repeat(npix, 0) + grad_temp
temp2dformelt = self._get_tempformelt(temp2d)
# Compute solid precipitation from total precipitation
prcp = np.atleast_2d(iprcp).repeat(npix, 0)
prcpsol = self._get_prcpsol(prcp, temp2d)
return temp2d, temp2dformelt, prcp, prcpsol
def get_monthly_climate(
self, heights: np.ndarray, year: float = None
) -> tuple:
"""Monthly climate information at given heights.
Note that prcp is corrected with the precipitation factor and that
all other model biases (temp and prcp) are applied.
Parameters
----------
heights : np.ndarray[np.float64]
Heights in m.
year : float, optional
The year (in the "floating year" convention). Default None.
Returns
-------
tuple[np.ndarray]
Temperatures, melt temperatures, total precipitation, and
solid precipitation.
"""
y, m = floatyear_to_date(year)
y = self.validate_year(year=y)
pok = np.where((self.years == y) & (self.months == m))[0][0]
t, tmelt, prcp, prcpsol = self._get_climate_for_index(
heights=heights, pok=pok,)
# get tmelt for the entire month
tmelt *= self.days_in_month(year=year)
return t, tmelt, prcp, prcpsol
def _get_2d_annual_climate(self, heights, year=None):
# Avoid code duplication with a getter routine
year = np.floor(year)
year = self.validate_year(year=year)
pok = np.where(self.years == year)[0]
if len(pok) < 1:
raise ValueError('Year {} not in record'.format(int(year)))
t, tmelt, prcp, prcpsol = self._get_climate_for_index(
heights=heights, pok=pok)
return t, tmelt, prcp, prcpsol
def get_annual_climate(self, heights, year=None):
"""Annual climate information at given heights.
Note that prcp is corrected with the precipitation factor and that
all other model biases (temp and prcp) are applied.
Returns
-------
tuple
Mean temperature, and sums of melt temperature,
precipitation, and solid precipitation.
"""
t, tmelt, prcp, prcpsol = self._get_2d_annual_climate(
heights=heights, year=year)
myr = date_to_floatyear(np.repeat(int(np.floor(year)), 12),
np.arange(1, 13))
days_of_month = [self.days_in_month(year=yr) for yr in myr]
# get tmelt for the entire month
tmelt *= days_of_month
return (t.mean(axis=1), tmelt.sum(axis=1),
prcp.sum(axis=1), prcpsol.sum(axis=1))
def get_monthly_mb(self,
heights: np.ndarray,
year: float = None,
add_climate: bool = False,
**kwargs,
) -> np.float64 or tuple:
"""Get monthly mass balance.
Parameters
----------
heights : array_like
Heights in m.
year : float, optional
The year (in the "floating year" convention). Default None.
add_climate : bool, default False
Additionally returns mean temperature and the sums of melt
temperature, total precipitation, and solid precipitation.
Avoids recalculating climatology later in some workflows,
e.g. ``run_with_hydro``.
**kwargs
Extra arguments passed to subclasses of this method.
Returns
-------
np.ndarray[np.float64] or tuple[np.ndarray]
Monthly mass balance in metres of ice per second. If
``add_climate`` is True, also returns mean temperature and
the sums of melt temperature, total precipitation, and
solid precipitation.
"""
t, tmelt, prcp, prcpsol = self.get_monthly_climate(heights, year=year)
# length of the month in days already considered in tmelt in get_monthly_climate
mb_month = prcpsol - self.melt_f * tmelt
sec_in_month = self.sec_in_month(year=year)
mb_month -= (self.bias * sec_in_month / self.sec_in_year(year=year))
if add_climate:
return mb_month / sec_in_month / self.ice_density, t, tmelt, prcp, prcpsol
return mb_month / sec_in_month / self.ice_density
def get_annual_mb(self, heights, year=None, add_climate=False, **kwargs):
"""Get annual mass balance.
Parameters
----------
heights : array_like
Heights in m.
year : float, optional
The year (in the "floating year" convention). Default None.
add_climate : bool, default False
Additionally returns mean temperature and the sums of melt
temperature, total precipitation, and solid precipitation.
Avoids recalculating climatology later in some workflows,
e.g. ``run_with_hydro``.
**kwargs
Extra arguments passed to subclasses of this method.
Returns
-------
np.ndarray[np.float64] or tuple[np.ndarray]
Annual mass balance in metres of ice per second. If
``add_climate`` is True, also returns mean temperature and
the sums of melt temperature, total precipitation, and
solid precipitation.
"""
t, tmelt, prcp, prcpsol = self._get_2d_annual_climate(heights, year)
myr = date_to_floatyear(np.repeat(int(np.floor(year)), 12),
np.arange(1, 13))
days_of_month = [self.days_in_month(year=yr) for yr in myr]
# get tmelt for the entire month
tmelt *= days_of_month
mb_annual = np.sum(prcpsol - self.melt_f * tmelt, axis=1)
mb_annual = ((mb_annual - self.bias) / self.sec_in_year(year=year) /
self.ice_density)
if add_climate:
return (mb_annual, t.mean(axis=1), tmelt.sum(axis=1),
prcp.sum(axis=1), prcpsol.sum(axis=1))
return mb_annual
[docs]
class DailyTIModel(MonthlyTIModel):
"""Daily temperature index model."""
[docs]
def __init__(
self,
gdir,
filename: str = 'climate_historical',
input_filesuffix: str = '_daily',
settings_filesuffix: str = '',
fl_id: int = None,
melt_f: float = None,
temp_bias: float = None,
prcp_fac: float = None,
bias: float = 0.0,
temp_melt: float = 0.0,
ys: int = None,
ye: int = None,
repeat: bool = False,
check_calib_params: bool = True,
check_climate_data: bool = True,
use_leap_years: bool = True,
):
"""Inherits from MonthlyTIModel.
Parameters
----------
gdir : GlacierDirectory
The glacier directory.
filename : str, default 'climate_historical'
Set to a different BASENAME if you want to use alternative
climate data.
input_filesuffix : str, default '_daily'
Append a suffix to the climate input filename (useful for
GCM runs).
settings_filesuffix : str, optional
Append a suffix to the settings file (useful for sensitivity
runs).
fl_id : int, optional
If this flowline has been calibrated alone and has specific
model parameters.
melt_f : float, optional
The value of the melt factor you want to use, here the unit
is kg m-2 day-1 K-1. Defaults to the calibrated value.
temp_bias : float, optional
The value of the temperature bias. Defaults to the
calibrated value.
prcp_fac : float, optional
The value of the precipitation factor. Defaults to the
calibrated value.
bias : float, default 0.0
The value of the calibration bias [mm we yr-1]. Defaults to
the calibrated value. Note that this bias is *subtracted*
from the computed MB. Indeed:
BIAS = MODEL_MB - REFERENCE_MB.
temp_melt : float, default 0.0
The threshold for the air temperature above which ice melt is
assumed to occur (0°C the default for daily mb models).
ys : int, optional
The end of the climate period where the MB model is valid.
Defaults to the period with available data.
ye : int, optional
The end of the climate period where the MB model is valid.
Defaults to the period with available data.
repeat : bool, default False
Whether the climate period given by [ys, ye] should be
repeated indefinitely in a circular way.
check_calib_params : bool, default True
OGGM will try hard not to use wrongly calibrated parameters
by checking the global parameters used during calibration
and the ones you are using at run time. If they don't
match, it will raise an error. Set to ``False`` to suppress
this check.
check_climate_data : bool, default True
If True, check the climate input data is provided in total
years and that the length matches.
use_leap_years : bool, default True
If the calendar should use leap years.
"""
self.settings_filesuffix = settings_filesuffix
gdir.settings_filesuffix = settings_filesuffix
super(DailyTIModel, self).__init__(
gdir=gdir,
filename=filename,
input_filesuffix=input_filesuffix,
settings_filesuffix=settings_filesuffix,
fl_id=fl_id,
melt_f=melt_f,
temp_bias=temp_bias,
prcp_fac=prcp_fac,
bias=bias,
temp_melt=temp_melt,
ys=ys,
ye=ye,
repeat=repeat,
check_calib_params=check_calib_params,
check_climate_data=check_climate_data,
use_leap_years=use_leap_years,
)
def _check_for_full_years(self):
# We check for full calendar years
nr_of_days = 0
for yr in np.arange(self.years[0], self.years[-1] + 1):
nr_of_days += self.days_in_year(yr)
len_data_ok = len(self.years) == nr_of_days
months_ok = (self.months[0] == 1) and (self.months[-1] == 12)
days_ok = (self.days[0] == 1) and (self.days[-1] == 31)
if not months_ok or not days_ok or not len_data_ok:
raise InvalidWorkflowError(
"We now work exclusively with full calendar years (01.01. - "
"31.12.). Check provided climate data!\nYour current selection: "
f"{self.days[0]:02d}.{self.months[0]:02d}.{self.years[0]:04d} - "
f"{self.days[-1]:02d}.{self.months[-1]:02d}.{self.years[-1]:04d}"
f"\nYour data has {len(self.years)} timestamps, but we expect "
f"{nr_of_days}."
)
def get_annual_climate(self, heights, year=None):
t, tmelt, prcp, prcpsol = self._get_2d_annual_climate(heights, year)
return (t.mean(axis=1), tmelt.sum(axis=1),
prcp.sum(axis=1), prcpsol.sum(axis=1))
def _get_2d_monthly_climate(
self, heights: np.ndarray, year: float = None
) -> tuple:
y, m = floatyear_to_date(year)
y = self.validate_year(year=y)
pok = np.where((self.years == y) & (self.months == m))[0]
t, tmelt, prcp, prcpsol = self._get_climate_for_index(
heights=heights, pok=pok)
return t, tmelt, prcp, prcpsol
def get_monthly_climate(
self, heights: np.ndarray, year: float = None
) -> tuple:
"""Monthly climate information at given heights.
Note that prcp is corrected with the precipitation factor and that
all other model biases (temp and prcp) are applied.
Parameters
----------
heights : np.ndarray[np.float64]
Heights in m.
year : float, optional
The year (in the "floating year" convention). Default None.
Returns
-------
tuple[np.ndarray]
Temperatures, melt temperatures, total precipitation, and
solid precipitation.
"""
t, tmelt, prcp, prcpsol = self._get_2d_monthly_climate(heights, year)
return (t.mean(axis=1), tmelt.sum(axis=1),
prcp.sum(axis=1), prcpsol.sum(axis=1))
def get_daily_climate(self,
heights: np.ndarray,
year: float = None,
) -> tuple:
"""Daily climate information at given heights.
Note that prcp is corrected with the precipitation factor and that
all other model biases (temp and prcp) are applied.
Parameters
----------
heights : np.ndarray[np.float64]
Heights in m.
year : float, optional
The year (in the "floating year" convention). Default None.
Returns
-------
tuple[np.ndarray]
Temperatures, melt temperatures, total precipitation, and
solid precipitation.
"""
y, m, d = floatyear_to_date(year, return_day=True)
y = self.validate_year(year=y)
pok = np.where((self.years == y) &
(self.months == m) &
(self.days == d))[0][0]
t, tmelt, prcp, prcpsol = self._get_climate_for_index(
heights=heights, pok=pok, )
return t, tmelt, prcp, prcpsol
def get_daily_mb(self,
heights: np.ndarray,
year: int = None,
add_climate: bool = False,
**kwargs,
) -> np.float64 or tuple:
"""Get daily mass balance.
Accounts for leap years by default.
Parameters
----------
heights : array_like
Heights in m.
year : int, optional
The year (in the "floating year" convention). Default None.
add_climate : bool, default False
Additionally returns mean temperature and the sums of melt
temperature, total precipitation, and solid precipitation.
Avoids recalculating climatology later in some workflows,
e.g. ``run_with_hydro``.
**kwargs
Extra arguments passed to subclasses of this method.
Returns
-------
np.ndarray[np.float64] or tuple[np.ndarray]
Daily mass balance in metres of ice per second. If
``add_climate`` is True, also returns mean temperature and
the sums of melt temperature, total precipitation, and
solid precipitation.
"""
t, tmelt, prcp, prcpsol = self.get_daily_climate(heights, year=year)
mb_daily = prcpsol - self.melt_f * tmelt
mb_daily -= (self.bias * SEC_IN_DAY / self.sec_in_year(year=year))
if add_climate:
return (mb_daily / SEC_IN_DAY / self.ice_density,
t, tmelt, prcp, prcpsol)
return mb_daily / SEC_IN_DAY / self.ice_density
def get_monthly_mb(self,
heights: np.ndarray,
year: float = None,
add_climate: bool = False,
**kwargs,
) -> np.float64 or tuple:
"""Get monthly mass balance.
Parameters
----------
heights : np.ndarray
Heights in m.
year : float, optional
The year (in the "floating year" convention). Default None.
add_climate : bool, default False
Additionally returns mean temperature and the sums of melt
temperature, total precipitation, and solid precipitation.
Avoids recalculating climatology later in some workflows,
e.g. ``run_with_hydro``.
**kwargs
Extra arguments passed to subclass implementations of this
method.
Returns
-------
np.float64 or tuple[np.ndarray]
Monthly mass balance in metres of ice per second. If
``add_climate`` is True, also returns mean temperature and
the sums of melt temperature, total precipitation, and
solid precipitation.
"""
t, tmelt, prcp, prcpsol = self._get_2d_monthly_climate(heights, year)
mb_month = np.sum(prcpsol - self.melt_f * tmelt,
axis=1)
sec_in_month = self.sec_in_month(year=year)
mb_month -= (self.bias * sec_in_month / self.sec_in_year(year=year))
if add_climate:
return (mb_month / sec_in_month / self.ice_density, t.mean(axis=1),
tmelt.sum(axis=1), prcp.sum(axis=1), prcpsol.sum(axis=1))
return mb_month / sec_in_month / self.ice_density
def get_annual_mb(self,
heights: np.ndarray,
year: float = None,
add_climate: bool = False,
**kwargs,
) -> np.float64 or tuple:
"""Get annual mass balance.
This is equivalent to taking the sum of ``get_daily_mb``.
Parameters
----------
heights : array_like
Heights in m.
year : float, optional
The year (in the "floating year" convention). Default None.
add_climate : bool, default False
Additionally returns mean temperature and the sums of melt
temperature, total precipitation, and solid precipitation.
Avoids recalculating climatology later in some workflows,
e.g. ``run_with_hydro``.
**kwargs
Extra arguments passed to ``get_2d_temperature``.
Returns
-------
np.ndarray[np.float64] or tuple[np.ndarray]
Annual mass balance in metres of ice per second. If
``add_climate`` is True, also returns mean temperature and
the sums of melt temperature, total precipitation, and
solid precipitation.
"""
t, tmelt, prcp, prcpsol = self._get_2d_annual_climate(heights, year)
mb_annual = np.sum(prcpsol - self.melt_f * tmelt,
axis=1)
mb_annual = ((mb_annual - self.bias) / self.sec_in_year(year=year) /
self.ice_density)
if add_climate:
return (mb_annual, t.mean(axis=1), tmelt.sum(axis=1),
prcp.sum(axis=1), prcpsol.sum(axis=1))
return mb_annual
[docs]
class SfcTypeTIModel(MassBalanceModel):
"""Temperature-Index model with surface type distinction using a bucket
system.
Adapted from OGGM/massbalance-sandbox and explained in Schuster et al. 2023.
"""
[docs]
def __init__(
self,
gdir,
settings_filesuffix: str = "",
use_leap_years: bool = True,
mb_model_class=MonthlyTIModel,
climate_resolution: str = "monthly",
aging_frequency: str = "monthly",
melt_f_ratio: float = 0.5,
melt_f_change: str = "neg_exp",
tau_e: float = 1.0,
ys: int = None,
spinup_years: int = 6,
save_spinup_mbs: bool = True,
spinup_buckets: np.ndarray = None,
fl = None,
use_main_fl_from: str = 'inversion_flowlines',
hbins: ArrayLike = None,
store_buckets: bool or str = False,
store_buckets_dates: ArrayLike = None,
use_previous_mbs: bool = False,
store_snowline: bool = False,
store_snowline_start_month: str = 'Oct',
snow_density: float = 300,
density_change: str = "neg_exp",
**kwargs,
):
"""Surface type temperature index model.
Parameters
----------
gdir : GlacierDirectory
The glacier directory.
settings_filesuffix : str, optional
append a suffix to the settings file (useful for
sensitivity runs).
use_leap_years : bool, default True
If the calendar should use leap years.
mb_model_class : MassBalanceModel, default ``DailyTIModel``
The MassBalanceModel to use together with surface type tracking.
Options are DailyTIModel and MonthlyTIModel.
climate_resolution : str, default 'annual'
Temporal mass balance resolution. Options are 'annaul', 'monthly'
and 'daily', but the mb_model_class need to be able to provide this
resolution.
aging_frequency : str, default "annual"
Frequency at which the buckets are moved to the next one (= getting
older). Options are "annual" or "monthly".
If annual, the model uses one snow and (spinup_years-1) firn buckets.
If monthly, the snow ages over the number of months * spinup_years.
melt_f_ratio : float, default 0.5
Ratio of snow melt factor to ice melt factor.
Between 0 and 1, where 1 is no surface type distinction.
Default 0.5 to match GloGEM.
melt_f_change : str, default "neg_exp"
How the snow melt factor changes relative to the ice melt
factor, either "linear" or "neg_exp" (see `tau_e` for the equation).
tau_e : float, default 1
Only used if melt_f_change is 'neg_exp'. It describes how fast the
snow melt factor approximates to the ice melt factor via
melt_f=melt_f_ice+(melt_f_snow-melt_f_ice)*np.exp(-time_yr/tau_e_fold_yr)
Must be larger than zero to prevent ``melt_f`` being set to NaN in
the first bucket.
ys : int, default None
The initial year from where we want to get mb values. This means the
bucket system is spun up so that at ys for the first time ice
can form. For spinup the years ys - spinup_years up to ys are used.
If None (the default), ys is not fixed at initialisation. Instead
it is set the first time get_annual_mb, get_monthly_mb or
get_daily_mb is called, using the first requested year (rounded
down to the next integer year via np.floor), and the buckets are
initialised at that point. Every call to reset_state resets ys
back to None again, so it is re-derived on the next call. If ys
is explicitly provided here, it is fixed and unaffected by
reset_state.
spinup_years : int, default 6
Number of spinup years. This defines the number of buckets we use
(see aging_frequency for explanation). The minimum allowed value is
1 (corresponds to only having a snow bucket for
aging_frequency='annual'). The spinup ensures every bucket has the
opportunity to be filled and ice can form at ys.
save_spinup_mbs : bool, default False
If we want to safe the climatic mb and ice mb during the spinup
period. This is mainly useful for testing.
spinup_buckets : np.ndarray, default None
Instead of conducting a spinup of the buckets you can provide the
initial buckets. It is assumed that these buckets are valid at the
time ys.
fl: Flowline, default None
The flowline for defining the grid-points. If None, the flowline
defined in `use_main_fl_from` is used. During all consecutive calls
it is always checked the length of provided heights equals the
length of the grid-points of the flowline.
use_main_fl_from: str, default 'inversion_flowlines'
If the main flowline of 'inversion_flowlines' or 'model_flowlines'
should be used. If fl is provided this is ignored.
hbins: ArrayLike, default None
Height bins for classifying surface types. Only needed for
``ConstantMBModel``. Currently untested!
store_buckets: bool or str, default False
If buckets should be stored along the way. Options are 'annual',
'monthly', 'daily' or False.
store_buckets_dates: ArrayLike, default []
In addition to the frequency of when the buckets should be stored
defined in `store_buckets` you can also provide a few selected dates
where the buckets should be stored.
use_previous_mbs: bool, default False
If True, and you call get_annual_mb, get_monthly_mb or get_daily_mb
it is checked if the desired year was already computed and return
this values. In this case the provided heights will be ignored. This
can be needed for some tasks (e.g. run_with_hydro), but the user
should be aware that this behaviour is different than other
mb_models work, because with surface tracking we have included a
memory of the past. If False and you try to revisit a past year and
error is raised.
store_snowline: bool, default False
Experimental: save the snowline in the climate_resolution. For this
the lowest elevation where the bucket is not zero is set as the
snowline. Older buckets are considered starting from
store_snowline_start_month.
store_snowline_start_month: str, default 'Oct'
Experimental: the start month of buckets used for the derivation of
the snowline. E.g. with the default 'Oct', if you want to derive the
snowline at Mar, all buckets starting from the previous Oct are
considered.
snow_density: float, default 300
The assumed density of fresh snow.
density_change: str, default "neg_exp"
How the density from snow to firn to ice changes over time. The
options "linear" or "neg_exp" (see `tau_e` for the equation) just
mimik the implementations for melt_f (assuming a one to one relation
between melt_f and density). This is currently experimental and
needs further research.
**kwargs:
keyword arguments to pass to the mb_model_class
"""
self.settings_filesuffix = settings_filesuffix
gdir.settings_filesuffix = settings_filesuffix
super(SfcTypeTIModel, self).__init__(gdir=gdir,
use_leap_years=use_leap_years)
# the mass balance model we use to get the climate data, as well as
# melt_f, prcp_fac and temp_bias
if mb_model_class not in [MonthlyTIModel, DailyTIModel]:
raise NotImplementedError(f"mb_model_class: {mb_model_class}")
self.mb_model_class = mb_model_class.__name__
self.mbmod = mb_model_class(gdir=gdir,
settings_filesuffix=settings_filesuffix,
**kwargs)
self.filename = self.mbmod.filename
self.input_filesuffix = self.mbmod.input_filesuffix
self.hemisphere = self.mbmod.hemisphere
self.bias = self.mbmod.bias
self.ye = self.mbmod.ye
# if ys is not provided we leave self.ys as None and set it lazily to
# the first year requested via get_annual_mb, get_monthly_mb or
# get_daily_mb (see _ensure_buckets_initialized). reset_state also
# resets self.ys back to None in that case, so it is again derived
# from the next requested year.
self._ys_is_dynamic = ys is None
self.ys = ys
# check compatibility of aging_frequency and climate_resolution: aging
# can not happen at higher temporal resolution than the climate steps
if aging_frequency not in ["annual", "monthly"]:
raise NotImplementedError(f"aging_frequency. {aging_frequency}")
climate_and_aging_compatible = True
if climate_resolution == 'annual':
if aging_frequency != 'annual':
climate_and_aging_compatible = False
elif climate_resolution in ['monthly', 'daily']:
# currently the fastest aging supported is monthly
pass
else:
raise NotImplementedError(
f"climate_resolution: {climate_resolution}")
if not climate_and_aging_compatible:
raise InvalidParamsError(
f"The selected climate_resolution '{climate_resolution}' is "
f"not compatible with an aging_frequency '{aging_frequency}'. "
"The climate resolution must be the same or shorter than "
"aging (e.g. you can not age monthly while the climate is "
"only applied annual).")
self.aging_frequency = aging_frequency
self.climate_resolution = climate_resolution
# stuff related to spinup, only used if spinup_buckets is None
if spinup_years < 1:
raise InvalidParamsError("spinup_years needed to be at least 1, you "
f"provided {spinup_years}.")
self.spinup_years = spinup_years
self.save_spinup_mbs = save_spinup_mbs
self.spinup_buckets = spinup_buckets
if isinstance(self.spinup_buckets, pd.core.frame.DataFrame):
self.spinup_buckets = self.spinup_buckets.values
if self.ys is not None:
self._validate_spinup_climate_data()
# defining the number of grid points and the spinup heights, either with
# fl or hbins
self.hbins = hbins
# resolve the flowline into grid labels and spinup heights; the fl
# object itself is not stored — only the derived arrays are needed
if fl is not None:
_fl = fl
else:
if self.hbins is not None:
_fl = None
elif use_main_fl_from == 'inversion_flowlines':
_fl = self.gdir.read_pickle("inversion_flowlines")[-1]
elif use_main_fl_from == 'model_flowlines':
_fl = self.gdir.read_pickle("model_flowlines")[-1]
else:
raise InvalidParamsError("We need a flowline or height bins "
"(hbins) for defining the number of "
"buckets we want to compute!")
# create labels for the grid_points and define heights used during spinup
if _fl is not None:
# for the flowline we use the distance along the flowline
self.buckets_grid_point_label = _fl.dx_meter * np.arange(_fl.nx)
self.spinup_heights = _fl.surface_h
else:
# for hbins we just use numbers for the labels
self.buckets_grid_point_label = np.arange(len(self.hbins))
self.spinup_heights = self.hbins
# here we set the names of the buckets, this depends on the
# aging_frequency and spinup_years
if self.aging_frequency == "annual":
firn_buckets = [f"firn_{i}" for i in range(1, self.spinup_years, 1)]
elif self.aging_frequency == "monthly":
firn_buckets = [f"firn_{i}" for i in
range(1, 12 * self.spinup_years, 1)]
else:
raise InvalidParamsError("aging_frequency must be 'annual' or "
"'monthly'")
# we also add a snow and an ice bucket
self.buckets = ["snow"] + firn_buckets + ["ice"]
# set a template for an empty bucket and mb containers
self._empty_mb_buckets_np = np.zeros((len(self.buckets_grid_point_label),
len(self.buckets)))
# stuff related to varying melt_f for each bucket
self.tau_e = tau_e
self.melt_f_ratio = melt_f_ratio
self.melt_f_change = melt_f_change
self.melt_f_buckets = None
self._melt_f_buckets_np = None
self.set_melt_f_buckets()
# stuff related to varying density for each bucket
self.snow_density = snow_density
self.density_change = density_change
self.density_buckets = None
self._density_buckets_np = None
self.set_density_buckets()
# define if some additonal outputs should be saved along the way
if ((store_buckets == 'monthly' and
self.climate_resolution == 'annual') or
(store_buckets == 'daily' and
self.climate_resolution != 'daily')):
raise InvalidParamsError(
"It is not possible to save the buckets in a higher resolution "
"than the applied climate! Your selection: store_buckets = "
f"{store_buckets}, climate_resolution = "
f"{self.climate_resolution}")
self.store_buckets = store_buckets
if store_buckets_dates is not None:
# if specific dates are provided this overrules store_buckets
self.store_buckets = True
self.store_buckets_dates = store_buckets_dates
# if the user wants to return previously calculated mb values
self.use_previous_mbs = use_previous_mbs
# snowline stuff
self.store_snowline = store_snowline
if self.store_snowline:
# how many buckets we need to add for the snowline buckets
self._snowline_start_month = {
"Jan": 11, "Feb": 10, "Mar": 9, "Apr": 8, "May": 7, "Jun": 6,
"Jul": 5, "Aug": 4, "Sep": 3, "Oct": 2, "Nov": 1, "Dec": 0,
}[store_snowline_start_month]
self._snowline = []
self._snowline_year = []
# this are the height values for the special cases fully snow
# covered or fully snow free
self.snowline_inf_values = {}
# Initialise buckets and conduct a potential spinup. If ys was not
# provided this is deferred to the first call of get_annual_mb,
# get_monthly_mb or get_daily_mb (see _ensure_buckets_initialized).
if self.ys is not None:
self._init_buckets()
def reset_state(self):
if self._ys_is_dynamic:
# ys will be re-derived from the first year requested at the
# next call to get_annual_mb, get_monthly_mb or get_daily_mb
self.ys = None
else:
self._init_buckets()
def _validate_spinup_climate_data(self):
if self.spinup_buckets is None:
# check if climate data for spinup is available
spinup_start = self.ys - self.spinup_years
try:
self.mbmod.validate_year(spinup_start)
self.mbmod.validate_year(self.ys)
except ValueError as e:
raise ValueError(
"Climate data for spinup not available. We need data for "
f"the period {spinup_start} (ys - spinup_years) to "
f"{self.ys}, but we get the following error: {e}")
def _ensure_buckets_initialized(self, year):
# if ys was not provided at initialisation, derive it from the
# first requested year and (re-)initialise the buckets
if self.ys is None:
self.ys = int(np.floor(year))
self._validate_spinup_climate_data()
self._init_buckets()
def _init_buckets(self):
# reset some containers for a fresh start
self.mb_buckets_np = self._empty_mb_buckets_np.copy() # kg m-2
# define length of needed timesteps
first_year = self.ys
if self.save_spinup_mbs:
first_year -= self.spinup_years
if self.climate_resolution == 'annual':
self.nr_timesteps = len(range(first_year, self.ye + 1))
elif self.climate_resolution == 'monthly':
self.nr_timesteps = len(float_years_timeseries(
y0=first_year, y1=self.ye, include_last_year=True, ))
elif self.climate_resolution == 'daily':
self.nr_timesteps = len(float_years_timeseries(
y0=first_year, y1=self.ye, include_last_year=True,
daily=True))
else:
raise NotImplementedError(
f"climate_resolution {self.climate_resolution}")
output_shape = (self.nr_timesteps, # total nr of timesteps
len(self.buckets_grid_point_label)) # nr of grid points
self._climatic_mb = np.empty(output_shape) # kg m-2
self._ice_mb = np.empty(output_shape) # kg m-2
self._mb_heights = np.empty(output_shape) # m
self._year_to_index = {} # saving the array positions of years
self._current_index = 0 # keep track of last added position
if self.store_buckets:
# the mb_buckets are stored in a dict, with key corresponding to the
# date and the buckets saved as pandas dataframes
self.mb_buckets_stored = {}
# now define the initial buckets
if self.spinup_buckets is not None:
# ok user provided the initial buckets, check shape and use
provided_shape = self.spinup_buckets.shape
needed_shape = self._empty_mb_buckets_np.shape
if (provided_shape[0] != needed_shape[0] or
provided_shape[1] != needed_shape[1] - 1): # exclude ice
raise InvalidParamsError(
"The provided spinup_buckets have the shape "
f"{provided_shape}, but we expect the shape "
f"({needed_shape[0]}, {needed_shape[1] - 1}).")
self.mb_buckets_np = self._empty_mb_buckets_np.copy()
self.mb_buckets_np[:, :-1] = self.spinup_buckets.copy()
self.mb_buckets_year = self.ys
else:
# we need to make a spinup
# define the spinup timesteps depending on the climate resolution
spinup_start_year = self.ys - self.spinup_years
if self.climate_resolution == 'annual':
spinup_steps = range(spinup_start_year, self.ys)
elif self.climate_resolution == 'monthly':
spinup_steps = float_years_timeseries(
y0=spinup_start_year, y1=self.ys)[:-1]
elif self.climate_resolution == 'daily':
spinup_steps = float_years_timeseries(
y0=spinup_start_year, y1=self.ys, daily=True)[:-1]
else:
raise NotImplementedError(
f"climate_resolution {self.climate_resolution}")
# do the actual spinup
for yr in spinup_steps:
self._apply_climate_step_and_aging_to_buckets(
heights=self.spinup_heights, year=yr,
save_mbs=self.save_spinup_mbs)
assert self.mb_buckets_year == self.ys
@property
def mb_buckets(self):
# returns current snow and firn buckets, but not the ice bucket,
# for ice look at ice_mb
return pd.DataFrame(self.mb_buckets_np[:, :-1],
index=self.buckets_grid_point_label,
columns=self.buckets[:-1],)
@property
def climatic_mb(self):
pd_dict = {}
for year in self._year_to_index:
pd_dict[year] = self._climatic_mb[[self._year_to_index[year]]][0]
return pd.DataFrame(pd_dict,
index=self.buckets_grid_point_label)
@property
def ice_mb(self):
pd_dict = {}
for year in self._year_to_index:
pd_dict[year] = self._ice_mb[[self._year_to_index[year]]][0]
return pd.DataFrame(pd_dict,
index=self.buckets_grid_point_label)
@property
def mb_heights(self):
pd_dict = {}
for year in self._year_to_index:
pd_dict[year] = self._mb_heights[[self._year_to_index[year]]][0]
return pd.DataFrame(pd_dict,
index=self.buckets_grid_point_label)
@property
def columns_thickness_m(self):
# total thickness of all snow and firn buckets, ignoring ice bucket
return np.sum(self.buckets_thickness_m, axis=1)
@property
def buckets_thickness_m(self):
# individual thickness of each bucket, ignoring ice bucket
return self.mb_buckets_np[:, :-1] / self._density_buckets_np[:, :-1]
@property
def columns_mass_kg_per_sqm(self):
return np.sum(self.mb_buckets_np, axis=1)
@property
def snowline(self):
return np.array(self._snowline)
@property
def snowline_year(self):
return np.array(self._snowline_year)
@property
def melt_f(self):
return self.mbmod.melt_f
@melt_f.setter
def melt_f(self, value):
"""Set new melt_f and reset the buckets."""
self.mbmod.melt_f = value
self.set_melt_f_buckets()
# Reset state, we do not want to change parameters midway
self.reset_state()
@property
def prcp_fac(self):
"""Precipitation factor"""
return self.mbmod.prcp_fac
@prcp_fac.setter
def prcp_fac(self, value):
"""Set new precipitation factor and reset buckets."""
self.mbmod.prcp_fac = value
# Reset state, we do not want to change parameters midway
self.reset_state()
@property
def temp_bias(self):
return self.mbmod.temp_bias
@temp_bias.setter
def temp_bias(self, value):
"""Set new temperature bias and reset buckets."""
self.mbmod.temp_bias = value
# Reset state, we do not want to change parameters midway
self.reset_state()
def set_melt_f_buckets(self):
"""Set the melt factor for each bucket."""
if self.melt_f_change == "linear":
self.melt_f_buckets = dict(
zip(self.buckets,
np.linspace(self.melt_f * self.melt_f_ratio, self.melt_f,
len(self.buckets),),
)
)
elif self.melt_f_change == "neg_exp":
if self.tau_e <= 0:
raise InvalidParamsError("`tau_e` must be above zero for"
"`melt_f_change` = 'neg_exp'.")
buckets_linspace = np.linspace(0, self.spinup_years,
len(self.buckets))
melt_f_snow = self.melt_f_ratio * self.melt_f
self.melt_f_buckets = dict(
zip(self.buckets,
self.melt_f + (melt_f_snow - self.melt_f) *
np.exp(-buckets_linspace / self.tau_e),
)
)
# ice bucket should be exactly melt_f (neg_exp only asymptotes)
self.melt_f_buckets[self.buckets[-1]] = self.melt_f
else:
raise NotImplementedError(f"melt_f_change: {self.melt_f_change}")
# save the melt_f values as pure numpy array
self._melt_f_buckets_np = np.asarray(
list(self.melt_f_buckets.values()), dtype=float)[None, :]
def set_density_buckets(self):
"""Set the density for each bucket."""
if self.density_change == "linear":
self.density_buckets = dict(
zip(self.buckets,
np.linspace(self.snow_density, self.ice_density,
len(self.buckets),),
)
)
elif self.density_change == "neg_exp":
if self.tau_e <= 0:
raise InvalidParamsError("`tau_e` must be above zero for"
"`density_change` = 'neg_exp'.")
buckets_linspace = np.linspace(0, self.spinup_years,
len(self.buckets))
self.density_buckets = dict(
zip(self.buckets,
self.ice_density + (self.snow_density - self.ice_density) *
np.exp(-buckets_linspace / self.tau_e),
)
)
# ice bucket should be exactly ice_density (neg_exp only asymptotes)
self.density_buckets[self.buckets[-1]] = self.ice_density
else:
raise NotImplementedError(f"density_change: {self.density_change}")
# save the density values as pure numpy array
self._density_buckets_np = np.asarray(
list(self.density_buckets.values()), dtype=float)[None, :]
def _apply_climate_step_and_aging_to_buckets(
self,
heights: ArrayLike,
year: float = None,
save_mbs: bool = True,
):
""" This is the central method where the magic is happening.
It gets the climate input data for the provided year, adds fresh snow to
the snow bucket, and melts the buckets in the correct order. At the end
it checks if aging is needed, which moves everything into the next older
bucket.
Parameters
----------
heights: array_like
heights used to get the climate input data, need to be the same
length as the number of defined grid points and in the same order.
The number of grid points was defined either through fl or hbins at
initialisation.
year: float
year in calendar float year
save_mbs: bool, default True
If True, the climatic_mb and the ice_mb are saved. This is set to
False during spinup
"""
# Need to provide heights for the same grid points as the bucket was
# initialized. They also need to be in the same order, but this we can
# not check
if len(heights) != len(self.buckets_grid_point_label):
raise InvalidParamsError(f'Number of heights ({len(heights)}) does '
'not match number of grid points provided '
'on initialization '
f'({len(self.buckets_grid_point_label)}).')
# get the current buckets, first is snow, last is ice, inbetween firn
mb_buckets_np = self.mb_buckets_np
# get the climate data in the defined resolution
if self.climate_resolution == "annual":
t, tmelt, prcp, prcpsol = self.mbmod.get_annual_climate(heights, year)
elif self.climate_resolution == "monthly":
t, tmelt, prcp, prcpsol = self.mbmod.get_monthly_climate(heights, year)
elif self.climate_resolution == "daily":
t, tmelt, prcp, prcpsol = self.mbmod.get_daily_climate(heights, year)
else:
raise NotImplementedError(
f"climate_resolution: {self.climate_resolution}")
# now we use tmelt for melting the buckets in order, the following
# algorithm is optimized for np arrays smaller to shape (1000, 72),
# which corresponds to 1000 flowline grid points and 72 buckets (= 6
# years with monthly aging). We do this only for the snow and firn
# buckets and deal with ice afterwards. For checking the code I add the
# shape of variables as comments.
# here we will save the results, use view to modify in-place
snow_buckets_new = mb_buckets_np[:, :-1]
nr_grid_points, nr_buckets = snow_buckets_new.shape
# we need the sum of the old buckets later for calculating delta kg m-2
snow_buckets_old_sum = snow_buckets_new.sum(axis=1)
# melt_f per bucket without ice, (1, nr_buckets)
melt_f_buckets_firn = self._melt_f_buckets_np[0, : -1]
# add one axis to tmelt for correct shape, (nr_grid_points, 1)
tmelt = tmelt[:, None]
# all solid precip. goes into fresh snow bucket before we start melting
snow_buckets_new[:, 0] += prcpsol
# now calculate cumulative tfm needed to melt each bucket and subtract
# available tmelt, finally we convert back to mass in each bucket
# (nr_grid_points, nr_buckets)
buckets_kg_m2_cumsum_left = (((snow_buckets_new / melt_f_buckets_firn
).cumsum(axis=1) - tmelt) *
melt_f_buckets_firn)
# when the cumsum is negative this bucket and all above have melted
# completely, (nr_grid_points, nr_buckets)
melted_buckets = buckets_kg_m2_cumsum_left < 0
# number of fully melted buckets per grid point, (nr_grid_points,)
nr_melted_buckets = melted_buckets.sum(axis=1)
# set melted buckets to zero
snow_buckets_new[melted_buckets] = 0.0
# set buckets which have only partially melted
some_left_buckets = np.where(nr_melted_buckets < nr_buckets)[0]
if some_left_buckets.size:
partly_melted_bucket = nr_melted_buckets[some_left_buckets]
# Set the partially melted bucket to the leftover mass
snow_buckets_new[some_left_buckets, partly_melted_bucket] = (
buckets_kg_m2_cumsum_left[some_left_buckets, partly_melted_bucket])
# Now deal with ice, ice melts where some tmelt is still left after
# melting all buckets
all_melted_grid_points = (nr_melted_buckets == nr_buckets)
if np.any(all_melted_grid_points):
# convert to tfm using melt_f of last firn bucket
remaining_tfm = -(buckets_kg_m2_cumsum_left[all_melted_grid_points, -1] /
melt_f_buckets_firn[-1])
# _melt_f_buckets_np[0, -1] is ice melt_f
ice_melt_kg_m2 = remaining_tfm * self._melt_f_buckets_np[0, -1]
# we use -= here because there could be some newly formed ice
# already in the ice bucket after aging
self.mb_buckets_np[all_melted_grid_points, -1] -= ice_melt_kg_m2
else:
ice_melt_kg_m2 = None
if save_mbs:
# Calculate the climatic mb as delta of buckets and minus potential
# ice melt, all in kg m-2
delta_kg_m2 = (snow_buckets_new.sum(axis=1) # sum of buckets at the end
- snow_buckets_old_sum) # sum of buckets at the start
if ice_melt_kg_m2 is not None:
delta_kg_m2[all_melted_grid_points] -= ice_melt_kg_m2 # ice melt
# save the climatic mb of this timestep
self._climatic_mb[self._current_index] = delta_kg_m2
# save the mb of ice, this includes potential ice gain from aging after
# the call of _bucket_aging and melt where all snow/firn buckets are
# empty
if save_mbs:
self._ice_mb[self._current_index] = self.mb_buckets_np[:, -1].copy()
# we empty the ice bucket after saving to avoid any double counting
self.mb_buckets_np[:, -1] = 0.0
if save_mbs:
self._mb_heights[self._current_index] = heights
# at the end we save the year of current index and increase it
if save_mbs:
self._year_to_index[year] = self._current_index
self._current_index += 1
# update the current year of the buckets, this is set one timestep later
# to the currently applied climate step (e.g. after applying the climate
# of January the bucket refers to the first of February)
if self.climate_resolution == 'annual':
buckets_yr = int(year) + 1
buckets_month = 1
buckets_day = 1
self.mb_buckets_year = buckets_yr
elif self.climate_resolution == 'monthly':
buckets_yr, buckets_month = floatyear_to_date(float(year))
buckets_day = 1
# special case when a new year starts
if buckets_month == 12:
buckets_yr += 1
buckets_month = 1
else:
buckets_month += 1
self.mb_buckets_year = date_to_floatyear(buckets_yr, buckets_month)
elif self.climate_resolution == 'daily':
buckets_yr, buckets_month, buckets_day = floatyear_to_date(
float(year), return_day=True)
# date_to_floatyear can deal with monthly and yearly overflows
# (e.g. 32.01. == 01.02., and 32.12.2000 == 01.01.2001)
self.mb_buckets_year = date_to_floatyear(
buckets_yr, buckets_month, buckets_day + 1)
# finally we get the values without the overflow for aging below
buckets_yr, buckets_month, buckets_day = floatyear_to_date(
self.mb_buckets_year, return_day=True
)
else:
raise NotImplementedError(
f"climate_resolution: {self.climate_resolution} ")
# Finally the bucket aging, potentially new formed ice is considered in
# the next climate step
if self.aging_frequency == 'annual':
if self.climate_resolution == 'annual':
# we need to age each step
self._bucket_aging()
elif self.climate_resolution == 'monthly':
# we age if the bucket month is January (bucket year is always
# one timestep ahead of current climate year, see above)
if buckets_month == 1:
self._bucket_aging()
elif self.climate_resolution == 'daily':
if buckets_month == 1 and buckets_day == 1:
self._bucket_aging()
else:
raise NotImplementedError(
f"climate_resolution: {self.climate_resolution} ")
elif self.aging_frequency == 'monthly':
if self.climate_resolution == 'monthly':
# we need to age each step
self._bucket_aging()
elif self.climate_resolution == 'daily':
if buckets_day == 1:
self._bucket_aging()
else:
# it is already checked at initialisation that
# climate_resolution is <= aging_frequency, but if we have
# overseen something, this should raise
raise NotImplementedError(
f"climate_resolution: {self.climate_resolution} ")
else:
raise NotImplementedError(
f"aging_frequency: {self.aging_frequency} ")
# check if the resulting bucket should be stored for later
if self.store_buckets:
store_current_bucket = False
if self.store_buckets_dates is not None:
if self.mb_buckets_year in self.store_buckets_dates:
store_current_bucket = True
elif self.store_buckets == 'annual':
if buckets_month == 1 and buckets_day == 1:
store_current_bucket = True
elif self.store_buckets == 'monthly':
if buckets_day == 1:
store_current_bucket = True
elif self.store_buckets == 'daily':
store_current_bucket = True
else:
raise NotImplementedError(f"store_buckets: {self.store_buckets}")
if store_current_bucket:
self.mb_buckets_stored[self.mb_buckets_year] = pd.DataFrame(
self.mb_buckets_np[:, :-1].copy(),
index=self.buckets_grid_point_label,
columns=self.buckets[:-1],)
# store snowline
if self.store_snowline:
if self.mb_buckets_year not in self._snowline_year:
number_buckets = buckets_month + self._snowline_start_month
# + 1 is for the snow bucket
not_melted_layers = np.any(
self.mb_buckets_np[:, :number_buckets + 1] > 0, axis=1)
# check if completely snow free
if not np.any(not_melted_layers):
self._snowline.append(np.inf)
# store a height value for snow free case
if np.inf not in self.snowline_inf_values:
self.snowline_inf_values[np.inf] = np.max(heights) + 50
# check if fully snow covered
elif np.all(not_melted_layers):
self._snowline.append(-np.inf)
if -np.inf not in self.snowline_inf_values:
# store a height value for fully snow covered case
self.snowline_inf_values[-np.inf] = np.min(heights) - 50
# otherwise get the lowest elevation band with snow cover
else:
self._snowline.append(heights[not_melted_layers][-1])
self._snowline_year.append(self.mb_buckets_year)
# nothing to return as every thing is stored is some variables
return None
def _bucket_aging(self):
"""In this function the aging of buckets happens.
This means all buckets are moved one step and an empty fresh snow bucket
is added at the top. The last bucket is the ice bucket and it is always
set to zero before calling the aging in
_apply_climate_step_and_aging_to_buckets (but we also check for this).
"""
mb_buckets_np = self.mb_buckets_np
# check that all snow and firn buckets are positive
if mb_buckets_np[:, :-1].min() < 0:
raise ValueError("Bucket values must all be positive.")
# check ice bucket is empty and we do not loose any mass
if np.any(mb_buckets_np[:, -1]):
raise ValueError("Ice bucket not empty, need to check!")
mb_buckets_val_old = mb_buckets_np[:, :-1]
len_h = len(self.buckets_grid_point_label)
# add a new fresh snow bucket and move everything to the next older
# bucket, the last firn bucket is transferred to ice here
updated_bucket_np = np.concatenate(
[np.zeros(len_h).reshape(len_h, 1), mb_buckets_val_old], axis=1)
self.mb_buckets_np = updated_bucket_np
def _run_until(self, heights, year, mb_resolution):
# check the current year of the bucket has not exceeded the current year
if self.mb_buckets_year > year and not self.use_previous_mbs:
raise InvalidWorkflowError(
"The current buckets are valid for the float year "
f"{self.mb_buckets_year}, and you want to have values for "
f"{year}. If you want to get previously calculated mass "
"balance values set use_previous_mbs to True. Be aware that "
"in case that previously calculated mb values are available "
"your provided heights here will be ignored!")
# calculate all needed time steps with the same heights
if self.climate_resolution == 'annual':
if mb_resolution == 'annual':
missing_float_years = range(int(self.mb_buckets_year), int(year) + 1)
else:
# mb_resolution can not be shorter than climate_resolution
raise NotImplementedError(f"mb_resolution: {mb_resolution}")
elif self.climate_resolution == 'monthly':
if mb_resolution == 'annual':
missing_float_years = float_years_timeseries(
y0=np.floor(self.mb_buckets_year), y1=year,
include_last_year=True, )
elif mb_resolution == 'monthly':
missing_float_years = float_years_timeseries(
y0=np.floor(self.mb_buckets_year), y1=np.ceil(year) + 1)
# only keep those actually needed
missing_float_years = [yr for yr in missing_float_years
if yr <= year]
else:
# mb_resolution can not be shorter than climate_resolution
raise NotImplementedError(f"mb_resolution: {mb_resolution}")
elif self.climate_resolution == 'daily':
if mb_resolution == 'annual':
missing_float_years = float_years_timeseries(
y0=np.floor(self.mb_buckets_year), y1=year,
include_last_year=True, daily=True)
elif mb_resolution == 'monthly':
missing_float_years = float_years_timeseries(
y0=np.floor(self.mb_buckets_year), y1=np.ceil(year) + 1,
daily=True
)
# only keep those actually needed
y, m = floatyear_to_date(year)
y_end = y if m != 12 else y + 1
m_end = m + 1 if m != 12 else 1
missing_float_years = [yr for yr in missing_float_years
if yr < date_to_floatyear(y_end, m_end)]
elif mb_resolution == 'daily':
missing_float_years = float_years_timeseries(
y0=np.floor(self.mb_buckets_year), y1=np.ceil(year) + 1,
daily=True
)
# only keep those actually needed
y, m, d = floatyear_to_date(year, return_day=True)
missing_float_years = [yr for yr in missing_float_years
# date_to_floatyear can handle overflows
# e.g. 32.12.2000 == 01.01.2001
if yr < date_to_floatyear(y, m, d + 1)]
else:
raise NotImplementedError(f"mb_resolution: {mb_resolution}")
else:
raise NotImplementedError(
f"'climate_resolution': {self.climate_resolution}")
# if use_previous_mbs=True it could be there are no missing_float_years
for yr in missing_float_years:
if yr < self.mb_buckets_year:
continue
self._apply_climate_step_and_aging_to_buckets(heights=heights,
year=yr)
def get_annual_mb(self,
heights: ArrayLike,
year: int or float,
add_climate: bool = False,
climatic_mb_or_ice_mb: str = 'climatic_mb',
include_mb_model_heights: bool = False,
**kwargs,
) -> np.float64 or tuple:
"""Get annual climatic mass balance or the ice mass balance.
The ice mass balance is important for ice dynamics.
Parameters
----------
heights : ArrayLike
The heights of each grid point for the provided year. They need to
have the same length as the number of defined grid points and in
the same order. The number of grid points at its order were defined
either through fl or hbins at initialisation.
year : int or float
Year in calendar float year or as int
add_climate : bool, default False
Additionally returns mean temperature and the sums of melt
temperature, total precipitation, and solid precipitation.
Avoids recalculating climatology later in some workflows,
e.g. ``run_with_hydro``.
climatic_mb_or_ice_mb : str, default 'climatic_mb'
Defines if you want to retrive the climatic mass balance or the mass
balance only for ice. The later one is meant to be used together
with ice dynamics
include_mb_model_heights : bool, default False
If True we add the current bucket heights to the provided heights
to account for elevation feedback due to the bucket heights.
**kwargs
Extra arguments passed to subclasses of this method.
Returns
-------
np.ndarray[np.float64] or tuple[np.ndarray]
Annual climatic mass balance or ice mass balance in metres of ice
per second. If ``add_climate`` is True, also returns mean
temperature, the sums of melt temperature, total precipitation, and
solid precipitation.
"""
# if ys was not provided at initialisation, this sets it from year
# and (re-)initialises the buckets on the first call
self._ensure_buckets_initialized(year)
# check if we should add the current bucket heights
if include_mb_model_heights:
heights = heights + self.columns_thickness_m
# compute all steps up to the desired target year using constant heights
self._run_until(heights=heights, year=year,
mb_resolution='annual')
if climatic_mb_or_ice_mb == 'climatic_mb':
mbs = self._climatic_mb
elif climatic_mb_or_ice_mb == 'ice_mb':
mbs = self._ice_mb
else:
raise NotImplementedError(
f"'climatic_mb_or_ice_mb': {climatic_mb_or_ice_mb}")
# ok now everything should be available, and we can sum up annual values
# as needed
if self.climate_resolution == 'annual':
annual_mb = mbs[self._year_to_index[year]]
elif self.climate_resolution == 'monthly':
float_months = float_years_timeseries(y0=year, y1=year+1,
daily=False)[:-1]
idx = [self._year_to_index[yr] for yr in float_months]
annual_mb = np.sum(mbs[idx], axis=0)
elif self.climate_resolution == 'daily':
float_days = float_years_timeseries(y0=year, y1=year+1,
daily=True)[:-1]
idx = [self._year_to_index[yr] for yr in float_days]
annual_mb = np.sum(mbs[idx], axis=0)
else:
raise NotImplementedError(
f"'climate_resolution': {self.climate_resolution}")
# convert from kg m-2 to m s-1
annual_mb = ((annual_mb - self.mbmod.bias) / self.sec_in_year(year) /
self.mbmod.ice_density)
if add_climate:
# because of the use of different climate resolutions we always need
# to make a distinct call here, even it is potentially already
# called in _apply_climate_step_and_aging_to_buckets
t, tmelt, prcp, prcpsol = self.mbmod.get_annual_climate(
heights=self.mb_heights[year], year=year)
return annual_mb, t, tmelt, prcp, prcpsol
else:
return annual_mb
def get_monthly_mb(self,
heights: ArrayLike,
year: float,
add_climate: bool = False,
climatic_mb_or_ice_mb: str = 'climatic_mb',
include_mb_model_heights: bool = False,
**kwargs,
) -> np.float64 or tuple:
"""Get monthly climatic mass balance or the ice mass balance.
The ice mass balance is important for ice dynamics.
Parameters
----------
heights : ArrayLike
The heights of each grid point for the provided year. They need to
have the same length as the number of defined grid points and in
the same order. The number of grid points at its order were defined
either through fl or hbins at initialisation.
year : float
Year in calendar float year
add_climate : bool, default False
Additionally returns mean temperature and the sums of melt
temperature, total precipitation, and solid precipitation.
Avoids recalculating climatology later in some workflows,
e.g. ``run_with_hydro``.
climatic_mb_or_ice_mb : str, default 'climatic_mb'
Defines if you want to retrive the climatic mass balance or the mass
balance only for ice. The later one is meant to be used together
with ice dynamics
include_mb_model_heights : bool, default False
If True we add the current bucket heights to the provided heights
to account for elevation feedback due to the bucket heights.
**kwargs
Extra arguments passed to subclasses of this method.
Returns
-------
np.ndarray[np.float64] or tuple[np.ndarray]
Monthly climatic mass balance or ice mass balance in metres of ice
per second. If ``add_climate`` is True, also returns mean
temperature, the sums of melt temperature, total precipitation, and
solid precipitation.
"""
# if ys was not provided at initialisation, this sets it from year
# and (re-)initialises the buckets on the first call
self._ensure_buckets_initialized(year)
# check if we should add the current bucket heights
if include_mb_model_heights:
heights = heights + self.columns_thickness_m
# compute all steps up to the desired target year using constant heights
self._run_until(heights=heights, year=year,
mb_resolution='monthly')
if climatic_mb_or_ice_mb == 'climatic_mb':
mbs = self._climatic_mb
elif climatic_mb_or_ice_mb == 'ice_mb':
mbs = self._ice_mb
else:
raise NotImplementedError(
f"'climatic_mb_or_ice_mb': {climatic_mb_or_ice_mb}")
# ok now everything should be available, and we can sum up monthly
# values as needed
if self.climate_resolution == 'annual':
raise NotImplementedError('You can not get a monthly mb with an '
'annual climate resolution!')
elif self.climate_resolution == 'monthly':
monthly_mb = mbs[self._year_to_index[year]]
elif self.climate_resolution == 'daily':
y_start, m_start, d_start = floatyear_to_date(year,
return_day=True)
float_days = float_years_timeseries(
y0=year, y1=year + 1, daily=True)[:-1]
# only keep days larger equal than the start of the month
float_days = [day for day in float_days
if day >= date_to_floatyear(y_start, m_start, 1)]
# only keep days smaller the start of the next month
m_end = m_start + 1 if m_start != 12 else 1
y_end = y_start if m_start != 12 else y_start + 1
float_days = [day for day in float_days
if day < date_to_floatyear(y_end, m_end, 1)]
idx = [self._year_to_index[yr] for yr in float_days]
monthly_mb = np.sum(mbs[idx], axis=0)
else:
raise NotImplementedError(
f"'climate_resolution': {self.climate_resolution}")
# convert from kg m-2 to m s-1
monthly_mb = ((monthly_mb / self.sec_in_month(year=year) -
self.mbmod.bias / self.sec_in_year(year)) /
self.mbmod.ice_density)
if add_climate:
# because of the use of different climate resolutions we always need
# to make a distinct call here, even it is potentially already
# called in _apply_climate_step_and_aging_to_buckets
t, tmelt, prcp, prcpsol = self.mbmod.get_monthly_climate(
heights=self.mb_heights[year], year=year)
return monthly_mb, t, tmelt, prcp, prcpsol
else:
return monthly_mb
def get_daily_mb(self,
heights: ArrayLike,
year: float,
add_climate: bool = False,
climatic_mb_or_ice_mb: str = 'climatic_mb',
include_mb_model_heights: bool = False,
**kwargs,
) -> np.float64 or tuple:
"""Get daily climatic mass balance or the ice mass balance.
The ice mass balance is important for ice dynamics.
Parameters
----------
heights : ArrayLike
The heights of each grid point for the provided year. They need to
have the same length as the number of defined grid points and in
the same order. The number of grid points at its order were defined
either through fl or hbins at initialisation.
year : float
Year in calendar float year
add_climate : bool, default False
Additionally returns mean temperature and the sums of melt
temperature, total precipitation, and solid precipitation.
Avoids recalculating climatology later in some workflows,
e.g. ``run_with_hydro``.
climatic_mb_or_ice_mb : str, default 'climatic_mb'
Defines if you want to retrive the climatic mass balance or the mass
balance only for ice. The later one is meant to be used together
with ice dynamics
include_mb_model_heights : bool, default False
If True we add the current bucket heights to the provided heights
to account for elevation feedback due to the bucket heights.
**kwargs
Extra arguments passed to subclasses of this method.
Returns
-------
np.ndarray[np.float64] or tuple[np.ndarray]
Daily climatic mass balance or ice mass balance in metres of ice
per second. If ``add_climate`` is True, also returns mean
temperature, the sums of melt temperature, total precipitation, and
solid precipitation.
"""
# if ys was not provided at initialisation, this sets it from year
# and (re-)initialises the buckets on the first call
self._ensure_buckets_initialized(year)
# check if we should add the current bucket heights
if include_mb_model_heights:
heights = heights + self.columns_thickness_m
# compute all steps up to the desired target year using constant heights
self._run_until(heights=heights, year=year,
mb_resolution='daily')
if climatic_mb_or_ice_mb == 'climatic_mb':
mbs = self._climatic_mb
elif climatic_mb_or_ice_mb == 'ice_mb':
mbs = self._ice_mb
else:
raise NotImplementedError(
f"'climatic_mb_or_ice_mb': {climatic_mb_or_ice_mb}")
# ok now everything should be available
if self.climate_resolution in ['annual', 'monthly']:
raise NotImplementedError(
'You can not get a daily mb with an annual or monthly climate '
f'resolution! Your climate resolution: {self.climate_resolution}')
elif self.climate_resolution == 'daily':
daily_mb = mbs[self._year_to_index[year]]
else:
raise NotImplementedError(
f"'climate_resolution': {self.climate_resolution}")
# convert from kg m-2 to m s-1
daily_mb = ((daily_mb / SEC_IN_DAY -
self.mbmod.bias / self.sec_in_year(year)) /
self.mbmod.ice_density)
if add_climate:
# because of the use of different climate resolutions we always need
# to make a distinct call here, even it is potentially already
# called in _apply_climate_step_and_aging_to_buckets
t, tmelt, prcp, prcpsol = self.mbmod.get_daily_climate(
heights=self.mb_heights[year], year=year)
return daily_mb, t, tmelt, prcp, prcpsol
else:
return daily_mb
def is_year_valid(self, year):
return self.mbmod.is_year_valid(year)
def get_ela(self, year=None, **kwargs):
# an idea to implement get_ela could be to use the previously computed
# climatic_mb values together with the used heights and do an
# interpolation to find the height where climatic_mb = 0, see spline_fit
# in dynamic spinup
raise NotImplementedError("Getting the ela for SfcTypeTIModel is"
"currently not supported.")
def _to_dataset(self):
"""Build and return the xr.Dataset representing the full model state.
Used by :meth:`save_to_file` and by
:meth:`MultipleFlowlineMassBalance.save_to_file` (which writes each
flowline as a separate nc group in one shared file).
"""
current_index = self._current_index
n_pts = self.mb_buckets_np.shape[0]
n_buckets = self.mb_buckets_np.shape[1] # snow + firn + ice
# coordinates
coords = {
'grid_point': xr.DataArray(
np.arange(n_pts),
dims=['grid_point'],
attrs={'description': 'grid points along the flowline'},
),
'bucket': xr.DataArray(
np.arange(n_buckets),
dims=['bucket'],
attrs={'description': 'buckets per grid point '
'(snow, firn_1 … firn_N, ice)'},
),
'time': xr.DataArray(
np.array(list(self._year_to_index.keys()), dtype=float),
dims=['time'],
attrs={'description': 'Floating year'},
),
}
# data variables
data_vars = {
# full bucket state including ice
'mb_buckets': xr.DataArray(
self.mb_buckets_np.copy(),
dims=['grid_point', 'bucket'],
attrs={'units': 'kg m-2',
'long_name': 'bucket mass per grid point '
'(snow, firn_1 … firn_N, ice)'},
),
# per-bucket arrays including ice
'melt_f_buckets': xr.DataArray(
np.array(list(self.melt_f_buckets.values())),
dims=['bucket'],
attrs={'units': 'kg m-2 day-1 K-1',
'long_name': 'melt factor per bucket '
'(snow, firn_1 … firn_N, ice)'},
),
'density_buckets': xr.DataArray(
np.array(list(self.density_buckets.values())),
dims=['bucket'],
attrs={'units': 'kg m-3',
'long_name': 'density per bucket '
'(snow, firn_1 … firn_N, ice)'},
),
# grid geometry
'buckets_grid_point_label': xr.DataArray(
self.buckets_grid_point_label,
dims=['grid_point'],
attrs={'long_name': 'grid point label '
'(distance along flowline or bin index)'},
),
'spinup_heights': xr.DataArray(
self.spinup_heights,
dims=['grid_point'],
attrs={'units': 'm', 'long_name': 'heights used during spinup'},
),
# computed MB output arrays (only filled portion);
# 'time' coordinate carries the float-year values
'climatic_mb': xr.DataArray(
self._climatic_mb[:current_index],
dims=['time', 'grid_point'],
attrs={'units': 'kg m-2',
'long_name': 'climatic mass balance per timestep'},
),
'ice_mb': xr.DataArray(
self._ice_mb[:current_index],
dims=['time', 'grid_point'],
attrs={'units': 'kg m-2',
'long_name': 'ice mass balance per timestep'},
),
'mb_heights_stored': xr.DataArray(
self._mb_heights[:current_index],
dims=['time', 'grid_point'],
attrs={'units': 'm',
'long_name': 'heights used per timestep'},
),
}
# conditional data variables
if self.store_snowline and len(self._snowline) > 0:
data_vars['snowline'] = xr.DataArray(
np.array(self._snowline),
dims=['snowline_step'],
attrs={'units': 'm', 'long_name': 'snowline elevation'},
)
data_vars['snowline_year'] = xr.DataArray(
np.array(self._snowline_year),
dims=['snowline_step'],
attrs={'long_name': 'float year of snowline record'},
)
if self.store_buckets_dates is not None:
data_vars['store_buckets_dates_arr'] = xr.DataArray(
np.array(self.store_buckets_dates, dtype=float),
dims=['bucket_date'],
attrs={'long_name': 'float years for which buckets are stored'},
)
if self.hbins is not None:
data_vars['hbins'] = xr.DataArray(
np.array(self.hbins),
dims=['grid_point'],
attrs={'units': 'm', 'long_name': 'height bins'},
)
# attributes (scalars / strings)
_snowline_month_to_str = {
11: 'Jan', 10: 'Feb', 9: 'Mar', 8: 'Apr', 7: 'May', 6: 'Jun',
5: 'Jul', 4: 'Aug', 3: 'Sep', 2: 'Oct', 1: 'Nov', 0: 'Dec',
}
store_snowline_start_month_str = (
_snowline_month_to_str[self._snowline_start_month]
if self.store_snowline else 'Oct'
)
attrs = {
# model class
'mb_model_class': self.mb_model_class,
# construction parameters
'settings_filesuffix': self.settings_filesuffix,
'use_leap_years': int(self.use_leap_years),
'climate_resolution': self.climate_resolution,
'aging_frequency': self.aging_frequency,
'melt_f_ratio': float(self.melt_f_ratio),
'melt_f_change': self.melt_f_change,
'tau_e': float(self.tau_e),
'ys': int(self.ys),
'spinup_years': int(self.spinup_years),
'save_spinup_mbs': int(self.save_spinup_mbs),
'use_previous_mbs': int(self.use_previous_mbs),
'store_snowline': int(self.store_snowline),
'store_snowline_start_month': store_snowline_start_month_str,
'store_buckets': str(self.store_buckets),
'snow_density': float(self.snow_density),
'ice_density': float(self.ice_density),
'density_change': self.density_change,
'bucket_names': ','.join(self.buckets),
# mbmod calibrated parameters
'melt_f': float(self.mbmod.melt_f),
'temp_bias': float(self.mbmod.temp_bias),
'prcp_fac': float(self.mbmod.prcp_fac),
'bias': float(self.mbmod.bias),
'mb_filename': self.mbmod.filename,
'mb_input_filesuffix': self.mbmod.input_filesuffix,
'mb_ys': int(self.mbmod.ys),
'mb_ye': int(self.mbmod.ye),
'temp_melt': float(self.mbmod.temp_melt),
'repeat': int(self.mbmod.repeat),
'fl_id': (int(self.mbmod.fl_id)
if self.mbmod.fl_id is not None else -1),
# current runtime state
'mb_buckets_year': float(self.mb_buckets_year),
'current_index': int(current_index),
}
return xr.Dataset(data_vars=data_vars, coords=coords, attrs=attrs)
def save_to_file(self, filesuffix=""):
"""Save the complete state of this model to a NetCDF file.
The file is written to the glacier directory as
``mb_diagnostics{filesuffix}.nc``. All information required to
reproduce an identical model via :meth:`load_from_file` is stored.
Parameters
----------
filesuffix : str, optional
Appended to the output filename, e.g. ``'_v2'`` gives
``mb_diagnostics_v2.nc``.
"""
fp = self.gdir.get_filepath('mb_diagnostics', filesuffix=filesuffix)
self._to_dataset().to_netcdf(fp)
@classmethod
def load_from_file(cls, gdir, filesuffix="", group=None,
climate_filename=None, climate_input_filesuffix=None):
"""Load a previously saved :class:`SfcTypeTIModel` from a NetCDF file.
Reads ``mb_diagnostics{filesuffix}.nc`` from the glacier directory and
re-constructs the model.
**Continue** (``climate_filename`` and ``climate_input_filesuffix`` both
``None``):
The complete runtime state (MB arrays, snowline) is
restored so that integration continues seamlessly using the same climate
input.
**Scenario branching** (either climate parameter supplied):
Only the bucket state is restored. The MB output arrays are freshly
allocated for the new climate period (from ``int(mb_buckets_year)`` to
the new ``ye``). ``current_index`` is reset to zero; the historical MB
record is not carried over.
Parameters
----------
gdir : GlacierDirectory
The glacier directory containing the saved file.
filesuffix : str, optional
Suffix appended to ``mb_diagnostics`` when the file was saved.
group : str, optional
nc group to read from (used internally by
:meth:`MultipleFlowlineMassBalance.load_from_file`).
climate_filename : str, optional
Override the stored climate filename (e.g. ``'gcm_data'``).
If ``None``, the filename from the saved file is used.
climate_input_filesuffix : str, optional
Override the stored climate input filesuffix (e.g. ``'_ssp585'``).
If ``None``, the filesuffix from the saved file is used.
Returns
-------
SfcTypeTIModel
"""
fp = gdir.get_filepath('mb_diagnostics', filesuffix=filesuffix)
new_climate = (climate_filename is not None or
climate_input_filesuffix is not None)
open_kw = {'group': group} if group is not None else {}
with xr.open_dataset(fp, **open_kw) as ds:
attrs = ds.attrs
# mb_model_class
mb_model_class_str = attrs['mb_model_class']
if mb_model_class_str == 'DailyTIModel':
mb_model_class = DailyTIModel
elif mb_model_class_str == 'MonthlyTIModel':
mb_model_class = MonthlyTIModel
else:
raise NotImplementedError(
f"mb_model_class: {mb_model_class_str}")
# store_buckets
store_buckets_str = attrs['store_buckets']
if store_buckets_str == 'False':
store_buckets_val = False
elif store_buckets_str == 'True':
# store_buckets was overridden to True by store_buckets_dates;
# pass False here and let the dates re-trigger the override
store_buckets_val = False
else:
store_buckets_val = store_buckets_str # 'annual'/'monthly'/'daily'
# store_buckets_dates
store_buckets_dates = None
if 'store_buckets_dates_arr' in ds:
store_buckets_dates = list(
ds['store_buckets_dates_arr'].values.tolist())
# saved arrays
saved_mb_buckets = ds['mb_buckets'].values # (n_pts, n_buckets)
saved_spinup_heights = ds['spinup_heights'].values # (n_pts,)
saved_grid_label = ds['buckets_grid_point_label'].values # (n_pts,)
# fl_id
fl_id = int(attrs['fl_id'])
fl_id = None if fl_id == -1 else fl_id
# construct the model
# We always pass hbins=saved_spinup_heights so that the constructor
# builds the correct grid size without needing the original flowline.
# buckets_grid_point_label is overridden below to restore the exact
# saved labels (which may differ from np.arange(n_pts) when a
# flowline was originally used).
# spinup_buckets skips the actual spinup computation.
saved_mb_buckets_year = float(attrs['mb_buckets_year'])
if new_climate:
ys_for_model = int(saved_mb_buckets_year)
save_spinup_mbs_for_model = False
else:
ys_for_model = int(attrs['ys'])
save_spinup_mbs_for_model = bool(attrs['save_spinup_mbs'])
fn_for_model = (climate_filename
if climate_filename is not None
else attrs['mb_filename'])
isuf_for_model = (climate_input_filesuffix
if climate_input_filesuffix is not None
else attrs['mb_input_filesuffix'])
model = cls(
gdir=gdir,
settings_filesuffix=attrs['settings_filesuffix'],
use_leap_years=bool(attrs['use_leap_years']),
mb_model_class=mb_model_class,
climate_resolution=attrs['climate_resolution'],
aging_frequency=attrs['aging_frequency'],
melt_f_ratio=float(attrs['melt_f_ratio']),
melt_f_change=attrs['melt_f_change'],
tau_e=float(attrs['tau_e']),
ys=ys_for_model,
spinup_years=int(attrs['spinup_years']),
save_spinup_mbs=save_spinup_mbs_for_model,
spinup_buckets=saved_mb_buckets[:, :-1],
hbins=saved_spinup_heights,
store_buckets=store_buckets_val,
store_buckets_dates=store_buckets_dates,
use_previous_mbs=bool(attrs['use_previous_mbs']),
store_snowline=bool(attrs['store_snowline']),
store_snowline_start_month=attrs['store_snowline_start_month'],
snow_density=float(attrs['snow_density']),
density_change=attrs['density_change'],
# mbmod kwargs (passed through **kwargs to mb_model_class)
melt_f=float(attrs['melt_f']),
temp_bias=float(attrs['temp_bias']),
prcp_fac=float(attrs['prcp_fac']),
bias=float(attrs['bias']),
filename=fn_for_model,
input_filesuffix=isuf_for_model,
temp_melt=float(attrs['temp_melt']),
repeat=bool(attrs['repeat']),
fl_id=fl_id,
check_calib_params=False,
)
# Restore exact grid labels
model.buckets_grid_point_label = saved_grid_label
# _init_buckets already placed saved_mb_buckets[:,:-1] into
# mb_buckets_np via spinup_buckets and set mb_buckets_year = ys.
# Override with the full saved array (ice column included).
model.mb_buckets_np[:] = saved_mb_buckets
model.mb_buckets_year = saved_mb_buckets_year
if new_climate:
# start fresh, no historical MB to restore
pass
else:
# restore full MB history
current_index = int(attrs['current_index'])
model._current_index = current_index
if current_index > 0:
model._climatic_mb[:current_index] = (
ds['climatic_mb'].values)
model._ice_mb[:current_index] = ds['ice_mb'].values
model._mb_heights[:current_index] = (
ds['mb_heights_stored'].values)
# 'time' coordinate holds the float-year keys; array
# indices are simply 0…N-1
y2i_keys = ds.coords['time'].values
model._year_to_index = {
float(k): int(i)
for i, k in enumerate(y2i_keys)
}
# snowline state
if bool(attrs['store_snowline']) and 'snowline' in ds:
model._snowline = list(ds['snowline'].values.tolist())
model._snowline_year = list(
ds['snowline_year'].values.tolist())
# snowline_inf_values is repopulated automatically on the
# next climate step
return model
[docs]
class ConstantMassBalance(MassBalanceModel):
"""Constant mass balance during a chosen period.
This is useful for equilibrium experiments.
IMPORTANT: the "naive" implementation requires to compute the massbalance
N times for each simulation year, where N is the number of years over the
climate period to average. This is very expensive, and therefore we use
interpolation. This makes it *unusable* with MB models relying on the
computational domain being always the same.
If your model requires constant domain size, conisder using RandomMassBalance
instead.
Note that it uses the "correct" way to represent the average mass balance
over a given period. See: https://oggm.org/2021/08/05/mean-forcing/
Attributes
----------
y0 : int
the center year of the period
halfsize : int
the halfsize of the period
years : ndarray
the years of the period
"""
[docs]
def __init__(self, gdir, mb_model_class=MonthlyTIModel,
y0=None, halfsize=15,
**kwargs):
"""Initialize
Parameters
----------
gdir : GlacierDirectory
the glacier directory
mb_model_class : MassBalanceModel, default ``MonthlyTIModel``
the MassBalanceModel to use for the constant climate
y0 : int, required
the year at the center of the period of interest.
halfsize : int, optional
the half-size of the time window (window size = 2 * halfsize + 1)
**kwargs:
keyword arguments to pass to the mb_model_class
"""
super().__init__()
if y0 is None:
raise InvalidParamsError('Please set `y0` explicitly')
# This is a quick'n dirty optimisation
try:
fls = gdir.read_pickle('model_flowlines')
h = []
for fl in fls:
# We use bed because of overdeepenings
h = np.append(h, fl.bed_h)
h = np.append(h, fl.surface_h)
zminmax = np.round([np.min(h)-50, np.max(h)+2000])
except FileNotFoundError:
# in case we don't have them
with ncDataset(gdir.get_filepath('gridded_data')) as nc:
if np.isfinite(nc.min_h_dem):
# a bug sometimes led to non-finite
zminmax = [nc.min_h_dem-250, nc.max_h_dem+1500]
else:
zminmax = [nc.min_h_glacier-1250, nc.max_h_glacier+1500]
self.hbins = np.arange(*zminmax, step=10)
self.valid_bounds = self.hbins[[0, -1]]
self.y0 = y0
self.halfsize = halfsize
self.years = np.arange(y0-halfsize, y0+halfsize+1)
self.hemisphere = gdir.hemisphere
if isinstance(mb_model_class, partial):
mb_model_name = mb_model_class.func.__name__
else:
mb_model_name = mb_model_class.__name__
if mb_model_name in ['SfcTypeTIModel']:
self.mbmod = mb_model_class(gdir=gdir, hbins=self.hbins, **kwargs)
else:
self.mbmod = mb_model_class(gdir=gdir, **kwargs)
@property
def temp_bias(self):
"""Temperature bias to add to the original series."""
return self.mbmod.temp_bias
@temp_bias.setter
def temp_bias(self, value):
for attr_name in ['_lazy_interp_yr', '_lazy_interp_m']:
if hasattr(self, attr_name):
delattr(self, attr_name)
self.mbmod.temp_bias = value
@property
def prcp_fac(self):
"""Precipitation factor to apply to the original series."""
return self.mbmod.prcp_fac
@prcp_fac.setter
def prcp_fac(self, value):
for attr_name in ['_lazy_interp_yr', '_lazy_interp_m']:
if hasattr(self, attr_name):
delattr(self, attr_name)
self.mbmod.prcp_fac = value
@property
def bias(self):
"""Residual bias to apply to the original series."""
return self.mbmod.bias
@bias.setter
def bias(self, value):
self.mbmod.bias = value
@lazy_property
def interp_yr(self):
# annual MB
mb_on_h = self.hbins * 0.
for yr in self.years:
mb_on_h += self.mbmod.get_annual_mb(self.hbins, year=yr)
return interp1d(self.hbins, mb_on_h / len(self.years))
@lazy_property
def interp_m(self):
# monthly MB
months = np.arange(12)+1
interp_m = []
for m in months:
mb_on_h = self.hbins*0.
for yr in self.years:
yr = date_to_floatyear(yr, m)
mb_on_h += self.mbmod.get_monthly_mb(self.hbins, year=yr)
interp_m.append(interp1d(self.hbins, mb_on_h / len(self.years)))
return interp_m
def is_year_valid(self, year):
return True
def get_monthly_climate(self, heights, year=None):
"""Average climate information at given heights.
Note that prcp is corrected with the precipitation factor and that
all other biases (precipitation, temp) are applied
Returns
-------
(temp, tempformelt, prcp, prcpsol)
"""
_, m = floatyear_to_date(year)
yrs = [date_to_floatyear(y, m) for y in self.years]
heights = np.atleast_1d(heights)
nh = len(heights)
shape = (len(yrs), nh)
temp = np.zeros(shape)
tempformelt = np.zeros(shape)
prcp = np.zeros(shape)
prcpsol = np.zeros(shape)
for i, yr in enumerate(yrs):
t, tm, p, ps = self.mbmod.get_monthly_climate(heights, year=yr)
temp[i, :] = t
tempformelt[i, :] = tm
prcp[i, :] = p
prcpsol[i, :] = ps
return (np.mean(temp, axis=0),
np.mean(tempformelt, axis=0),
np.mean(prcp, axis=0),
np.mean(prcpsol, axis=0))
def get_annual_climate(self, heights, year=None):
"""Average climate information at given heights.
Note that prcp is corrected with the precipitation factor and that
all other biases (precipitation, temp) are applied
Returns
-------
(temp, tempformelt, prcp, prcpsol)
"""
yrs = float_years_timeseries(self.years[0], self.years[-1],
include_last_year=True)
heights = np.atleast_1d(heights)
nh = len(heights)
shape = (len(yrs), nh)
temp = np.zeros(shape)
tempformelt = np.zeros(shape)
prcp = np.zeros(shape)
prcpsol = np.zeros(shape)
for i, yr in enumerate(yrs):
t, tm, p, ps = self.mbmod.get_monthly_climate(heights, year=yr)
temp[i, :] = t
tempformelt[i, :] = tm
prcp[i, :] = p
prcpsol[i, :] = ps
# Note that we do not weight for number of days per month:
# this is consistent with OGGM's calendar
return (np.mean(temp, axis=0),
np.mean(tempformelt, axis=0) * 12,
np.mean(prcp, axis=0) * 12,
np.mean(prcpsol, axis=0) * 12)
def get_monthly_mb(self, heights, year=None, add_climate=False, **kwargs):
yr, m = floatyear_to_date(year)
if add_climate:
t, tmelt, prcp, prcpsol = self.get_monthly_climate(heights, year=year)
return self.interp_m[m-1](heights), t, tmelt, prcp, prcpsol
return self.interp_m[m-1](heights)
def get_annual_mb(self, heights, year=None, add_climate=False, **kwargs):
mb = self.interp_yr(heights)
if add_climate:
t, tmelt, prcp, prcpsol = self.get_annual_climate(heights)
return mb, t, tmelt, prcp, prcpsol
return mb
[docs]
class RandomMassBalance(MassBalanceModel):
"""Random shuffle of all MB years within a given time period.
This is useful for finding a possible past glacier state or for sensitivity
experiments.
Note that this is going to be sensitive to extreme years in certain
periods, but it is by far more physically reasonable than other
approaches based on gaussian assumptions.
"""
[docs]
def __init__(self, gdir, mb_model_class=MonthlyTIModel,
y0=None, halfsize=15, seed=None,
all_years=False, unique_samples=False,
prescribe_years=None,
**kwargs):
"""Initialize.
Parameters
----------
gdir : GlacierDirectory
the glacier directory
mb_model_class : MassBalanceModel, default ``MonthlyTIModel``
the MassBalanceModel to use for the random shuffle
y0 : int, required
the year at the center of the period of interest.
halfsize : int, optional
the half-size of the time window (window size = 2 * halfsize + 1)
seed : int, optional
Random seed used to initialize the pseudo-random number generator.
all_years : bool
if True, overwrites ``y0`` and ``halfsize`` to use all available
years.
unique_samples: bool
if true, chosen random mass balance years will only be available
once per random climate period-length
if false, every model year will be chosen from the random climate
period with the same probability
prescribe_years : pandas Series
instead of random samples, take a series of (i, y) pairs where
(i) is the simulation year index and (y) is the year to pick in the
original timeseries. Overrides `y0`, `halfsize`, `all_years`,
`unique_samples` and `seed`.
**kwargs:
keyword arguments to pass to the mb_model_class
"""
super().__init__()
self.valid_bounds = [-1e4, 2e4] # in m
self.mbmod = mb_model_class(gdir=gdir, **kwargs)
# Climate period
self.prescribe_years = prescribe_years
if self.prescribe_years is None:
# Normal stuff
self.rng = np.random.RandomState(seed)
if all_years:
self.years = self.mbmod.years
else:
if y0 is None:
raise InvalidParamsError('Please set `y0` explicitly')
self.years = np.arange(y0 - halfsize, y0 + halfsize + 1)
else:
self.rng = None
self.years = self.prescribe_years.index
self.yr_range = (self.years[0], self.years[-1] + 1)
self.ny = len(self.years)
self.hemisphere = gdir.hemisphere
self._state_yr = dict()
# Sampling without replacement
self.unique_samples = unique_samples
if self.unique_samples:
self.sampling_years = self.years
@property
def temp_bias(self):
"""Temperature bias to add to the original series."""
return self.mbmod.temp_bias
@temp_bias.setter
def temp_bias(self, value):
"""Temperature bias to add to the original series."""
self.mbmod.temp_bias = value
@property
def prcp_fac(self):
"""Precipitation factor to apply to the original series."""
return self.mbmod.prcp_fac
@prcp_fac.setter
def prcp_fac(self, value):
"""Precipitation factor to apply to the original series."""
self.mbmod.prcp_fac = value
@property
def bias(self):
"""Residual bias to apply to the original series."""
return self.mbmod.bias
@bias.setter
def bias(self, value):
"""Residual bias to apply to the original series."""
self.mbmod.bias = value
def is_year_valid(self, year):
return True
def get_state_yr(self, year=None):
"""For a given year, get the random year associated to it."""
year = int(year)
if year not in self._state_yr:
if self.prescribe_years is not None:
self._state_yr[year] = self.prescribe_years.loc[year]
else:
if self.unique_samples:
# --- Sampling without replacement ---
if self.sampling_years.size == 0:
# refill sample pool when all years were picked once
self.sampling_years = self.years
# choose one year which was not used in the current period
_sample = self.rng.choice(self.sampling_years)
# write chosen year to dictionary
self._state_yr[year] = _sample
# update sample pool: remove the chosen year from it
self.sampling_years = np.delete(
self.sampling_years,
np.where(self.sampling_years == _sample))
else:
# --- Sampling with replacement ---
self._state_yr[year] = self.rng.randint(*self.yr_range)
return self._state_yr[year]
def get_monthly_mb(self, heights, year=None, **kwargs):
ryr, m = floatyear_to_date(year)
ryr = date_to_floatyear(self.get_state_yr(ryr), m)
return self.mbmod.get_monthly_mb(heights, year=ryr, **kwargs)
def get_annual_mb(self, heights, year=None, **kwargs):
ryr = self.get_state_yr(int(year))
return self.mbmod.get_annual_mb(heights, year=ryr, **kwargs)
[docs]
class UncertainMassBalance(MassBalanceModel):
"""Adding uncertainty to a mass balance model.
There are three variables for which you can add uncertainty:
- temperature (additive bias)
- precipitation (multiplicative factor)
- residual (a bias in units of MB)
"""
[docs]
def __init__(self, basis_model,
rdn_temp_bias_seed=None, rdn_temp_bias_sigma=0.1,
rdn_prcp_fac_seed=None, rdn_prcp_fac_sigma=0.1,
rdn_bias_seed=None, rdn_bias_sigma=100):
"""Initialize.
Parameters
----------
basis_model : MassBalanceModel
the model to which you want to add the uncertainty to
rdn_temp_bias_seed : int
the seed of the random number generator
rdn_temp_bias_sigma : float
the standard deviation of the random temperature error
rdn_prcp_fac_seed : int
the seed of the random number generator
rdn_prcp_fac_sigma : float
the standard deviation of the random precipitation error
(to be consistent this should be renamed prcp_fac as well)
rdn_bias_seed : int
the seed of the random number generator
rdn_bias_sigma : float
the standard deviation of the random MB error
"""
super(UncertainMassBalance, self).__init__()
# the aim here is to change temp_bias and prcp_fac so
self.mbmod = basis_model
self.hemisphere = basis_model.hemisphere
self.valid_bounds = self.mbmod.valid_bounds
self.is_year_valid = self.mbmod.is_year_valid
self.rng_temp = np.random.RandomState(rdn_temp_bias_seed)
self.rng_prcp = np.random.RandomState(rdn_prcp_fac_seed)
self.rng_bias = np.random.RandomState(rdn_bias_seed)
self._temp_sigma = rdn_temp_bias_sigma
self._prcp_sigma = rdn_prcp_fac_sigma
self._bias_sigma = rdn_bias_sigma
self._state_temp = dict()
self._state_prcp = dict()
self._state_bias = dict()
def is_year_valid(self, year):
return self.mbmod.is_year_valid(year)
@property
def temp_bias(self):
"""Temperature bias to add to the original series."""
return self.mbmod.temp_bias
@temp_bias.setter
def temp_bias(self, value):
"""Temperature bias to add to the original series."""
self.mbmod.temp_bias = value
@property
def prcp_fac(self):
"""Precipitation factor to apply to the original series."""
return self.mbmod.prcp_fac
@prcp_fac.setter
def prcp_fac(self, value):
"""Precipitation factor to apply to the original series."""
self.mbmod.prcp_fac = value
def _get_state_temp(self, year):
year = int(year)
if year not in self._state_temp:
self._state_temp[year] = self.rng_temp.randn() * self._temp_sigma
return self._state_temp[year]
def _get_state_prcp(self, year):
year = int(year)
if year not in self._state_prcp:
self._state_prcp[year] = self.rng_prcp.randn() * self._prcp_sigma
return self._state_prcp[year]
def _get_state_bias(self, year):
year = int(year)
if year not in self._state_bias:
self._state_bias[year] = self.rng_bias.randn() * self._bias_sigma
return self._state_bias[year]
def get_monthly_mb(self, heights, year=None, **kwargs):
raise NotImplementedError()
def get_annual_mb(self, heights, year=None, fl_id=None, **kwargs):
# Keep the original biases and add a random error
_t = self.mbmod.temp_bias
_p = self.mbmod.prcp_fac
_b = self.mbmod.bias
self.mbmod.temp_bias = self._get_state_temp(year) + _t
self.mbmod.prcp_fac = self._get_state_prcp(year) + _p
self.mbmod.bias = self._get_state_bias(year) + _b
try:
out = self.mbmod.get_annual_mb(heights, year=year, fl_id=fl_id)
except BaseException:
self.mbmod.temp_bias = _t
self.mbmod.prcp_fac = _p
self.mbmod.bias = _b
raise
# Back to normal
self.mbmod.temp_bias = _t
self.mbmod.prcp_fac = _p
self.mbmod.bias = _b
return out
[docs]
class MultipleFlowlineMassBalance(MassBalanceModel):
"""Handle mass balance at the glacier level instead of flowline level.
Convenience class doing not much more than wrapping a list of mass balance
models, one for each flowline.
This is useful for real-case studies, where each flowline might have
different model parameters.
Attributes
----------
fls : list
list of flowline objects
"""
[docs]
def __init__(self, gdir, settings_filesuffix='',
fls=None, mb_model_class=MonthlyTIModel,
use_inversion_flowlines=False,
flowlines_filesuffix='',
input_filesuffix=None,
**kwargs):
"""Initialize.
Parameters
----------
gdir : GlacierDirectory
the glacier directory
settings_filesuffix : str, optional
You can use a different set of settings by providing a
filesuffix. This is useful for sensitivity experiments.
fls : list, optional
list of flowline objects to use (defaults to 'model_flowlines')
mb_model_class : MassBalanceModel, default ``MonthlyTIModel``
the MassBalanceModel to use e.g. ``DailyTIModel``,
``ConstantMassBalance``.
use_inversion_flowlines: bool, optional
use 'inversion_flowlines' instead of 'model_flowlines'
flowlines_filesuffix : str
suffix to the flowlines filename to use (could be
'inversion_flowlines' or 'model_flowlines', depending on
'use_inversion_flowlines').
Default is ''
kwargs : kwargs to pass to mb_model_class
"""
gdir.settings_filesuffix = settings_filesuffix
self.gdir = gdir
# Read in the flowlines
if use_inversion_flowlines:
fls = gdir.read_pickle('inversion_flowlines',
filesuffix=flowlines_filesuffix)
if fls is None:
try:
fls = gdir.read_pickle('model_flowlines',
filesuffix=flowlines_filesuffix)
except FileNotFoundError:
raise InvalidWorkflowError('Need a valid `model_flowlines` '
'file. If you explicitly want to '
'use `inversion_flowlines`, set '
'use_inversion_flowlines=True.')
self.fls = fls
# Initialise the mb models
self.flowline_mb_models = []
for fl in self.fls:
# Merged glaciers will need different climate files, use filesuffix
if (fl.rgi_id is not None) and (fl.rgi_id != gdir.rgi_id):
input_filesuffix = '' if input_filesuffix is None else None
rgi_filesuffix = '_' + fl.rgi_id + input_filesuffix
else:
rgi_filesuffix = input_filesuffix
# some mb_models need the actual flowlines (e.g. SfcTypeTIModel)
if 'fl' in inspect.signature(mb_model_class).parameters:
kwargs['fl'] = fl
if rgi_filesuffix is not None:
kwargs['input_filesuffix'] = rgi_filesuffix
self.flowline_mb_models.append(
mb_model_class(
gdir=gdir,
settings_filesuffix=settings_filesuffix,
**kwargs,
)
)
self.valid_bounds = self.flowline_mb_models[-1].valid_bounds
self.hemisphere = gdir.hemisphere
self.ice_density = self.flowline_mb_models[-1].ice_density
self.use_leap_years = self.flowline_mb_models[-1].use_leap_years
@property
def temp_bias(self):
"""Temperature bias to add to the original series."""
return self.flowline_mb_models[0].temp_bias
@temp_bias.setter
def temp_bias(self, value):
"""Temperature bias to add to the original series."""
for mbmod in self.flowline_mb_models:
mbmod.temp_bias = value
@property
def prcp_fac(self):
"""Precipitation factor to apply to the original series."""
return self.flowline_mb_models[0].prcp_fac
@prcp_fac.setter
def prcp_fac(self, value):
"""Precipitation factor to apply to the original series."""
for mbmod in self.flowline_mb_models:
mbmod.prcp_fac = value
@property
def bias(self):
"""Residual bias to apply to the original series."""
return self.flowline_mb_models[0].bias
@bias.setter
def bias(self, value):
"""Residual bias to apply to the original series."""
for mbmod in self.flowline_mb_models:
mbmod.bias = value
@property
def melt_f(self):
"""Melt factor."""
return self.flowline_mb_models[0].melt_f
@melt_f.setter
def melt_f(self, value):
"""Melt factor."""
for mbmod in self.flowline_mb_models:
mbmod.melt_f = value
@property
def filename(self):
return self.flowline_mb_models[0].filename
@property
def input_filesuffix(self):
return self.flowline_mb_models[0].input_filesuffix
@property
def ys_float(self):
return self.flowline_mb_models[0].ys_float
@property
def ye_float(self):
return self.flowline_mb_models[0].ye_float
def is_year_valid(self, year):
return self.flowline_mb_models[0].is_year_valid(year)
def sec_in_month(self, year):
return self.flowline_mb_models[0].sec_in_month(year)
def sec_in_year(self, year):
return self.flowline_mb_models[0].sec_in_year(year)
def reset_state(self):
for fl_mb_mod in self.flowline_mb_models:
fl_mb_mod.reset_state()
def get_daily_mb(self, heights, year=None, fl_id=None, **kwargs):
if fl_id is None:
raise ValueError("`fl_id` is required for"
"MultipleFlowlineMassBalance.")
return self.flowline_mb_models[fl_id].get_daily_mb(heights,
year=year,
**kwargs)
def get_monthly_mb(self, heights, year=None, fl_id=None, **kwargs):
if fl_id is None:
raise ValueError('`fl_id` is required for '
'MultipleFlowlineMassBalance!')
return self.flowline_mb_models[fl_id].get_monthly_mb(heights,
year=year,
**kwargs)
def get_annual_mb(self, heights, year=None, fl_id=None, **kwargs):
if fl_id is None:
raise ValueError('`fl_id` is required for '
'MultipleFlowlineMassBalance!')
return self.flowline_mb_models[fl_id].get_annual_mb(heights,
year=year,
**kwargs)
def _get_mb_on_flowlines(self, fls=None, year=None, mb_call=None):
if fls is None:
fls = self.fls
heights = []
widths = []
mbs = []
for i, fl in enumerate(fls):
h = fl.surface_h
heights = np.append(heights, h)
widths = np.append(widths, fl.widths)
mbs = np.append(mbs, mb_call(h, year=year, fl_id=i))
return heights, widths, mbs
def get_daily_mb_on_flowlines(self, fls=None, year=None):
"""Get the MB on all points of the glacier at once.
Parameters
----------
fls: list, optional
the list of flowlines to get the mass balance from. Defaults
to self.fls
year: float, optional
the time (in the "floating year" convention)
Returns
-------
Tuple of (heights, widths, mass_balance) 1D arrays
"""
return self._get_mb_on_flowlines(fls=fls, year=year,
mb_call=self.get_daily_mb)
def get_monthly_mb_on_flowlines(self, fls=None, year=None):
"""Get the MB on all points of the glacier at once.
Parameters
----------
fls: list, optional
the list of flowlines to get the mass balance from. Defaults
to self.fls
year: float, optional
the time (in the "floating year" convention)
Returns
-------
Tuple of (heights, widths, mass_balance) 1D arrays
"""
return self._get_mb_on_flowlines(fls=fls, year=year,
mb_call=self.get_monthly_mb)
def get_annual_mb_on_flowlines(self, fls=None, year=None):
"""Get the MB on all points of the glacier at once.
Parameters
----------
fls: list, optional
the list of flowlines to get the mass balance from. Defaults
to self.fls
year: float, optional
the time (in the "floating year" convention)
Returns
-------
Tuple of (heights, widths, mass_balance) 1D arrays
"""
return self._get_mb_on_flowlines(fls=fls, year=year,
mb_call=self.get_annual_mb)
def get_specific_mb(self, heights=None, widths=None, fls=None, year=None,
time_resolution='annual',):
"""Specific mass balance for a given glacier geometry.
Units depends on time_resolution:
- 'annual': [mm w.e. yr-1], or millimeter water equivalent per year.
- 'monthly': [mm w.e. month-1], or millimeter water equivalent per month.
- 'daily': [mm w.e. day-1], or millimeter water equivalent per day.
Parameters
----------
heights : array_like, default None
Altitudes at which the mass balance will be computed.
Overridden by ``fls`` if provided.
widths : array_like, default None
Widths of the flowline (necessary for the weighted average).
Overridden by ``fls`` if provided.
fls : list[oggm.Flowline], default None
List of flowline instances. Alternative to heights and
widths, and overrides them if provided.
year : array_like[float] or float, default None
Year, or a range of years in "floating year" convention.
time_resolution : str
The resolution of the provided "floating year". Options are
'annual', 'monthly' or 'daily'. Default is 'annual'.
Returns
-------
np.ndarray
Specific mass balance (units: mm w.e. yr-1).
"""
if heights is not None or widths is not None:
raise ValueError(
"`heights` and `widths` kwargs do not work with "
"MultipleFlowlineMassBalance!"
)
if fls is None:
fls = self.fls
# we can use the function from MassBalanceModel as the correct mb models
# are selected by the fl_id in get_annual_mb, get_monthly_mb and
# get_daily_mb as defined in MultipleFlowlineMassBalance
return super().get_specific_mb(fls=fls, year=year,
time_resolution=time_resolution,)
def get_ela(self, year=None, **kwargs):
"""Get the equilibrium line altitude for a given year.
The ELA here is not without ambiguity: it computes a mean
weighted by area.
Parameters
----------
year : array_like[float] or float, default None
Year, or a range of years in "floating year" convention.
Returns
-------
float or np.ndarray
The equilibrium line altitude (ELA) in m.
"""
stack = []
year = np.atleast_1d(year)
for mb_yr in year:
elas = []
areas = []
for fl_id, (fl, mb_mod) in enumerate(
zip(self.fls, self.flowline_mb_models)
):
elas.append(
mb_mod.get_ela(year=mb_yr, fl_id=fl_id, fls=self.fls)
)
areas.append(np.sum(fl.widths))
stack.append(weighted_average_1d(elas, areas))
return set_array_type(stack)
def save_to_file(self, filesuffix=""):
"""Save the state of all flowline MB models to a single NetCDF file.
Writes ``mb_diagnostics{filesuffix}.nc`` to the glacier directory.
Each :class:`SfcTypeTIModel` is stored as a separate nc group named
(``fl_0``, ``fl_1``, ...) matching the flowline index. The root of the
file carries a ``n_flowlines`` attribute so that :meth:`load_from_file`
knows how many groups to expect.
Only supported when all ``flowline_mb_models`` are instances of
:class:`SfcTypeTIModel`.
Parameters
----------
filesuffix : str, optional
Appended to the output filename.
"""
fp = self.gdir.get_filepath('mb_diagnostics', filesuffix=filesuffix)
# Welcome ds
ds = xr.Dataset()
ds.attrs['description'] = ('OGGM SfcTypeTIModel state on flowlines. '
'Check groups for data.')
ds.attrs['oggm_version'] = __version__
ds.attrs['n_flowlines'] = len(self.flowline_mb_models)
ds.to_netcdf(fp, 'w')
# append each state as a group
for i, mb_mod in enumerate(self.flowline_mb_models):
mb_mod._to_dataset().to_netcdf(fp, group=f'fl_{i}', mode='a')
@classmethod
def load_from_file(cls, gdir, filesuffix="", climate_filename=None,
climate_input_filesuffix=None):
"""Load a :class:`MultipleFlowlineMassBalance` from a saved nc file.
Reads ``mb_diagnostics{filesuffix}.nc``, reconstructs one
:class:`SfcTypeTIModel` per flowline from the corresponding nc group,
and returns a fully initialised :class:`MultipleFlowlineMassBalance`.
To continue with same climate input leave ``climate_filename`` and
``climate_input_filesuffix`` as ``None``. For scenario branching
(switch to a new climate file while keeping the bucket state), supply
the new climate parameters. The MB history is then reset and the
output arrays are sized for the new climate period.
Parameters
----------
gdir : GlacierDirectory
filesuffix : str, optional
climate_filename : str, optional
Override the stored climate filename (e.g. ``'gcm_data'``).
climate_input_filesuffix : str, optional
Override the stored climate input filesuffix (e.g. ``'_ssp585'``).
Returns
-------
MultipleFlowlineMassBalance
"""
fp = gdir.get_filepath('mb_diagnostics', filesuffix=filesuffix)
with xr.open_dataset(fp) as ds_root:
n_fls = int(ds_root.attrs['n_flowlines'])
flowline_mb_models = [
SfcTypeTIModel.load_from_file(
gdir,
filesuffix=filesuffix,
group=f'fl_{i}',
climate_filename=climate_filename,
climate_input_filesuffix=climate_input_filesuffix,
)
for i in range(n_fls)
]
# Build without triggering __init__ (which would re-run spinup and
# re-read flowlines from disk).
obj = object.__new__(cls)
try:
obj.fls = gdir.read_pickle('model_flowlines')
except FileNotFoundError:
obj.fls = None
obj.gdir = gdir
obj.flowline_mb_models = flowline_mb_models
obj.valid_bounds = flowline_mb_models[-1].valid_bounds
obj.hemisphere = gdir.hemisphere
obj.ice_density = flowline_mb_models[-1].ice_density
obj.use_leap_years = flowline_mb_models[-1].use_leap_years
return obj
def calving_mb(gdir):
"""Calving mass-loss in specific MB equivalent.
This is necessary to calibrate the mass balance.
"""
if not gdir.is_tidewater:
return 0.
# Ok. Just take the calving rate from cfg and change its units
# Original units: km3 a-1, to change to mm a-1 (units of specific MB)
rho = gdir.settings['ice_density']
return gdir.inversion_calving_rate * 1e9 * rho / gdir.rgi_area_m2
def decide_winter_precip_factor(gdir):
"""Utility function to decide on a precip factor based on winter precip.
The values here are hardcoded as OGGM evolves - there should be an
easy way to change it if people need more flexibility one day.
"""
# get non-corrected winter daily mean prcp (kg m-2 day-1)
# it is easier to get this directly from the raw climate files
fp = gdir.get_filepath('climate_historical')
with xr.open_dataset(fp).prcp as ds_pr:
# just select winter months
if gdir.hemisphere == 'nh':
m_winter = [10, 11, 12, 1, 2, 3, 4]
else:
m_winter = [4, 5, 6, 7, 8, 9, 10]
ds_pr_winter = ds_pr.where(ds_pr['time.month'].isin(m_winter), drop=True)
# select the correct 41 year time period
ds_pr_winter = ds_pr_winter.sel(time=slice('1979-01-01', '2019-12-01'))
# check if we have the full time period: 41 years * 7 months
text = ('the climate period has to go from 1979-01 to 2019-12,',
'use W5E5 or GSWP3_W5E5 as baseline climate and',
'repeat the climate processing')
assert len(ds_pr_winter.time) == 41 * 7, text
w_prcp = float((ds_pr_winter / ds_pr_winter.time.dt.daysinmonth).mean())
climsource = gdir.get_climate_info()['baseline_climate_source']
if 'w5e5' in climsource.lower():
# from OGGM calibration to winter MB, LOG
# repeated by Lily in November 2025 with newest gdirs
# using t_melt=-1, cte lapse rate, monthly resolution
a, b = -1.0614, 3.9200
prcp_fac = a * np.log(w_prcp) + b
elif 'era5' in climsource.lower():
# from OGGM calibration to winter MB, LINEAR
# repeated by Lily in November 2025 with newest gdirs
# using t_melt=-1, cte lapse rate, monthly resolution
a, b = -0.09078476, 2.43505368
prcp_fac = a * w_prcp + b
else:
msg = (f'Baseline climate {climsource} not suitable for'
'decide_winter_precip_factor(). Set prcp_fac.')
raise InvalidWorkflowError(msg)
# don't allow extremely low/high prcp. factors!!!
return clip_scalar(prcp_fac,
gdir.settings['prcp_fac_min'],
gdir.settings['prcp_fac_max'])
[docs]
@entity_task(log, writes=['mb_calib'])
def mb_calibration_from_wgms_mb(gdir, settings_filesuffix='',
observations_filesuffix='',
**kwargs):
"""Calibrate for in-situ, annual MB.
This only works for glaciers which have WGMS data!
For now this just calls mb_calibration_from_scalar_mb internally,
but could be cleverer than that if someone wishes to implement it.
Parameters
----------
gdir : GlacierDirectory
the glacier directory to process
settings_filesuffix: str
You can use a different set of settings by providing a filesuffix. This
is useful for sensitivity experiments. Code-wise the settings_filesuffix
is set in the @entity-task decorater.
observations_filesuffix: str
The observations filesuffix, where the used calibration data will be
stored. Code-wise the observations_filesuffix is set in the @entity-task
decorater.
**kwargs : any kwarg accepted by mb_calibration_from_scalar_mb
except `ref_mb` and `ref_mb_years`
"""
# Note that this currently does not work for hydro years (WGMS uses hydro)
# A way to go would be to teach the mb models to use calendar years
# internally but still output annual MB in hydro convention.
mbdf = gdir.get_ref_mb_data()
# Keep only valid values
mbdf = mbdf.loc[~mbdf['ANNUAL_BALANCE'].isnull()]
gdir.observations['ref_mb'] = {
'value': mbdf['ANNUAL_BALANCE'].mean(),
'unit': 'kg m-2 yr-1',
'years': mbdf.index.values,
}
return mb_calibration_from_scalar_mb(gdir,
settings_filesuffix=settings_filesuffix,
observations_filesuffix=observations_filesuffix,
**kwargs)
@entity_task(log, writes=['mb_calib'])
def mb_calibration_to_rmsd(gdir, *,
settings_filesuffix='',
ref_df=None,
write_to_gdir=True,
overwrite_gdir=False,
use_2d_mb=False,
calibrate_params=('melt_f',),
melt_f=None,
melt_f_min=None,
melt_f_max=None,
prcp_fac=None,
prcp_fac_min=None,
prcp_fac_max=None,
temp_bias=None,
temp_bias_min=None,
temp_bias_max=None,
mb_model_class=MonthlyTIModel,
filesuffix='',
optimisation_kwargs=None,):
"""Determine the MB parameters by minimising RMSD to a reference timeseries
This calibrates the mass balance parameters using interannual
MB data from the WGMS data over a given period. This calibration uses
differential evolution to calibrate all given parameters to minimize
the RMSD as much as possible.
This function is useful to calibrate all three parameters at once,
on glaciers where WGMS or other in-situ observations are available.
This is achieved by minimising the RMSD between the reference MB
timeseries and the modelled MB timeseries over the period of available
observations. The minimisiation technique chosen here is differential
evolution, which is a global optimization technique that does not
require the function to be differentiable. This makes it
suitable for our problem, where the relationship between the parameters
and the MB timeseries can be complex and non-linear, and we are able
to calibrate all three parameters at once.
Note that this does not compute the apparent mass balance at
the same time - users need to run `apparent_mb_from_any_mb after`
calibration.
Parameters
----------
gdir : :py:class:`oggm.GlacierDirectory`
the glacier directory to calibrate
settings_filesuffix: str
You can use a different set of settings by providing a filesuffix. This
is useful for sensitivity experiments. Code-wise the settings_filesuffix
is set in the @entity-task decorater.
ref_df : pandas dataframe, required
the dataframe of annual mass balance values from the wgms data
(units: kg m-2 yr-1).
It is required here - if you want to use available observations,
write_to_gdir : bool
whether to write the results of the calibration to the glacier
directory. If True (the default), this will be saved as `mb_calib.json`
and be used by the MassBalanceModel class as parameters in subsequent
tasks.
overwrite_gdir : bool
if a `mb_calib.json` exists, this task won't overwrite it per default.
Set this to True to enforce overwriting (i.e. with consequences for the
future workflow).
use_2d_mb : bool
Set to True if the mass balance calibration has to be done of the 2D mask
of the glacier (for fully distributed runs only).
mb_model_class : MassBalanceModel class
the MassBalanceModel to use for the calibration. Needs to use the
same parameters as MonthlyTIModel (the default): melt_f,
temp_bias, prcp_fac.
calibrate_params : tuple
the parameter(s) that will be used in the calibration, it must be at least one of:
'melt_f', 'temp_bias', 'prcp_fac'. Defaults to ('melt_f',)
melt_f: float
the default value to use as melt factor (or the starting value when
optimizing MB). Defaults to gdir.settings['melt_f'].
melt_f_min: float
the minimum accepted value for the melt factor during optimisation.
Defaults to gdir.settings['melt_f_min'].
melt_f_max: float
the maximum accepted value for the melt factor during optimisation.
Defaults to gdir.settings['melt_f_max'].
prcp_fac: float
the default value to use as precipitation scaling factor
(or the starting value when optimizing MB). Defaults to the method
chosen in `params.cfg` (winter prcp or global factor).
prcp_fac_min: float
the minimum accepted value for the precipitation scaling factor during
optimisation. Defaults to gdir.settings['prcp_fac_min'].
prcp_fac_max: float
the maximum accepted value for the precipitation scaling factor during
optimisation. Defaults to gdir.settings['prcp_fac_max'].
temp_bias: float
the default value to use as temperature bias (or the starting value when
optimizing MB). Defaults to 0.
temp_bias_min: float
the minimum accepted value for the temperature bias during optimisation.
Defaults to cfg.PARAMS['temp_bias_min'].
temp_bias_max: float
the maximum accepted value for the temperature bias during optimisation.
Defaults to cfg.PARAMS['temp_bias_max'].
filesuffix: str
add a filesuffix to mb_calib.json. This could be useful for sensitivity
analyses with MB models, if they need to fetch other sets of params for
example.
optimisation_kwargs: dict
optional keyword arguments forwarded to
`scipy.optimize.differential_evolution`, overriding the defaults
(``tol=1e-2``, ``maxiter=5000``). Useful to set a ``seed`` for
reproducibility or to tighten ``tol`` for a more precise optimum.
"""
# Param constraints
if melt_f_min is None:
melt_f_min = gdir.settings['melt_f_min']
if melt_f_max is None:
melt_f_max = gdir.settings['melt_f_max']
if prcp_fac_min is None:
prcp_fac_min = gdir.settings['prcp_fac_min']
if prcp_fac_max is None:
prcp_fac_max = gdir.settings['prcp_fac_max']
if temp_bias_min is None:
temp_bias_min = gdir.settings['temp_bias_min']
if temp_bias_max is None:
temp_bias_max = gdir.settings['temp_bias_max']
if not use_2d_mb:
fls = gdir.read_pickle('inversion_flowlines')
else:
# if the 2D data is used, the flowline is not needed.
fls = None
# get the 2D data
fp = gdir.get_filepath('gridded_data')
with xr.open_dataset(fp) as ds:
# 'topo' instead of 'topo_smoothed'?
heights = ds.topo_smoothed.data[ds.glacier_mask.data == 1]
widths = np.ones(len(heights))
# Climate period
ref_mb_years = ref_df.index.values
years = ref_mb_years
# Do we have a calving glacier?
cmb = calving_mb(gdir)
if cmb != 0:
raise NotImplementedError('Calving with geodetic MB is not implemented '
'yet, but it should actually work. Well keep '
'you posted!')
# Ok, regardless on how we want to calibrate, we start with defaults
if melt_f is None:
melt_f = gdir.settings['melt_f']
if prcp_fac is None:
if gdir.settings['prcp_fac'] is None:
prcp_fac = decide_winter_precip_factor(gdir)
else:
prcp_fac = gdir.settings['prcp_fac']
if temp_bias is None:
temp_bias = 0
# Create the MB model we will calibrate
mb_mod = mb_model_class(gdir,
settings_filesuffix=settings_filesuffix,
melt_f=melt_f,
temp_bias=temp_bias,
prcp_fac=prcp_fac,
check_calib_params=False)
# Check that the years are available
for y in years:
if not mb_mod.is_year_valid(y):
raise ValueError(f'year {y} out of the valid time bounds: '
f'[{mb_mod.ys}, {mb_mod.ye}]')
# Check that the calibrate params are valid
_param_bounds = {
"melt_f": (melt_f_min, melt_f_max),
"prcp_fac": (prcp_fac_min, prcp_fac_max),
"temp_bias": (temp_bias_min, temp_bias_max),
}
bounds = []
for param in calibrate_params:
if param not in _param_bounds:
raise InvalidParamsError(
"calibrate_params must be a tuple with any of "
"'melt_f', 'prcp_fac', 'temp_bias'"
)
bounds.append(_param_bounds[param])
ref_values = ref_df.values
def rmsd_cost_function(x, *model_attrs):
for model_attr, val in zip(model_attrs, x):
setattr(mb_mod, model_attr, val)
if use_2d_mb:
sim_out = mb_mod.get_specific_mb(
heights=heights, widths=widths, year=years
)
else:
sim_out = mb_mod.get_specific_mb(fls=fls, year=years)
return rmsd(ref_values, sim_out)
# Default optimiser settings, can be overridden by the caller (e.g. to
# set a seed for reproducibility). tol=1e-2 is the scipy default; tighter
# values cost many more cost-function evaluations for negligible gain here.
de_kwargs = dict(tol=1e-2, maxiter=5000)
if optimisation_kwargs:
de_kwargs.update(optimisation_kwargs)
try:
res = optimize.differential_evolution(
rmsd_cost_function,
bounds=bounds,
args=calibrate_params,
**de_kwargs,
)
for param, val in zip(calibrate_params, res.x):
if param == "prcp_fac":
prcp_fac = val
elif param == "melt_f":
melt_f = val
elif param == "temp_bias":
temp_bias = val
except ValueError:
raise RuntimeError(f'{gdir.rgi_id}: could not minimise the rmsd. '
f'Try another technique.')
# Store parameters
df = {}
df['rgi_id'] = gdir.rgi_id
df['bias'] = 0
df['melt_f'] = melt_f
df['prcp_fac'] = prcp_fac
df['temp_bias'] = temp_bias
# What did we try to match?
df['reference_mb'] = ref_df.values.mean()
df['reference_period'] = str(ref_mb_years)
df['rmsd'] = res.fun
# Add the climate related params to the GlacierDir to make sure
# other tools cannot fool around without re-calibration
df['mb_global_params'] = {k: gdir.settings[k] for k in MB_GLOBAL_PARAMS}
df['baseline_climate_source'] = gdir.get_climate_info(
filename=mb_mod.filename, input_filesuffix=mb_mod.input_filesuffix
)['baseline_climate_source']
# Write
if write_to_gdir:
if any(key in gdir.get_stored_settings(filesuffix=settings_filesuffix)
for key in ['melt_f', 'prcp_fac', 'temp_bias']) and not overwrite_gdir:
raise InvalidWorkflowError('Their are already mass balance parameters '
'stored in the settings file. Set '
'`overwrite_gdir` to True if you want to '
'overwrite a previous calibration.')
for key in ['rgi_id', 'bias', 'melt_f', 'prcp_fac', 'temp_bias',
'reference_mb', 'reference_period',
'mb_global_params', 'baseline_climate_source']:
gdir.settings[key] = df[key]
return df
[docs]
@entity_task(log, writes=['mb_calib'])
def mb_calibration_from_geodetic_mb(gdir, *,
settings_filesuffix='',
observations_filesuffix='',
use_observations_file=False,
ref_mb_period=None,
file_path=None,
temp_bias_file_path=None,
write_to_gdir=True,
overwrite_gdir=False,
use_regional_avg=False,
override_missing=None,
use_2d_mb=False,
informed_threestep=False,
calibrate_param1='melt_f',
calibrate_param2=None,
calibrate_param3=None,
mb_model_class=MonthlyTIModel,
**kwargs: dict,
):
"""Calibrate for geodetic MB data (from Hugonnet et al., 2021 or other).
The data table can be obtained with utils.get_geodetic_mb_dataframe().
It is equivalent to the original data from Hugonnet, but has some outlier
values filtered. See this notebook* for more details.
https://nbviewer.org/urls/cluster.klima.uni-bremen.de/~oggm/geodetic_ref_mb/convert_vold1.ipynb
This glacier-specific calibration can be replaced by a region-wide calibration
by using regional averages (same units: mm w.e.) instead of the glacier
specific averages.
The problem of calibrating many unknown parameters on geodetic data is
currently unsolved. This is OGGM's current take, based on trial and
error and based on ideas from the literature.
Parameters
----------
gdir : :py:class:`oggm.GlacierDirectory`
the glacier directory to calibrate
settings_filesuffix: str
You can use a different set of settings by providing a filesuffix. This
is useful for sensitivity experiments. Code-wise the settings_filesuffix
is set in the @entity-task decorater.
observations_filesuffix: str
The observations filesuffix, where the used calibration data will be
stored. Code-wise the observations_filesuffix is set in the @entity-task
decorater.
use_observations_file : bool
By default this function reads the data from Hugonnet and adds it to the
observations file. If you want to use different observations within this
function you can set this to True. This can be useful for sensitivity
tests. Default is False.
ref_mb_period : str, default: PARAMS['geodetic_mb_period']
one of '2000-01-01_2010-01-01', '2010-01-01_2020-01-01',
'2000-01-01_2020-01-01'. If `ref_mb` is set, this should still match
the same format but can be any date.
file_path : str, optional
path or URL to a custom geodetic mass-balance file, passed to
utils.get_geodetic_mb_dataframe.
temp_bias_file_path : str, optional
path or URL to a custom temperature-bias file, passed to
utils.get_temp_bias_dataframe. Only used with `informed_threestep`.
When set, it overrides the default w5e5/era5 file selection based on
the glacier's climate source, so it can be used together with an
arbitrary (custom) climate dataset. The file must follow the same
format as the default temp-bias files (check the format first!).
write_to_gdir : bool
whether to write the results of the calibration to the glacier
directory. If True (the default), this will be saved as `mb_calib.json`
and be used by the MassBalanceModel class as parameters in subsequent
tasks.
overwrite_gdir : bool
if a `mb_calib.json` exists, this task won't overwrite it per default.
Set this to True to enforce overwriting (i.e. with consequences for the
future workflow).
use_regional_avg : bool
use the regional average instead of the glacier specific one.
override_missing : scalar
if the reference geodetic data is not available, use this value instead
(mostly for testing with exotic datasets, but could be used to open
the door to using other datasets).
use_2d_mb : bool
Set to True if the mass balance calibration has to be done of the 2D mask
of the glacier (for fully distributed runs only).
informed_threestep : bool
the magic method Fabi found out one day before release.
Overrides the calibrate_param order below.
calibrate_param1 : str
in the three-step calibration, the name of the first parameter
to calibrate (one of 'melt_f', 'temp_bias', 'prcp_fac').
calibrate_param2 : str
in the three-step calibration, the name of the second parameter
to calibrate (one of 'melt_f', 'temp_bias', 'prcp_fac'). If not
set and the algorithm cannot match observations, it will raise an
error.
calibrate_param3 : str
in the three-step calibration, the name of the third parameter
to calibrate (one of 'melt_f', 'temp_bias', 'prcp_fac'). If not
set and the algorithm cannot match observations, it will raise an
error.
mb_model_class : MassBalanceModel, default ``MonthlyTIModel``
the MassBalanceModel to use for the calibration. Needs to use the
same parameters as MonthlyTIModel (the default): melt_f,
temp_bias, prcp_fac.
kwargs : dict
kwargs to pass to the mb_model_class instance
Returns
-------
the calibrated parameters as dict
"""
# instead of the given values by hugonnet use the once from the provided
# observations file
if use_observations_file:
ref_mb_use = gdir.observations['ref_mb']
else:
if not ref_mb_period:
ref_mb_period = gdir.settings['geodetic_mb_period']
# Get the reference data
ref_mb_err = np.nan
if use_regional_avg:
ref_mb_df_o = get_geodetic_mb_dataframe(file_path=file_path,
regional=True)
ref_mb_df = ref_mb_df_o.loc[ref_mb_df_o.period == ref_mb_period].set_index('reg')
if len(ref_mb_df) == 0:
raise InvalidParamsError(f'Ref period {ref_mb_period} not found '
f'in file: {ref_mb_df_o.period.unique()}')
# dmdtda: in meters water-equivalent per year -> we convert to kg m-2 yr-1
ref_mb = ref_mb_df.loc[int(gdir.rgi_region), 'dmdtda'] * 1000
ref_mb_err = ref_mb_df.loc[int(gdir.rgi_region), 'err_dmdtda'] * 1000
else:
try:
ref_mb_df = get_geodetic_mb_dataframe(file_path=file_path).loc[gdir.rgi_id]
ref_mb_df = ref_mb_df.loc[ref_mb_df['period'] == ref_mb_period]
# dmdtda: in meters water-equivalent per year -> we convert to kg m-2 yr-1
ref_mb = ref_mb_df['dmdtda'].iloc[0] * 1000
ref_mb_err = ref_mb_df['err_dmdtda'].iloc[0] * 1000
except KeyError:
if override_missing is None:
raise
ref_mb = override_missing
ref_mb_use = {
'value': ref_mb,
'unit': 'kg m-2 yr-1',
'period': ref_mb_period,
'err': ref_mb_err,
}
gdir.observations['ref_mb'] = ref_mb_use
temp_bias = 0
if informed_threestep:
climinfo = gdir.get_climate_info()
climsource = climinfo['baseline_climate_source']
if temp_bias_file_path is not None:
bias_df = get_temp_bias_dataframe(file_path=temp_bias_file_path,
regional=use_regional_avg)
elif 'w5e5' in climsource.lower():
bias_df = get_temp_bias_dataframe('w5e5',
rgi_version=gdir.rgi_version,
regional=use_regional_avg)
elif 'era5' in climsource.lower():
bias_df = get_temp_bias_dataframe('era5',
rgi_version=gdir.rgi_version,
regional=use_regional_avg)
else:
raise InvalidWorkflowError('Dataset not suitable for '
f'informed 3-steps: {climsource}')
ref_lon = climinfo['baseline_climate_ref_pix_lon']
ref_lat = climinfo['baseline_climate_ref_pix_lat']
# Take nearest
dis = ((bias_df.lon_val - ref_lon)**2 + (bias_df.lat_val - ref_lat)**2)**0.5
assert dis.min() < 1, 'Somethings wrong with lons'
sel_df = bias_df.iloc[np.argmin(dis)]
# Which bias central value to use?
if use_regional_avg:
centralval = 'median_temp_bias_w_area_grouped'
else:
centralval = 'median_temp_bias_w_err_grouped'
temp_bias = sel_df[centralval]
assert np.isfinite(temp_bias), 'Temp bias not finite?'
if gdir.settings['prcp_fac'] is not None:
raise InvalidParamsError('With `informed_threestep` you cannot use '
'a preset prcp_fac - we need to rely on '
'decide_winter_precip_factor().')
# Some magic heuristics - we just decide to calibrate
# precip -> melt_f -> temp but informed by previous data.
# Temp bias was decided anyway, we keep as previous value and
# allow it to vary as last resort
# We use the precip factor but allow it to vary between 0.8, 1.2 of
# the previous value (uncertainty).
prcp_fac = decide_winter_precip_factor(gdir)
mi, ma = gdir.settings['prcp_fac_min'], gdir.settings['prcp_fac_max']
prcp_fac_min = clip_scalar(prcp_fac * 0.8, mi, ma)
prcp_fac_max = clip_scalar(prcp_fac * 1.2, mi, ma)
return mb_calibration_from_scalar_mb(gdir=gdir,
settings_filesuffix=settings_filesuffix,
observations_filesuffix=observations_filesuffix,
write_to_gdir=write_to_gdir,
overwrite_gdir=overwrite_gdir,
use_2d_mb=use_2d_mb,
calibrate_param1='prcp_fac',
calibrate_param2='melt_f',
calibrate_param3='temp_bias',
prcp_fac=prcp_fac,
prcp_fac_min=prcp_fac_min,
prcp_fac_max=prcp_fac_max,
temp_bias=temp_bias,
mb_model_class=mb_model_class,
**kwargs
)
else:
return mb_calibration_from_scalar_mb(gdir=gdir,
settings_filesuffix=settings_filesuffix,
observations_filesuffix=observations_filesuffix,
write_to_gdir=write_to_gdir,
overwrite_gdir=overwrite_gdir,
use_2d_mb=use_2d_mb,
calibrate_param1=calibrate_param1,
calibrate_param2=calibrate_param2,
calibrate_param3=calibrate_param3,
temp_bias=temp_bias,
mb_model_class=mb_model_class,
**kwargs
)
def _floatyears_from_ref_mb_period(ref_mb_period):
"""Helper function to detect the time resolution required by the `mb_model`
and to provide all necessary dates in the floatyear convention.
- If the period starts on January 1st and ends on January 1st or December
31st, an annual mass balance and a yearly timeseries can be used.
- If the period starts on the first day of a month and ends on the first day
of another month or the last day of a month, a monthly mass balance and a
monthly timeseries can be used.
- If the period starts or ends on an arbitrary day, a daily mass balance and
a daily timeseries are used.
"""
date0, date1 = ref_mb_period.split('_')
y0, m0, d0 = [int(i) for i in date0.split('-')]
y1, m1, d1 = [int(i) for i in date1.split('-')]
start_date = date(y0, m0, d0)
end_date = date(y1, m1, d1)
# Check which resolution we need to use within the mb_model:
# annual: period starts 01.01.y0 and ends with 01.01.y1 (exclude y1) or
# 31.12.y1 (include y1)
if (start_date == date(y0, 1, 1) and
(end_date == date(y1, 1, 1) or end_date == date(y1, 12, 31))):
time_resolution = 'annual'
if end_date == date(y1, 1, 1):
floatyears = np.arange(y0, y1)
else:
floatyears = np.arange(y0, y1 + 1)
# monthly: period starts with 01.m0.y0 and ends with 01.m1.y1 (exclude
# m1) or with the last day of m1 (include m1)
elif (start_date.day == 1 and
(end_date.day == 1 or
end_date.day == calendar.monthrange(y1, m1)[1])):
time_resolution = 'monthly'
if end_date.day == 1:
dates = np.arange(
np.datetime64(start_date, 'M'),
np.datetime64(end_date, 'M'),
dtype='datetime64[M]'
)
else:
dates = np.arange(
np.datetime64(start_date, 'M'),
np.datetime64(end_date + timedelta(days=1), 'M'),
dtype='datetime64[M]'
)
floatyears = date_to_floatyear(
y=dates.astype('datetime64[Y]').astype(int) + 1970,
m=(dates.astype('datetime64[M]').astype(int) % 12) + 1)
# daily: all the rest, the end day is always included
else:
time_resolution = 'daily'
dates = np.arange(
np.datetime64(start_date, 'D'),
np.datetime64(end_date + timedelta(days=1), 'D'),
dtype='datetime64[D]'
)
floatyears = date_to_floatyear(
y=dates.astype('datetime64[Y]').astype(int) + 1970,
m=(dates.astype('datetime64[M]').astype(int) % 12) + 1,
d=(dates - dates.astype('datetime64[M]').astype('datetime64[D]')
).astype(int) + 1
)
return time_resolution, floatyears
[docs]
@entity_task(log, writes=['mb_calib'])
def mb_calibration_from_scalar_mb(gdir, *,
settings_filesuffix='',
observations_filesuffix='',
overwrite_observations=False,
ref_mb=None,
ref_mb_unit='kg m-2 yr-1',
ref_mb_err=None,
ref_mb_period=None,
ref_mb_years=None,
write_to_gdir=True,
overwrite_gdir=False,
use_2d_mb=False,
calibrate_param1='melt_f',
calibrate_param2=None,
calibrate_param3=None,
melt_f=None,
melt_f_min=None,
melt_f_max=None,
prcp_fac=None,
prcp_fac_min=None,
prcp_fac_max=None,
temp_bias=None,
temp_bias_min=None,
temp_bias_max=None,
mb_model_class=MonthlyTIModel,
return_mb_model=False,
**kwargs: dict,
):
"""Determine the mass balance parameters from a scalar mass-balance value.
This calibrates the mass balance parameters using reference MB data over a
given period (annual average or cumulative in-situ SMB or geodetic MB).
This flexible calibration allows to calibrate three parameters one after
another. The first parameter is varied between two chosen values (a range)
until the ref MB value is matched. If this fails, the second parameter
can be changed, etc.
This can be used for example to apply the "three-step calibration"
introduced by Huss & Hock 2015, but you can choose any order of
calibration.
This task can be called by other, "higher level" tasks, for example
:py:func:`oggm.core.massbalance.mb_calibration_from_geodetic_mb` or
:py:func:`oggm.core.massbalance.mb_calibration_from_wgms_mb`.
Note that this does not compute the apparent mass balance at
the same time - users need to run `apparent_mb_from_any_mb after`
calibration.
Parameters
----------
gdir : :py:class:`oggm.GlacierDirectory`
the glacier directory to calibrate
settings_filesuffix: str
You can use a different set of settings by providing a filesuffix. This
is useful for sensitivity experiments. Code-wise the settings_filesuffix
is set in the @entity-task decorater.
observations_filesuffix: str
You can provide a filesuffix for the mb observations to use. If you
provide ref_mb, ref_mb_err, ref_mb_period and/or ref_mb_years, then this
values will be stored in the observations file, if ref_mb is not already
present. If you want to force to use the provided values and override
the current ones, set overwrite_observations to True. Code-wise the
observations_filesuffix is set in the @entity-task decorater.
overwrite_observations : bool
If you want to overwrite already existing observation values in the
provided observations file set this to True. Default is False.
ref_mb : float, required
The reference mass balance to match, either provided as an annual
average (kg m-2 yr-1) or as a cumulative value (kg m-2) over the
provided ref_mb_period. The correct unit must be set in ref_mb_unit.
To use available observations, see
:py:func:`oggm.core.massbalance.mb_calibration_from_geodetic_mb` or
:py:func:`oggm.core.massbalance.mb_calibration_from_wgms_mb`.
ref_mb_unit : str, optional
The unit of ref_mb`. Options are:
- 'kg m-2 yr-1': annual average MB over a full-year ref_mb_period.
- 'kg m-2': cumulative MB over any ref_mb_period.
ref_mb_err : float, optional
currently only used for logging - it is not used in the calibration.
ref_mb_period : str, optional
date format - for example '2000-01-01_2010-01-01'. If this is not
set, ref_mb_years needs to be set.
ref_mb_years : tuple of length 2 (range) or list of years.
convenience kwarg to override ref_mb_period. If a tuple of length 2 is
given, all years between this range (excluding the last one) are used.
If a list of years is given, all these will be used (useful for
data with gaps)
write_to_gdir : bool
whether to write the results of the calibration to the glacier
directory. If True (the default), this will be saved as `mb_calib.json`
and be used by the MassBalanceModel class as parameters in subsequent
tasks.
overwrite_gdir : bool
if mass balance parameters exists, this task won't overwrite it per
default. Set this to True to enforce overwriting (i.e. with consequences
for the future workflow).
use_2d_mb : bool
Set to True if the mass balance calibration has to be done of the 2D mask
of the glacier (for fully distributed runs only).
mb_model_class : MassBalanceModel, default ``MonthlyTIModel``
the MassBalanceModel to use for the calibration. Needs to use the
same parameters as MonthlyTIModel (the default): melt_f,
temp_bias, prcp_fac.
calibrate_param1 : str
in the three-step calibration, the name of the first parameter
to calibrate (one of 'melt_f', 'temp_bias', 'prcp_fac').
calibrate_param2 : str
in the three-step calibration, the name of the second parameter
to calibrate (one of 'melt_f', 'temp_bias', 'prcp_fac'). If not
set and the algorithm cannot match observations, it will raise an
error.
calibrate_param3 : str
in the three-step calibration, the name of the third parameter
to calibrate (one of 'melt_f', 'temp_bias', 'prcp_fac'). If not
set and the algorithm cannot match observations, it will raise an
error.
melt_f: float
the default value to use as melt factor (or the starting value when
optimizing MB). Defaults to gdir.settings['melt_f'].
melt_f_min: float
the minimum accepted value for the melt factor during optimisation.
Defaults to gdir.settings['melt_f_min'].
melt_f_max: float
the maximum accepted value for the melt factor during optimisation.
Defaults to gdir.settings['melt_f_max'].
prcp_fac: float
the default value to use as precipitation scaling factor
(or the starting value when optimizing MB). Defaults to the method
chosen in `params.cfg` (winter prcp or global factor).
prcp_fac_min: float
the minimum accepted value for the precipitation scaling factor during
optimisation. Defaults to gdir.settings['prcp_fac_min'].
prcp_fac_max: float
the maximum accepted value for the precipitation scaling factor during
optimisation. Defaults to gdir.settings['prcp_fac_max'].
temp_bias: float
the default value to use as temperature bias (or the starting value when
optimizing MB). Defaults to 0.
temp_bias_min: float
the minimum accepted value for the temperature bias during optimisation.
Defaults to gdir.settings['temp_bias_min'].
temp_bias_max: float
the maximum accepted value for the temperature bias during optimisation.
Defaults to gdir.settings['temp_bias_max'].
return_mb_model: bool
if the finally calibrated mb_model should be returned. Useful for
testing mb_models with a memory (e.g. SfcTypeTIModel). Default is False.
kwargs: dict
kwargs to pass to the mb_model_class instance
"""
# Param constraints
if melt_f_min is None:
melt_f_min = gdir.settings['melt_f_min']
if melt_f_max is None:
melt_f_max = gdir.settings['melt_f_max']
if prcp_fac_min is None:
prcp_fac_min = gdir.settings['prcp_fac_min']
if prcp_fac_max is None:
prcp_fac_max = gdir.settings['prcp_fac_max']
if temp_bias_min is None:
temp_bias_min = gdir.settings['temp_bias_min']
if temp_bias_max is None:
temp_bias_max = gdir.settings['temp_bias_max']
if ref_mb_years is not None and ref_mb_period is not None:
raise InvalidParamsError('Cannot set `ref_mb_years` and `ref_mb_period` '
'at the same time.')
if not use_2d_mb:
fls = gdir.read_pickle('inversion_flowlines')
else:
# if the 2D data is used, the flowline is not needed.
fls = None
# get the 2D data
fp = gdir.get_filepath('gridded_data')
with xr.open_dataset(fp) as ds:
# 'topo' instead of 'topo_smoothed'?
heights = ds.topo_smoothed.data[ds.glacier_mask.data == 1]
widths = np.ones(len(heights))
# handle which ref mb to use (provided or in observations file)
ref_mb_provided = {}
if ref_mb is not None:
ref_mb_provided['value'] = ref_mb
if ref_mb_unit is not None:
ref_mb_provided['unit'] = ref_mb_unit
if ref_mb_err is not None:
ref_mb_provided['err'] = ref_mb_err
if ref_mb_period is not None:
ref_mb_provided['period'] = ref_mb_period
if ref_mb_years is not None:
ref_mb_provided['years'] = ref_mb_years
if 'ref_mb' in gdir.observations:
ref_mb_in_file = gdir.observations['ref_mb']
else:
ref_mb_in_file = None
# if nothing is provided raise an error, 'unit' has a default value
if (set(ref_mb_provided.keys()) == {"unit"}) and (ref_mb_in_file is None):
raise InvalidWorkflowError(
'You have not provided an reference mass balance! Either add it to '
'the observations file '
f'({os.path.basename(gdir.observations.path)}), or pass it through '
f'kwargs (ref_mb, ref_mb_err, ref_mb_period/ref_mb_years.')
# here handle different cases of provided values for the ref mb
if (ref_mb_in_file is None) or (ref_mb_in_file is not None and
overwrite_observations):
gdir.observations['ref_mb'] = ref_mb_provided
ref_mb_use = ref_mb_provided
elif ref_mb_in_file is not None and set(ref_mb_provided.keys()) == {"unit"}:
# only provided in file, this is ok so continue
ref_mb_use = ref_mb_in_file
else:
# if the provided is the same as the one stored in the file it is fine
if ref_mb_in_file != ref_mb_provided:
raise InvalidWorkflowError(
'You provided a reference mass balance, but their is already '
'one stored in the current observation file '
f'({os.path.basename(gdir.observations.path)}). If you want to '
'overwrite set overwrite_observations = True.')
else:
ref_mb_use = ref_mb_in_file
# now we can extract the actual values we want to use
ref_mb = ref_mb_use['value']
if 'unit' in ref_mb_use:
ref_mb_unit = ref_mb_use['unit']
if 'err' in ref_mb_use:
ref_mb_err = ref_mb_use['err']
if 'period' in ref_mb_use:
ref_mb_period = ref_mb_use['period']
if 'years' in ref_mb_use:
ref_mb_years = ref_mb_use['years']
# Let's go
# Climate period
if ref_mb_years is not None:
time_resolution = 'annual'
if len(ref_mb_years) > 2:
years = np.asarray(ref_mb_years)
ref_mb_period = 'custom'
else:
years = np.arange(*ref_mb_years)
ref_mb_period = f'{ref_mb_years[0]}-01-01_{ref_mb_years[1]}-01-01'
gdir.observations['ref_mb']['period'] = ref_mb_period
elif ref_mb_period is not None:
time_resolution, years = _floatyears_from_ref_mb_period(ref_mb_period)
else:
raise InvalidParamsError('One of `ref_mb_years` or `ref_mb_period` '
'is required for calibration.')
# check that ref_mb_unit fits to time_resolution
if time_resolution != 'annual' and ref_mb_unit == 'kg m-2 yr-1':
raise InvalidParamsError(
"When the reference mass balance period does not correspond to full "
f"calendar years (your provided `ref_mb_period` is {ref_mb_period}), "
"the mass balance must be provided in 'kg m-2' instead of"
"'kg m-2 yr-1'. Please set the correct unit using the `ref_mb_unit` "
"parameter and make sure `ref_mb` is provided correctly.")
# Do we have a calving glacier?
cmb = calving_mb(gdir)
if cmb != 0:
raise NotImplementedError('Calving with geodetic MB is not implemented '
'yet, but it should actually work. Well keep '
'you posted!')
# Ok, regardless on how we want to calibrate, we start with defaults
if melt_f is None:
melt_f = gdir.settings['melt_f']
if prcp_fac is None:
if gdir.settings['prcp_fac'] is None:
prcp_fac = decide_winter_precip_factor(gdir)
else:
prcp_fac = gdir.settings['prcp_fac']
if temp_bias is None:
try:
# see if a default temp bias is available
temp_bias = gdir.settings['temp_bias']
except KeyError:
# if no default available set to 0
temp_bias = 0
# Create the MB model we will calibrate
if fls is not None and len(fls) > 1:
# one mb model instance per flowline, needed for mb_model_class
# models that need to know the flowline length at initialisation
# (e.g. SfcTypeTIModel) whenever the glacier has more than one
# flowline
mb_mod = MultipleFlowlineMassBalance(
gdir=gdir,
fls=fls,
mb_model_class=mb_model_class,
melt_f=melt_f,
temp_bias=temp_bias,
prcp_fac=prcp_fac,
check_calib_params=False,
settings_filesuffix=settings_filesuffix,
**kwargs
)
else:
mb_mod = mb_model_class(
gdir=gdir,
melt_f=melt_f,
temp_bias=temp_bias,
prcp_fac=prcp_fac,
check_calib_params=False,
settings_filesuffix=settings_filesuffix,
**kwargs
)
# Check that the years are available
for y in years:
if not mb_mod.is_year_valid(y):
raise ValueError(f'year {y} out of the valid time bounds: '
f'[{mb_mod.ys_float}, {mb_mod.ye_float}]')
if calibrate_param1 == 'melt_f':
min_range, max_range = melt_f_min, melt_f_max
elif calibrate_param1 == 'prcp_fac':
min_range, max_range = prcp_fac_min, prcp_fac_max
elif calibrate_param1 == 'temp_bias':
min_range, max_range = temp_bias_min, temp_bias_max
else:
raise InvalidParamsError("calibrate_param1 must be one of "
"['melt_f', 'prcp_fac', 'temp_bias']")
def to_minimize(x, model_attr):
# Set the new attr value
setattr(mb_mod, model_attr, x)
if use_2d_mb:
out = mb_mod.get_specific_mb(
heights=heights, widths=widths, year=years,
time_resolution=time_resolution)
else:
out = mb_mod.get_specific_mb(
fls=fls, year=years, time_resolution=time_resolution)
if ref_mb_unit == 'kg m-2 yr-1':
out = out.mean()
elif ref_mb_unit == 'kg m-2':
out = out.sum()
else:
raise NotImplementedError(
f"ref_mb_unit '{ref_mb_unit}' not implemented. Options are "
f"'kg m-2 yr-1' or 'kg m-2'.")
return np.mean(out - ref_mb)
try:
optim_param1 = optimize.brentq(to_minimize,
min_range, max_range,
args=(calibrate_param1,)
)
except ValueError:
if not calibrate_param2:
raise RuntimeError(f'{gdir.rgi_id}: ref mb not matched. '
f'Try to set calibrate_param2.')
# Check which direction we need to go
diff_1 = to_minimize(min_range, calibrate_param1)
diff_2 = to_minimize(max_range, calibrate_param1)
optim_param1 = min_range if abs(diff_1) < abs(diff_2) else max_range
setattr(mb_mod, calibrate_param1, optim_param1)
# Second step
if calibrate_param2 == 'melt_f':
min_range, max_range = melt_f_min, melt_f_max
elif calibrate_param2 == 'prcp_fac':
min_range, max_range = prcp_fac_min, prcp_fac_max
elif calibrate_param2 == 'temp_bias':
min_range, max_range = temp_bias_min, temp_bias_max
else:
raise InvalidParamsError("calibrate_param2 must be one of "
"['melt_f', 'prcp_fac', 'temp_bias']")
try:
optim_param2 = optimize.brentq(to_minimize,
min_range, max_range,
args=(calibrate_param2,)
)
except ValueError:
# Third step
if not calibrate_param3:
raise RuntimeError(f'{gdir.rgi_id}: ref mb not matched. '
f'Try to set calibrate_param3.')
# Check which direction we need to go
diff_1 = to_minimize(min_range, calibrate_param2)
diff_2 = to_minimize(max_range, calibrate_param2)
optim_param2 = min_range if abs(diff_1) < abs(diff_2) else max_range
setattr(mb_mod, calibrate_param2, optim_param2)
# Third step
if calibrate_param3 == 'melt_f':
min_range, max_range = melt_f_min, melt_f_max
elif calibrate_param3 == 'prcp_fac':
min_range, max_range = prcp_fac_min, prcp_fac_max
elif calibrate_param3 == 'temp_bias':
min_range, max_range = temp_bias_min, temp_bias_max
else:
raise InvalidParamsError("calibrate_param3 must be one of "
"['melt_f', 'prcp_fac', 'temp_bias']")
try:
optim_param3 = optimize.brentq(to_minimize,
min_range, max_range,
args=(calibrate_param3,)
)
except ValueError:
raise RuntimeError(f'{gdir.rgi_id}: we tried very hard but we '
f'could not find a combination of '
f'parameters that works for this ref mb.')
if calibrate_param3 == 'melt_f':
melt_f = optim_param3
elif calibrate_param3 == 'prcp_fac':
prcp_fac = optim_param3
elif calibrate_param3 == 'temp_bias':
temp_bias = optim_param3
if calibrate_param2 == 'melt_f':
melt_f = optim_param2
elif calibrate_param2 == 'prcp_fac':
prcp_fac = optim_param2
elif calibrate_param2 == 'temp_bias':
temp_bias = optim_param2
if calibrate_param1 == 'melt_f':
melt_f = optim_param1
elif calibrate_param1 == 'prcp_fac':
prcp_fac = optim_param1
elif calibrate_param1 == 'temp_bias':
temp_bias = optim_param1
# Store parameters
df = {}
df['rgi_id'] = gdir.rgi_id
df['bias'] = 0
df['melt_f'] = melt_f
df['prcp_fac'] = prcp_fac
df['temp_bias'] = temp_bias
# What did we try to match?
df['reference_mb'] = ref_mb
df['reference_mb_err'] = ref_mb_err
df['reference_period'] = ref_mb_period
# Add the climate related params to the GlacierDir to make sure
# other tools cannot fool around without re-calibration
df['mb_global_params'] = {k: gdir.settings[k] for k in MB_GLOBAL_PARAMS}
df['baseline_climate_source'] = gdir.get_climate_info(
filename=mb_mod.filename, input_filesuffix=mb_mod.input_filesuffix
)['baseline_climate_source']
# Write
if write_to_gdir:
if any(key in gdir.get_stored_settings(filesuffix=settings_filesuffix)
for key in ['melt_f', 'prcp_fac', 'temp_bias']) and not overwrite_gdir:
raise InvalidWorkflowError('Their are already mass balance parameters '
'stored in the settings file. Set '
'`overwrite_gdir` to True if you want to '
'overwrite a previous calibration.')
for key in ['rgi_id', 'bias', 'melt_f', 'prcp_fac', 'temp_bias',
'reference_mb', 'reference_mb_err', 'reference_period',
'mb_global_params', 'baseline_climate_source']:
gdir.settings[key] = df[key]
if return_mb_model:
return df, mb_mod
return df
[docs]
@entity_task(log, writes=['mb_calib'])
def perturbate_mb_params(gdir, input_filesuffix='', perturbation=None,
reset_default=False, output_filesuffix=''):
"""Replaces pre-calibrated MB params with perturbed ones for this glacier.
It simply replaces the existing `mb_calib.json` file with an
updated one with perturbed parameters. The original ones
are stored in the file for re-use after perturbation.
Users can change the following 4 parameters:
- **melt_f': unit [kg m-2 day-1 K-1], the melt factor.
- **prcp_fac': unit [-], the precipitation factor.
- **temp_bias': unit [K], the temperature correction applied to the timeseries.
- **bias': unit [mm we yr-1], *subtracted* from the computed MB. Rarely used.
All parameter perturbations are additive, i.e. the value
provided by the user is added to the *precalibrated* value.
For example, `temp_bias=1` means that the temp_bias used by the
model will be the precalibrated one, plus 1 Kelvin.
The only exception is prpc_fac, which is multiplicative.
For example prcp_fac=1 will leave the precalibrated prcp_fac unchanged,
while 2 will double it.
Parameters
----------
gdir : GlacierDirectory
The glacier directory.
perturbation : dict
The parameters to change and the associated value (see doc above)
reset_default : bool, default False
Reset the parameters to their original value. This might be
unnecessary if using the filesuffix mechanism.
filesuffix : str, optional
Write the modified parameters in a separate mb_calib.json file
with the filesuffix appended. This can then be read by the
MassBalanceModel for example instead of the default one.
Note that it's always the default, precalibrated params
file which is read to start with.
"""
df = gdir.read_yml('settings', filesuffix=input_filesuffix)
# Save original params if not there
if 'bias_orig' not in df:
for k in ['bias', 'melt_f', 'prcp_fac', 'temp_bias']:
df[k + '_orig'] = df[k]
if reset_default:
for k in ['bias', 'melt_f', 'prcp_fac', 'temp_bias']:
df[k] = df[k + '_orig']
gdir.write_yml(df, 'settings', filesuffix=output_filesuffix)
return df
for k, v in perturbation.items():
if k == 'prcp_fac':
df[k] = df[k + '_orig'] * v
elif k in ['bias', 'melt_f', 'temp_bias']:
df[k] = df[k + '_orig'] + v
else:
raise InvalidParamsError(f'Perturbation not valid: {k}')
gdir.write_yml(df, 'settings', filesuffix=output_filesuffix)
return df
def _check_terminus_mass_flux(gdir, fls):
# Check that we have done this correctly
rho = gdir.settings['ice_density']
cmb = calving_mb(gdir)
# This variable is in "sensible" units normalized by width
flux = fls[-1].flux_out
aflux = flux * (gdir.grid.dx ** 2) / rho * 1e-9 # km3 ice per year
# If not marine and a bit far from zero, warning
if cmb == 0 and not np.allclose(flux, 0, atol=0.01):
log.info('(%s) flux should be zero, but is: '
'%.4f km3 ice yr-1', gdir.rgi_id, aflux)
# If not marine and quite far from zero, error
if cmb == 0 and not np.allclose(flux, 0, atol=1):
msg = ('({}) flux should be zero, but is: {:.4f} km3 ice yr-1'
.format(gdir.rgi_id, aflux))
raise MassBalanceCalibrationError(msg)
[docs]
@entity_task(log, writes=['inversion_flowlines', 'linear_mb_params'])
def apparent_mb_from_linear_mb(gdir, settings_filesuffix:str='',
mb_gradient=3., ela_h=None):
"""Compute apparent mb from a linear mass balance assumption (for testing).
This is for testing currently, but could be used as alternative method
for the inversion quite easily.
Parameters
----------
gdir : GlacierDirectory
The glacier directory to process.
settings_filesuffix : str, optional
You can use a different set of settings by providing a
filesuffix. This is useful for sensitivity experiments.
If not given, ``settings_filesuffix`` is set by the
``@entity-task`` decorator.
"""
# Do we have a calving glacier?
cmb = calving_mb(gdir)
is_calving = cmb != 0.
# Get the height and widths along the fls
h, w = gdir.get_inversion_flowline_hw()
# Now find the ELA till the integrated mb is zero
from oggm.core.massbalance import LinearMassBalance
def to_minimize(ela_h):
mbmod = LinearMassBalance(ela_h, grad=mb_gradient)
smb = mbmod.get_specific_mb(heights=h, widths=w)
return smb - cmb
if ela_h is None:
ela_h = optimize.brentq(to_minimize, -1e5, 1e5)
# For each flowline compute the apparent MB
rho = gdir.settings['ice_density']
fls = gdir.read_pickle('inversion_flowlines')
# Reset flux
for fl in fls:
fl.flux = np.zeros(len(fl.surface_h))
# Flowlines in order to be sure
mbmod = LinearMassBalance(ela_h, grad=mb_gradient)
for fl in fls:
mbz = mbmod.get_annual_mb(fl.surface_h) * cfg.SEC_IN_YEAR * rho
fl.set_apparent_mb(mbz, is_calving=is_calving)
# Check and write
_check_terminus_mass_flux(gdir, fls)
gdir.write_pickle(fls, 'inversion_flowlines')
gdir.write_pickle({'ela_h': ela_h, 'grad': mb_gradient},
'linear_mb_params')
[docs]
@entity_task(log, writes=['inversion_flowlines'])
def apparent_mb_from_any_mb(gdir, settings_filesuffix='',
input_filesuffix='',
output_filesuffix=None,
mb_model=None,
mb_model_class=None,
mb_years=None,
include_mb_model_heights=True,
):
"""Compute apparent mb from an arbitrary mass balance profile.
This searches for a mass balance residual to add to the mass balance
profile so that the average specific MB is zero.
Parameters
----------
gdir : :py:class:`oggm.GlacierDirectory`
The glacier directory to process.
settings_filesuffix : str, optional
You can use a different set of settings by providing a
filesuffix. This is useful for sensitivity experiments.
If not given, ``settings_filesuffix`` is set by the
``@entity-task`` decorator.
input_filesuffix: str
the filesuffix of the inversion flowlines which should be used (useful
for conducting multiple experiments in the same gdir). By default we use
'', to start the workflow after geometrically constructing the inversion
flowlines.
output_filesuffix: str
the filesuffix of the final inversion flowlines which are saved back
into the gdir (useful for conducting multiple experiments in the same
gdir)
mb_model : :py:class:`oggm.core.massbalance.MassBalanceModel`
the mass balance model to use - if None, will use the
one given by mb_model_class.
mb_model_class : MassBalanceModel, default ``MonthlyTIModel``
The MassBalanceModel class to use.
mb_years : array_like or tuple, default None
The array of years over which you want to average the mass
balance. This argument has little effect on the final result,
but it should be a period long enough to have a representative
mass balance gradient.
If an array of length 2 is given, the method uses all years
between this range, excluding the last one.
If None, the method will use all the years from the reference
geodetic mass balance period ``gdir.settings['geodetic_mb_period']``.
include_mb_model_heights : bool, default True
If True we add the snow/firn height of the current mb_model (if
available) to include this in the elevation feedback of the mb
calculation.
"""
if input_filesuffix is None:
input_filesuffix = settings_filesuffix
if output_filesuffix is None:
output_filesuffix = settings_filesuffix
if mb_model_class is None:
mb_model_class = MonthlyTIModel
# Do we have a calving glacier?
cmb = calving_mb(gdir)
is_calving = cmb != 0
# For each flowline compute the apparent MB
fls = gdir.read_pickle('inversion_flowlines', filesuffix=input_filesuffix)
if mb_model is None:
mb_model = MultipleFlowlineMassBalance(
gdir, settings_filesuffix=settings_filesuffix,
fls=fls, mb_model_class=mb_model_class,
)
if mb_years is None:
mb_years = gdir.settings['geodetic_mb_period']
y0, y1 = mb_years.split('_')
y0 = int(y0.split('-')[0])
y1 = int(y1.split('-')[0])
mb_years = np.arange(y0, y1, 1)
if len(mb_years) == 2:
# Range
mb_years = np.arange(*mb_years, 1)
# Unchanged SMB
rho = gdir.settings['ice_density']
mb_on_fl = []
spec_mb = []
area_smb = []
for fl_id, fl in enumerate(fls):
widths = fl.widths
try:
# For rect and parabola don't compute spec mb
widths = np.where(fl.thick > 0, widths, 0)
except AttributeError:
pass
mbz = 0
smb = 0
for yr in mb_years:
# some models have a climatic and ice mb (e.g. SfcTIModel), for
# inversion we use climatic; if not available it is ignored;
# Similarly some models include a bucket height, and we can add this
# height on top to the ice surface height
amb = mb_model.get_annual_mb(fl.surface_h, fls=fls, fl_id=fl_id, year=yr,
climatic_mb_or_ice_mb='climatic_mb',
include_mb_model_heights=include_mb_model_heights
)
amb *= mb_model.sec_in_year(year=yr) * rho
mbz += amb
smb += weighted_average_1d(amb, widths)
mb_on_fl.append(mbz / len(mb_years))
spec_mb.append(smb / len(mb_years))
area_smb.append(np.sum(widths))
if len(mb_on_fl) == 1:
o_smb = spec_mb[0]
else:
o_smb = weighted_average_1d(spec_mb, area_smb)
def to_minimize(residual_to_opt):
return o_smb + residual_to_opt - cmb
residual = optimize.brentq(to_minimize, -1e5, 1e5)
# Reset flux
for fl in fls:
fl.reset_flux()
# Flowlines in order to be sure
for fl_id, (fl, mbz) in enumerate(zip(fls, mb_on_fl)):
fl.set_apparent_mb(mbz + residual, is_calving=is_calving)
if fl_id < len(fls) and fl.flux_out < -1e3:
log.warning('({}) a tributary has a strongly negative flux. '
'Inversion works but is physically quite '
'questionable.'.format(gdir.rgi_id))
# Check and write
_check_terminus_mass_flux(gdir, fls)
gdir.settings['apparent_mb_from_any_mb_residual'] = residual
# this is only for backwards compatibility
if settings_filesuffix == '':
gdir.add_to_diagnostics('apparent_mb_from_any_mb_residual', residual)
gdir.write_pickle(fls, 'inversion_flowlines', filesuffix=output_filesuffix)
[docs]
@entity_task(log)
def fixed_geometry_mass_balance(gdir, settings_filesuffix='',
ys=None, ye=None, years=None,
monthly_step=False,
use_inversion_flowlines=True,
climate_filename='climate_historical',
climate_input_filesuffix='',
temperature_bias=None,
precipitation_factor=None,
mb_model_class=None):
"""Computes the mass balance with climate input from e.g. CRU or a GCM.
Parameters
----------
gdir : GlacierDirectory
The glacier directory to process.
settings_filesuffix : str, optional
You can use a different set of settings by providing a filesuffix. This
is useful for sensitivity experiments. Code-wise the settings_filesuffix
is set in the @entity-task decorator.
ys : int, optional
Start year of the model run (default: from the climate file).
date)
ye : int, optional
End year of the model run (default: from the climate file).
years : array_like[int], optional
Override ``ys`` and ``ye`` with the years of your choice.
monthly_step : bool, default False
Store the diagnostic data at a monthly time resolution.
If False, stores it at an annual resolution.
use_inversion_flowlines : bool, default True
Use the inversion flowlines instead of the model flowlines.
climate_filename : str, default 'climate_historical'
Name of the climate file, e.g. 'climate_historical', 'gcm_data'.
climate_input_filesuffix: str, optional
Filesuffix for the input climate file.
temperature_bias : float, optional
Add a bias to the temperature timeseries.
precipitation_factor: float, optional
Multiplicative factor applied to the precipitation time series.
If None, uses the precipitation factor from the calibration in
``gdir.settings['prcp_fac']``.
mb_model_class : MassBalanceModel, defaults to ``MonthlyTIModel``
The MassBalanceModel class to use.
"""
if monthly_step:
raise NotImplementedError('monthly_step not implemented yet')
if mb_model_class is None:
mb_model_class = MonthlyTIModel
mbmod = MultipleFlowlineMassBalance(
gdir=gdir,
mb_model_class=mb_model_class,
filename=climate_filename,
use_inversion_flowlines=use_inversion_flowlines,
input_filesuffix=climate_input_filesuffix,
settings_filesuffix=settings_filesuffix,
)
if temperature_bias is not None:
mbmod.temp_bias = temperature_bias
if precipitation_factor is not None:
mbmod.prcp_fac = precipitation_factor
if years is None:
if ys is None:
ys = mbmod.flowline_mb_models[0].ys
if ys is None:
if isinstance(mbmod.flowline_mb_models[0], SfcTypeTIModel):
# for SfcTypeTIModel we need go one layer deeper
sfc_mbod = mbmod.flowline_mb_models[0]
ys = sfc_mbod.mbmod.ys + sfc_mbod.spinup_years
if ye is None:
ye = mbmod.flowline_mb_models[0].ye
years = np.arange(ys, ye + 1)
odf = pd.Series(data=mbmod.get_specific_mb(year=years), index=years)
return odf
[docs]
@entity_task(log)
def compute_ela(gdir, settings_filesuffix: str='',
ys=None, ye=None, years=None, climate_filename='climate_historical',
temperature_bias=None, precipitation_factor=None, climate_input_filesuffix='',
mb_model_class=MonthlyTIModel):
"""Computes the ELA of a glacier for specific years and climate.
Parameters
----------
gdir : GlacierDirectory
The glacier directory to process
settings_filesuffix : str, optional
You can use a different set of settings by providing a
filesuffix. This is useful for sensitivity experiments.
If not given, ``settings_filesuffix`` is set by the
``@entity-task`` decorator.
ys : int, optional
Start year.
ye : int, optional
End year.
years : array_like[int], optional
Override ``ys`` and ``ye`` with the years of your choice.
climate_filename : str, default 'climate_historical'
Name of the climate file, e.g. 'climate_historical', 'gcm_data'.
climate_input_filesuffix: str, optional
Filesuffix for the input climate file.
temperature_bias : float, optional
Add a bias to the temperature timeseries.
precipitation_factor: float, optional
Multiplicative factor applied to the precipitation time series.
If None, uses the precipitation factor from the calibration in
``gdir.settings['prcp_fac']``.
mb_model_class : MassBalanceModel, default ``MonthlyTIModel``
The MassBalanceModel class to use.
"""
mbmod = mb_model_class(
gdir=gdir,
settings_filesuffix=settings_filesuffix,
filename=climate_filename,
input_filesuffix=climate_input_filesuffix,
)
if temperature_bias is not None:
mbmod.temp_bias = temperature_bias
if precipitation_factor is not None:
mbmod.prcp_fac = precipitation_factor
mbmod.valid_bounds = [-10000, 20000]
if years is None:
years = np.arange(ys, ye+1)
ela = []
for yr in years:
ela.append(mbmod.get_ela(year=yr))
odf = pd.Series(data=ela, index=years)
return odf