• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

OGGM / oggm / 17036427554

18 Aug 2025 09:19AM UTC coverage: 84.387% (-0.03%) from 84.412%
17036427554

Pull #1795

github

web-flow
Merge f58c0836a into 0793d4813
Pull Request #1795: add new observation handling

208 of 217 new or added lines in 7 files covered. (95.85%)

28 existing lines in 2 files now uncovered.

12404 of 14699 relevant lines covered (84.39%)

3.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.25
/oggm/core/massbalance.py
1
"""Mass balance models - next generation"""
2

3
# Built ins
4
import logging
9✔
5
import os
9✔
6
# External libs
7
import cftime
9✔
8
import numpy as np
9✔
9
import xarray as xr
9✔
10
import pandas as pd
9✔
11
from scipy.interpolate import interp1d
9✔
12
from scipy import optimize
9✔
13
# Locals
14
import oggm.cfg as cfg
9✔
15
from oggm.cfg import SEC_IN_YEAR, SEC_IN_MONTH
9✔
16
from oggm.utils import (SuperclassMeta, get_geodetic_mb_dataframe,
9✔
17
                        floatyear_to_date, date_to_floatyear, get_demo_file,
18
                        monthly_timeseries, ncDataset, get_temp_bias_dataframe,
19
                        clip_min, clip_max, clip_array, clip_scalar,
20
                        weighted_average_1d, lazy_property, set_array_type)
21
from oggm.exceptions import (InvalidWorkflowError, InvalidParamsError,
9✔
22
                             MassBalanceCalibrationError)
23
from oggm import entity_task
9✔
24

25
# Module logger
26
log = logging.getLogger(__name__)
9✔
27

28
# Climate relevant global params - not optimised
29
MB_GLOBAL_PARAMS = ['temp_default_gradient',
9✔
30
                    'temp_all_solid',
31
                    'temp_all_liq',
32
                    'temp_melt']
33

34

35
class MassBalanceModel(object, metaclass=SuperclassMeta):
9✔
36
    """Interface and common logic for all mass balance models used in OGGM.
37

38
    All mass balance models should implement this interface.
39

40
    Attributes
41
    ----------
42
    valid_bounds : [float, float]
43
        The altitudinal bounds where the MassBalanceModel is valid. This is
44
        necessary for automated ELA search.
45
    hemisphere : str, {'nh', 'sh'}
46
        Used for certain methods - if the hydrological year is requested.
47
    rho : float, default: ``cfg.PARAMS['ice_density']``
48
        Density of ice
49
    """
50

51
    def __init__(self, gdir=None):
9✔
52
        """ Initialize."""
53
        self.valid_bounds = None
9✔
54
        self.hemisphere = None
9✔
55
        if gdir is None:
9✔
56
            self.rho = cfg.PARAMS['ice_density']
9✔
57
        else:
58
            self.rho = gdir.settings['ice_density']
7✔
59

60
    def __repr__(self):
61
        """String Representation of the mass balance model"""
62
        summary = ['<oggm.MassBalanceModel>']
63
        summary += ['  Class: ' + self.__class__.__name__]
64
        summary += ['  Attributes:']
65
        # Add all scalar attributes
66
        for k, v in self.__dict__.items():
67
            if np.isscalar(v) and not k.startswith('_'):
68
                nbform = '    - {}: {}'
69
                summary += [nbform.format(k, v)]
70
        return '\n'.join(summary) + '\n'
71

72
    def reset_state(self):
9✔
73
        """Reset any internal state of the model.
74
        This might not be needed by most models, but some models have an
75
        internal state (e.g. a snow cover history) which can be reset
76
        this way.
77
        """
78
        pass
×
79

80
    def get_monthly_mb(self, heights, year=None, fl_id=None, fls=None):
9✔
81
        """Monthly mass balance at given altitude(s) for a moment in time.
82

83
        Units: [m s-1], or meters of ice per second
84

85
        Note: `year` is optional because some simpler models have no time
86
        component.
87

88
        Parameters
89
        ----------
90
        heights: ndarray
91
            the atitudes at which the mass balance will be computed
92
        year: float, optional
93
            the time (in the "floating year" convention)
94
        fl_id: float, optional
95
            the index of the flowline in the fls array (might be ignored
96
            by some MB models)
97
        fls: list of flowline instances, optional
98
            the flowlines array, in case the MB model implementation needs
99
            to know details about the glacier geometry at the moment the
100
            MB model is called
101

102
        Returns
103
        -------
104
        the mass balance (same dim as `heights`) (units: [m s-1])
105
        """
106
        raise NotImplementedError()
107

108
    def get_annual_mb(self, heights, year=None, fl_id=None, fls=None):
9✔
109
        """Like `self.get_monthly_mb()`, but for annual MB.
110

111
        For some simpler mass balance models ``get_monthly_mb()` and
112
        `get_annual_mb()`` can be equivalent.
113

114
        Units: [m s-1], or meters of ice per second
115

116
        Note: `year` is optional because some simpler models have no time
117
        component.
118

119
        Parameters
120
        ----------
121
        heights: ndarray
122
            the altitudes at which the mass balance will be computed
123
        year: float, optional
124
            the time (in the "floating year" convention)
125
        fl_id: float, optional
126
            the index of the flowline in the fls array (might be ignored
127
            by some MB models)
128
        fls: list of flowline instances, optional
129
            the flowlines array, in case the MB model implementation needs
130
            to know details about the glacier geometry at the moment the
131
            MB model is called
132

133
        Returns
134
        -------
135
        the mass balance (same dim as `heights`) (units: [m s-1])
136
        """
137
        raise NotImplementedError()
138

139
    def get_annual_specific_mass_balance(self, fls: list, year: float) -> float:
9✔
140
        """Get annual specific mass balance from multiple flowlines.
141

142
        Parameters
143
        ----------
144
        fls : list[oggm.Flowline]
145
            Flowline instances.
146
        year: float
147
            Time in "floating year" convention.
148

149
        Returns
150
        -------
151
        float
152
            Annual specific mass balance from multiple flowlines.
153
        """
154
        mbs = []
8✔
155
        widths = []
8✔
156
        for i, fl in enumerate(fls):
8✔
157
            _widths = fl.widths
8✔
158
            try:
8✔
159
                # For rect and parabola don't compute spec mb
160
                _widths = np.where(fl.thick > 0, _widths, 0)
8✔
161
            except AttributeError:
7✔
162
                pass
7✔
163
            widths.append(_widths)
8✔
164
            mbs.append(
8✔
165
                self.get_annual_mb(fl.surface_h, fls=fls, fl_id=i, year=year)
166
            )
167
        widths = np.concatenate(widths, axis=0)  # 2x faster than np.append
8✔
168
        mbs = np.concatenate(mbs, axis=0)
8✔
169
        mbs = weighted_average_1d(mbs, widths)
8✔
170

171
        return mbs
8✔
172

173
    def get_specific_mb(self, heights=None, widths=None, fls=None, year=None):
9✔
174
        """Specific mass balance for a given glacier geometry.
175

176
        Units: [mm w.e. yr-1], or millimeter water equivalent per year.
177

178
        Parameters
179
        ----------
180
        heights : ArrayLike, default None
181
            Altitudes at which the mass balance will be computed.
182
            Overridden by ``fls`` if provided.
183
        widths : ArrayLike, default None
184
            Widths of the flowline (necessary for the weighted average).
185
            Overridden by ``fls`` if provided.
186
        fls : list[oggm.Flowline], default None
187
            List of flowline instances. Alternative to heights and
188
            widths, and overrides them if provided.
189
        year : ArrayLike[float] or float, default None
190
            Year, or a range of years in "floating year" convention.
191

192
        Returns
193
        -------
194
        np.ndarray
195
            Specific mass balance (units: mm w.e. yr-1).
196
        """
197
        stack = []
8✔
198
        year = np.atleast_1d(year)
8✔
199
        for mb_yr in year:
8✔
200
            if fls is not None:
8✔
201
                mbs = self.get_annual_specific_mass_balance(fls=fls, year=mb_yr)
8✔
202
            else:
203
                mbs = self.get_annual_mb(heights, year=mb_yr)
3✔
204
                mbs = weighted_average_1d(mbs, widths)
3✔
205
            stack.append(mbs)
8✔
206

207
        return set_array_type(stack) * SEC_IN_YEAR * self.rho
8✔
208

209
    def get_ela(self, year=None, **kwargs):
9✔
210
        """Get the equilibrium line altitude for a given year.
211

212
        Parameters
213
        ----------
214
        year : ArrayLike[float] or float, default None
215
            Year, or a range of years in "floating year" convention.
216
        **kwargs
217
            Any other keyword argument accepted by ``self.get_annual_mb``.
218

219
        Returns
220
        -------
221
        float or np.ndarray:
222
            The equilibrium line altitude (ELA) in m.
223
        """
224
        stack = []
4✔
225
        year = np.atleast_1d(year)
4✔
226
        for mb_year in year:
4✔
227
            if self.valid_bounds is None:
4✔
228
                raise ValueError('attribute `valid_bounds` needs to be '
×
229
                                'set for the ELA computation.')
230

231
            # Check for invalid ELAs
232
            b0, b1 = self.valid_bounds
4✔
233
            if (np.any(~np.isfinite(
4✔
234
                    self.get_annual_mb([b0, b1], year=mb_year, **kwargs))) or
235
                    (self.get_annual_mb([b0], year=mb_year, **kwargs)[0] > 0) or
236
                    (self.get_annual_mb([b1], year=mb_year, **kwargs)[0] < 0)):
237
                stack.append(np.nan)
1✔
238
            else:
239
                def to_minimize(x):
4✔
240
                    return (self.get_annual_mb([x], year=mb_year, **kwargs)[0] *
4✔
241
                    SEC_IN_YEAR * self.rho)
242
                stack.append(optimize.brentq(to_minimize, *self.valid_bounds, xtol=0.1))
4✔
243

244
        return set_array_type(stack)
4✔
245

246
    def is_year_valid(self, year):
9✔
247
        """Checks if a given date year be simulated by this model.
248

249
        Parameters
250
        ----------
251
        year : float, optional
252
            the time (in the "floating year" convention)
253

254
        Returns
255
        -------
256
        True if this year can be simulated by the model
257
        """
258
        raise NotImplementedError()
259

260

261
class ScalarMassBalance(MassBalanceModel):
9✔
262
    """Constant mass balance, everywhere."""
263

264
    def __init__(self, mb=0.):
9✔
265
        """ Initialize.
266

267
        Parameters
268
        ----------
269
        mb : float
270
            Fix the mass balance to a certain value (unit: [mm w.e. yr-1])
271
        """
272
        super(ScalarMassBalance, self).__init__()
2✔
273
        self.hemisphere = 'nh'
2✔
274
        self.valid_bounds = [-2e4, 2e4]  # in m
2✔
275
        self._mb = mb
2✔
276

277
    def get_monthly_mb(self, heights, **kwargs):
9✔
278
        mb = np.asarray(heights) * 0 + self._mb
×
279
        return mb / SEC_IN_YEAR / self.rho
×
280

281
    def get_annual_mb(self, heights, **kwargs):
9✔
282
        mb = np.asarray(heights) * 0 + self._mb
2✔
283
        return mb / SEC_IN_YEAR / self.rho
2✔
284

285
    def is_year_valid(self, year):
9✔
286
        return True
×
287

288

289
class LinearMassBalance(MassBalanceModel):
9✔
290
    """Constant mass balance as a linear function of altitude.
291

292
    Attributes
293
    ----------
294
    ela_h: float
295
        the equilibrium line altitude (units: [m])
296
    grad: float
297
        the mass balance gradient (unit: [mm w.e. yr-1 m-1])
298
    max_mb: float
299
        Cap the mass balance to a certain value (unit: [mm w.e. yr-1])
300
    temp_bias
301
    """
302

303
    def __init__(self, ela_h, grad=3., max_mb=None):
9✔
304
        """ Initialize.
305

306
        Parameters
307
        ----------
308
        ela_h: float
309
            Equilibrium line altitude (units: [m])
310
        grad: float
311
            Mass balance gradient (unit: [mm w.e. yr-1 m-1])
312
        max_mb: float
313
            Cap the mass balance to a certain value (unit: [mm w.e. yr-1])
314
        """
315
        super(LinearMassBalance, self).__init__()
8✔
316
        self.hemisphere = 'nh'
8✔
317
        self.valid_bounds = [-1e4, 2e4]  # in m
8✔
318
        self.orig_ela_h = ela_h
8✔
319
        self.ela_h = ela_h
8✔
320
        self.grad = grad
8✔
321
        self.max_mb = max_mb
8✔
322
        self._temp_bias = 0
8✔
323

324
    @property
9✔
325
    def temp_bias(self):
9✔
326
        """Change the ELA following a simple rule: + 1K -> ELA + 150 m
327

328
        A "temperature bias" doesn't makes much sense in the linear MB
329
        context, but we implemented a simple empirical rule:
330
        + 1K -> ELA + 150 m
331
        """
332
        return self._temp_bias
×
333

334
    @temp_bias.setter
9✔
335
    def temp_bias(self, value):
9✔
336
        self.ela_h = self.orig_ela_h + value * 150
1✔
337
        self._temp_bias = value
1✔
338

339
    def get_monthly_mb(self, heights, **kwargs):
9✔
340
        mb = (np.asarray(heights) - self.ela_h) * self.grad
6✔
341
        if self.max_mb is not None:
6✔
342
            clip_max(mb, self.max_mb, out=mb)
×
343
        return mb / SEC_IN_YEAR / self.rho
6✔
344

345
    def get_annual_mb(self, heights, **kwargs):
9✔
346
        return self.get_monthly_mb(heights, **kwargs)
6✔
347

348
    def is_year_valid(self, year):
9✔
349
        return True
×
350

351

352
class MonthlyTIModel(MassBalanceModel):
9✔
353
    """Monthly temperature index model.
354
    """
355
    def __init__(self, gdir,
9✔
356
                 filename='climate_historical',
357
                 input_filesuffix='',
358
                 settings_filesuffix='',
359
                 fl_id=None,
360
                 melt_f=None,
361
                 temp_bias=None,
362
                 prcp_fac=None,
363
                 bias=0,
364
                 ys=None,
365
                 ye=None,
366
                 repeat=False,
367
                 check_calib_params=True,
368
                 ):
369
        """Initialize.
370

371
        Parameters
372
        ----------
373
        gdir : GlacierDirectory
374
            the glacier directory
375
        filename : str, optional
376
            set to a different BASENAME if you want to use alternative climate
377
            data. Default is 'climate_historical'
378
        input_filesuffix : str, optional
379
            append a suffix to the climate input filename (useful for GCM runs).
380
        settings_filesuffix : str, optional
381
            append a suffix to the settings file (useful for
382
            sensitivity runs).
383
        fl_id : int, optional
384
            if this flowline has been calibrated alone and has specific
385
            model parameters.
386
        melt_f : float, optional
387
            set to the value of the melt factor you want to use,
388
            here the unit is kg m-2 day-1 K-1
389
            (the default is to use the calibrated value).
390
        temp_bias : float, optional
391
            set to the value of the temperature bias you want to use
392
            (the default is to use the calibrated value).
393
        prcp_fac : float, optional
394
            set to the value of the precipitation factor you want to use
395
            (the default is to use the calibrated value).
396
        bias : float, optional
397
            set to the alternative value of the calibration bias [mm we yr-1]
398
            you want to use (the default is to use the calibrated value)
399
            Note that this bias is *substracted* from the computed MB. Indeed:
400
            BIAS = MODEL_MB - REFERENCE_MB.
401
        ys : int
402
            The start of the climate period where the MB model is valid
403
            (default: the period with available data)
404
        ye : int
405
            The end of the climate period where the MB model is valid
406
            (default: the period with available data)
407
        repeat : bool
408
            Whether the climate period given by [ys, ye] should be repeated
409
            indefinitely in a circular way
410
        check_calib_params : bool
411
            OGGM will try hard not to use wrongly calibrated parameters
412
            by checking the global parameters used during calibration and
413
            the ones you are using at run time. If they don't match, it will
414
            raise an error. Set to "False" to suppress this check.
415
        """
416

417
        self.settings_filesuffix = settings_filesuffix
7✔
418
        gdir.settings_filesuffix = settings_filesuffix
7✔
419

420
        super(MonthlyTIModel, self).__init__(gdir=gdir)
7✔
421
        self.valid_bounds = [-1e4, 2e4]  # in m
7✔
422
        self.fl_id = fl_id  # which flowline are we the model of?
7✔
423
        self.gdir = gdir
7✔
424

425
        if melt_f is None:
7✔
426
            melt_f = self.calib_params['melt_f']
7✔
427

428
        if temp_bias is None:
7✔
429
            temp_bias = self.calib_params['temp_bias']
7✔
430

431
        if prcp_fac is None:
7✔
432
            prcp_fac = self.calib_params['prcp_fac']
7✔
433

434
        # Check the climate related params to the GlacierDir to make sure
435
        if check_calib_params:
7✔
436
            mb_calib = self.calib_params['mb_global_params']
7✔
437
            for k, v in mb_calib.items():
7✔
438
                if v != self.gdir.settings[k]:
7✔
439
                    msg = ('You seem to use different mass balance parameters '
×
440
                           'than used for the calibration: '
441
                           f"you use gdir.settings['{k}']={gdir.settings[k]} while "
442
                           f"it was calibrated with gdir.settings['{k}']={v}. "
443
                           'Set `check_calib_params=False` to ignore this '
444
                           'warning.')
445
                    raise InvalidWorkflowError(msg)
×
446
            src = self.calib_params['baseline_climate_source']
7✔
447
            src_calib = gdir.get_climate_info()['baseline_climate_source']
7✔
448
            if src != src_calib:
7✔
449
                msg = (f'You seem to have calibrated with the {src} '
×
450
                       f"climate data while this gdir was calibrated with "
451
                       f"{src_calib}. Set `check_calib_params=False` to "
452
                       f"ignore this warning.")
453
                raise InvalidWorkflowError(msg)
×
454

455
        self.melt_f = melt_f
7✔
456
        self.bias = bias
7✔
457

458
        # Global parameters
459
        self.t_solid = gdir.settings['temp_all_solid']
7✔
460
        self.t_liq = gdir.settings['temp_all_liq']
7✔
461
        self.t_melt = gdir.settings['temp_melt']
7✔
462

463
        # check if valid prcp_fac is used
464
        if prcp_fac <= 0:
7✔
465
            raise InvalidParamsError('prcp_fac has to be above zero!')
×
466
        default_grad = gdir.settings['temp_default_gradient']
7✔
467

468
        # Public attrs
469
        self.hemisphere = gdir.hemisphere
7✔
470
        self.repeat = repeat
7✔
471

472
        # Private attrs
473
        # to allow prcp_fac to be changed after instantiation
474
        # prescribe the prcp_fac as it is instantiated
475
        self._prcp_fac = prcp_fac
7✔
476
        # same for temp bias
477
        self._temp_bias = temp_bias
7✔
478

479
        # Read climate file
480
        fpath = gdir.get_filepath(filename, filesuffix=input_filesuffix)
7✔
481
        with ncDataset(fpath, mode='r') as nc:
7✔
482
            # time
483
            time = nc.variables['time']
7✔
484
            time = cftime.num2date(time[:], time.units, calendar=time.calendar)
7✔
485
            ny, r = divmod(len(time), 12)
7✔
486
            if r != 0:
7✔
487
                raise ValueError('Climate data should be N full years')
×
488

489
            # We check for calendar years
490
            if (time[0].month != 1) or (time[-1].month != 12):
7✔
491
                raise InvalidWorkflowError('We now work exclusively with '
×
492
                                           'calendar years.')
493

494
            # Quick trick because we know the size of our array
495
            years = np.repeat(np.arange(time[-1].year - ny + 1,
7✔
496
                                        time[-1].year + 1), 12)
497
            pok = slice(None)  # take all is default (optim)
7✔
498
            if ys is not None:
7✔
499
                pok = years >= ys
1✔
500
            if ye is not None:
7✔
501
                try:
1✔
502
                    pok = pok & (years <= ye)
1✔
503
                except TypeError:
×
504
                    pok = years <= ye
×
505

506
            self.years = years[pok]
7✔
507
            self.months = np.tile(np.arange(1, 13), ny)[pok]
7✔
508

509
            # Read timeseries and correct it
510
            self.temp = nc.variables['temp'][pok].astype(np.float64) + self._temp_bias
7✔
511
            self.prcp = nc.variables['prcp'][pok].astype(np.float64) * self._prcp_fac
7✔
512

513
            grad = self.prcp * 0 + default_grad
7✔
514
            self.grad = grad
7✔
515
            self.ref_hgt = nc.ref_hgt
7✔
516
            self.climate_source = nc.climate_source
7✔
517
            self.ys = self.years[0]
7✔
518
            self.ye = self.years[-1]
7✔
519

520
    def __repr__(self):
521
        """String Representation of the mass balance model"""
522
        summary = ['<oggm.MassBalanceModel>']
523
        summary += ['  Class: ' + self.__class__.__name__]
524
        summary += ['  Attributes:']
525
        # Add all scalar attributes
526
        done = []
527
        for k in ['hemisphere', 'climate_source', 'melt_f', 'prcp_fac', 'temp_bias', 'bias']:
528
            done.append(k)
529
            v = self.__getattribute__(k)
530
            if k == 'climate_source':
531
                if v.endswith('.nc'):
532
                    v = os.path.basename(v)
533
            nofloat = ['hemisphere', 'climate_source']
534
            nbform = '    - {}: {}' if k in nofloat else '    - {}: {:.02f}'
535
            summary += [nbform.format(k, v)]
536
        for k, v in self.__dict__.items():
537
            if np.isscalar(v) and not k.startswith('_') and k not in done:
538
                nbform = '    - {}: {}'
539
                summary += [nbform.format(k, v)]
540
        return '\n'.join(summary) + '\n'
541

542
    @property
9✔
543
    def monthly_melt_f(self):
9✔
544
        return self.melt_f * 365 / 12
7✔
545

546
    # adds the possibility of changing prcp_fac
547
    # after instantiation with properly changing the prcp time series
548
    @property
9✔
549
    def prcp_fac(self):
9✔
550
        """Precipitation factor (default: gdir.settings['prcp_fac'])
551

552
        Called factor to make clear that it is a multiplicative factor in
553
        contrast to the additive temperature bias
554
        """
555
        return self._prcp_fac
3✔
556

557
    @prcp_fac.setter
9✔
558
    def prcp_fac(self, new_prcp_fac):
9✔
559
        # just to check that no invalid prcp_factors are used
560
        if np.any(np.asarray(new_prcp_fac) <= 0):
5✔
561
            raise InvalidParamsError('prcp_fac has to be above zero!')
1✔
562

563
        if len(np.atleast_1d(new_prcp_fac)) == 12:
5✔
564
            # OK so that's monthly stuff
565
            new_prcp_fac = np.tile(new_prcp_fac, len(self.prcp) // 12)
1✔
566

567
        self.prcp *= new_prcp_fac / self._prcp_fac
5✔
568
        self._prcp_fac = new_prcp_fac
5✔
569

570
    @property
9✔
571
    def temp_bias(self):
9✔
572
        """Add a temperature bias to the time series"""
573
        return self._temp_bias
5✔
574

575
    @temp_bias.setter
9✔
576
    def temp_bias(self, new_temp_bias):
9✔
577

578
        if len(np.atleast_1d(new_temp_bias)) == 12:
4✔
579
            # OK so that's monthly stuff
580
            new_temp_bias = np.tile(new_temp_bias, len(self.temp) // 12)
1✔
581

582
        self.temp += new_temp_bias - self._temp_bias
4✔
583
        self._temp_bias = new_temp_bias
4✔
584

585
    @lazy_property
9✔
586
    def calib_params(self):
9✔
587
        if self.fl_id is None:
7✔
588
            return self.gdir.settings
7✔
589
        else:
590
            fp_fl_settings = self.gdir.get_filepath('settings',
×
591
                                                    filesuffix=f'_fl{self.fl_id}')
592
            if os.path.exists(fp_fl_settings):
×
593
                self.gdir.settings_filesuffix = f'_fl{self.fl_id}'
×
594
                out = self.gdir.settings
×
595
                if self.settings_filesuffix:
×
596
                    raise InvalidWorkflowError('settings_filesuffix cannot be '
×
597
                                               'used with multiple flowlines')
598
                return out
×
599
            else:
600
                return self.gdir.settings
×
601

602
    def is_year_valid(self, year):
9✔
603
        return self.ys <= year <= self.ye
7✔
604

605
    def get_monthly_climate(self, heights, year=None):
9✔
606
        """Monthly climate information at given heights.
607

608
        Note that prcp is corrected with the precipitation factor and that
609
        all other model biases (temp and prcp) are applied.
610

611
        Returns
612
        -------
613
        (temp, tempformelt, prcp, prcpsol)
614
        """
615

616
        y, m = floatyear_to_date(year)
4✔
617
        if self.repeat:
4✔
618
            y = self.ys + (y - self.ys) % (self.ye - self.ys + 1)
×
619
        if not self.is_year_valid(y):
4✔
620
            raise ValueError('year {} out of the valid time bounds: '
×
621
                             '[{}, {}]'.format(y, self.ys, self.ye))
622
        pok = np.where((self.years == y) & (self.months == m))[0][0]
4✔
623

624
        # Read already (temperature bias and precipitation factor corrected!)
625
        itemp = self.temp[pok]
4✔
626
        iprcp = self.prcp[pok]
4✔
627
        igrad = self.grad[pok]
4✔
628

629
        # For each height pixel:
630
        # Compute temp and tempformelt (temperature above melting threshold)
631
        npix = len(heights)
4✔
632
        temp = np.ones(npix) * itemp + igrad * (heights - self.ref_hgt)
4✔
633
        tempformelt = temp - self.t_melt
4✔
634
        clip_min(tempformelt, 0, out=tempformelt)
4✔
635

636
        # Compute solid precipitation from total precipitation
637
        prcp = np.ones(npix) * iprcp
4✔
638
        fac = 1 - (temp - self.t_solid) / (self.t_liq - self.t_solid)
4✔
639
        prcpsol = prcp * clip_array(fac, 0, 1)
4✔
640

641
        return temp, tempformelt, prcp, prcpsol
4✔
642

643
    def _get_2d_annual_climate(self, heights, year):
9✔
644
        # Avoid code duplication with a getter routine
645
        year = np.floor(year)
7✔
646
        if self.repeat:
7✔
647
            year = self.ys + (year - self.ys) % (self.ye - self.ys + 1)
1✔
648
        if not self.is_year_valid(year):
7✔
649
            raise ValueError('year {} out of the valid time bounds: '
×
650
                             '[{}, {}]'.format(year, self.ys, self.ye))
651
        pok = np.where(self.years == year)[0]
7✔
652
        if len(pok) < 1:
7✔
653
            raise ValueError('Year {} not in record'.format(int(year)))
×
654

655
        # Read already (temperature bias and precipitation factor corrected!)
656
        itemp = self.temp[pok]
7✔
657
        iprcp = self.prcp[pok]
7✔
658
        igrad = self.grad[pok]
7✔
659

660
        # For each height pixel:
661
        # Compute temp and tempformelt (temperature above melting threshold)
662
        heights = np.asarray(heights)
7✔
663
        npix = len(heights)
7✔
664
        grad_temp = np.atleast_2d(igrad).repeat(npix, 0)
7✔
665
        grad_temp *= (heights.repeat(12).reshape(grad_temp.shape) -
7✔
666
                      self.ref_hgt)
667
        temp2d = np.atleast_2d(itemp).repeat(npix, 0) + grad_temp
7✔
668
        temp2dformelt = temp2d - self.t_melt
7✔
669
        clip_min(temp2dformelt, 0, out=temp2dformelt)
7✔
670

671
        # Compute solid precipitation from total precipitation
672
        prcp = np.atleast_2d(iprcp).repeat(npix, 0)
7✔
673
        fac = 1 - (temp2d - self.t_solid) / (self.t_liq - self.t_solid)
7✔
674
        prcpsol = prcp * clip_array(fac, 0, 1)
7✔
675

676
        return temp2d, temp2dformelt, prcp, prcpsol
7✔
677

678
    def get_annual_climate(self, heights, year=None):
9✔
679
        """Annual climate information at given heights.
680

681
        Note that prcp is corrected with the precipitation factor and that
682
        all other model biases (temp and prcp) are applied.
683

684
        Returns
685
        -------
686
        (temp, tempformelt, prcp, prcpsol)
687
        """
688
        t, tmelt, prcp, prcpsol = self._get_2d_annual_climate(heights, year)
×
689
        return (t.mean(axis=1), tmelt.sum(axis=1),
×
690
                prcp.sum(axis=1), prcpsol.sum(axis=1))
691

692
    def get_monthly_mb(self, heights, year=None, add_climate=False, **kwargs):
9✔
693

694
        t, tmelt, prcp, prcpsol = self.get_monthly_climate(heights, year=year)
4✔
695
        mb_month = prcpsol - self.monthly_melt_f * tmelt
4✔
696
        mb_month -= self.bias * SEC_IN_MONTH / SEC_IN_YEAR
4✔
697
        if add_climate:
4✔
698
            return (mb_month / SEC_IN_MONTH / self.rho, t, tmelt,
1✔
699
                    prcp, prcpsol)
700
        return mb_month / SEC_IN_MONTH / self.rho
4✔
701

702
    def get_annual_mb(self, heights, year=None, add_climate=False, **kwargs):
9✔
703

704
        t, tmelt, prcp, prcpsol = self._get_2d_annual_climate(heights, year)
7✔
705
        mb_annual = np.sum(prcpsol - self.monthly_melt_f * tmelt, axis=1)
7✔
706
        mb_annual = (mb_annual - self.bias) / SEC_IN_YEAR / self.rho
7✔
707
        if add_climate:
7✔
708
            return (mb_annual, t.mean(axis=1), tmelt.sum(axis=1),
3✔
709
                    prcp.sum(axis=1), prcpsol.sum(axis=1))
710
        return mb_annual
7✔
711

712

713
class ConstantMassBalance(MassBalanceModel):
9✔
714
    """Constant mass balance during a chosen period.
715

716
    This is useful for equilibrium experiments.
717

718
    IMPORTANT: the "naive" implementation requires to compute the massbalance
719
    N times for each simulation year, where N is the number of years over the
720
    climate period to average. This is very expensive, and therefore we use
721
    interpolation. This makes it *unusable* with MB models relying on the
722
    computational domain being always the same.
723

724
    If your model requires constant domain size, conisder using RandomMassBalance
725
    instead.
726

727
    Note that it uses the "correct" way to represent the average mass balance
728
    over a given period. See: https://oggm.org/2021/08/05/mean-forcing/
729

730
    Attributes
731
    ----------
732
    y0 : int
733
        the center year of the period
734
    halfsize : int
735
        the halfsize of the period
736
    years : ndarray
737
        the years of the period
738
    """
739

740
    def __init__(self, gdir, mb_model_class=MonthlyTIModel,
9✔
741
                 y0=None, halfsize=15,
742
                 **kwargs):
743
        """Initialize
744

745
        Parameters
746
        ----------
747
        gdir : GlacierDirectory
748
            the glacier directory
749
        mb_model_class : MassBalanceModel class
750
            the MassBalanceModel to use for the constant climate
751
        y0 : int, required
752
            the year at the center of the period of interest.
753
        halfsize : int, optional
754
            the half-size of the time window (window size = 2 * halfsize + 1)
755
        **kwargs:
756
            keyword arguments to pass to the mb_model_class
757
        """
758

759
        super().__init__()
6✔
760
        self.mbmod = mb_model_class(gdir,
6✔
761
                                    **kwargs)
762

763
        if y0 is None:
6✔
764
            raise InvalidParamsError('Please set `y0` explicitly')
×
765

766
        # This is a quick'n dirty optimisation
767
        try:
6✔
768
            fls = gdir.read_pickle('model_flowlines')
6✔
769
            h = []
6✔
770
            for fl in fls:
6✔
771
                # We use bed because of overdeepenings
772
                h = np.append(h, fl.bed_h)
6✔
773
                h = np.append(h, fl.surface_h)
6✔
774
            zminmax = np.round([np.min(h)-50, np.max(h)+2000])
6✔
775
        except FileNotFoundError:
3✔
776
            # in case we don't have them
777
            with ncDataset(gdir.get_filepath('gridded_data')) as nc:
3✔
778
                if np.isfinite(nc.min_h_dem):
3✔
779
                    # a bug sometimes led to non-finite
780
                    zminmax = [nc.min_h_dem-250, nc.max_h_dem+1500]
3✔
781
                else:
782
                    zminmax = [nc.min_h_glacier-1250, nc.max_h_glacier+1500]
×
783
        self.hbins = np.arange(*zminmax, step=10)
6✔
784
        self.valid_bounds = self.hbins[[0, -1]]
6✔
785
        self.y0 = y0
6✔
786
        self.halfsize = halfsize
6✔
787
        self.years = np.arange(y0-halfsize, y0+halfsize+1)
6✔
788
        self.hemisphere = gdir.hemisphere
6✔
789

790
    @property
9✔
791
    def temp_bias(self):
9✔
792
        """Temperature bias to add to the original series."""
793
        return self.mbmod.temp_bias
3✔
794

795
    @temp_bias.setter
9✔
796
    def temp_bias(self, value):
9✔
797
        for attr_name in ['_lazy_interp_yr', '_lazy_interp_m']:
4✔
798
            if hasattr(self, attr_name):
4✔
799
                delattr(self, attr_name)
2✔
800
        self.mbmod.temp_bias = value
4✔
801

802
    @property
9✔
803
    def prcp_fac(self):
9✔
804
        """Precipitation factor to apply to the original series."""
805
        return self.mbmod.prcp_fac
1✔
806

807
    @prcp_fac.setter
9✔
808
    def prcp_fac(self, value):
9✔
809
        for attr_name in ['_lazy_interp_yr', '_lazy_interp_m']:
1✔
810
            if hasattr(self, attr_name):
1✔
811
                delattr(self, attr_name)
1✔
812
        self.mbmod.prcp_fac = value
1✔
813

814
    @property
9✔
815
    def bias(self):
9✔
816
        """Residual bias to apply to the original series."""
817
        return self.mbmod.bias
2✔
818

819
    @bias.setter
9✔
820
    def bias(self, value):
9✔
821
        self.mbmod.bias = value
1✔
822

823
    @lazy_property
9✔
824
    def interp_yr(self):
9✔
825
        # annual MB
826
        mb_on_h = self.hbins * 0.
6✔
827
        for yr in self.years:
6✔
828
            mb_on_h += self.mbmod.get_annual_mb(self.hbins, year=yr)
6✔
829
        return interp1d(self.hbins, mb_on_h / len(self.years))
6✔
830

831
    @lazy_property
9✔
832
    def interp_m(self):
9✔
833
        # monthly MB
834
        months = np.arange(12)+1
2✔
835
        interp_m = []
2✔
836
        for m in months:
2✔
837
            mb_on_h = self.hbins*0.
2✔
838
            for yr in self.years:
2✔
839
                yr = date_to_floatyear(yr, m)
2✔
840
                mb_on_h += self.mbmod.get_monthly_mb(self.hbins, year=yr)
2✔
841
            interp_m.append(interp1d(self.hbins, mb_on_h / len(self.years)))
2✔
842
        return interp_m
2✔
843

844
    def is_year_valid(self, year):
9✔
845
        return True
×
846

847
    def get_monthly_climate(self, heights, year=None):
9✔
848
        """Average climate information at given heights.
849

850
        Note that prcp is corrected with the precipitation factor and that
851
        all other biases (precipitation, temp) are applied
852

853
        Returns
854
        -------
855
        (temp, tempformelt, prcp, prcpsol)
856
        """
857
        _, m = floatyear_to_date(year)
2✔
858
        yrs = [date_to_floatyear(y, m) for y in self.years]
2✔
859
        heights = np.atleast_1d(heights)
2✔
860
        nh = len(heights)
2✔
861
        shape = (len(yrs), nh)
2✔
862
        temp = np.zeros(shape)
2✔
863
        tempformelt = np.zeros(shape)
2✔
864
        prcp = np.zeros(shape)
2✔
865
        prcpsol = np.zeros(shape)
2✔
866
        for i, yr in enumerate(yrs):
2✔
867
            t, tm, p, ps = self.mbmod.get_monthly_climate(heights, year=yr)
2✔
868
            temp[i, :] = t
2✔
869
            tempformelt[i, :] = tm
2✔
870
            prcp[i, :] = p
2✔
871
            prcpsol[i, :] = ps
2✔
872
        return (np.mean(temp, axis=0),
2✔
873
                np.mean(tempformelt, axis=0),
874
                np.mean(prcp, axis=0),
875
                np.mean(prcpsol, axis=0))
876

877
    def get_annual_climate(self, heights, year=None):
9✔
878
        """Average climate information at given heights.
879

880
        Note that prcp is corrected with the precipitation factor and that
881
        all other biases (precipitation, temp) are applied
882

883
        Returns
884
        -------
885
        (temp, tempformelt, prcp, prcpsol)
886
        """
887
        yrs = monthly_timeseries(self.years[0], self.years[-1],
4✔
888
                                 include_last_year=True)
889
        heights = np.atleast_1d(heights)
4✔
890
        nh = len(heights)
4✔
891
        shape = (len(yrs), nh)
4✔
892
        temp = np.zeros(shape)
4✔
893
        tempformelt = np.zeros(shape)
4✔
894
        prcp = np.zeros(shape)
4✔
895
        prcpsol = np.zeros(shape)
4✔
896
        for i, yr in enumerate(yrs):
4✔
897
            t, tm, p, ps = self.mbmod.get_monthly_climate(heights, year=yr)
4✔
898
            temp[i, :] = t
4✔
899
            tempformelt[i, :] = tm
4✔
900
            prcp[i, :] = p
4✔
901
            prcpsol[i, :] = ps
4✔
902
        # Note that we do not weight for number of days per month:
903
        # this is consistent with OGGM's calendar
904
        return (np.mean(temp, axis=0),
4✔
905
                np.mean(tempformelt, axis=0) * 12,
906
                np.mean(prcp, axis=0) * 12,
907
                np.mean(prcpsol, axis=0) * 12)
908

909
    def get_monthly_mb(self, heights, year=None, add_climate=False, **kwargs):
9✔
910
        yr, m = floatyear_to_date(year)
2✔
911
        if add_climate:
2✔
912
            t, tmelt, prcp, prcpsol = self.get_monthly_climate(heights, year=year)
2✔
913
            return self.interp_m[m-1](heights), t, tmelt, prcp, prcpsol
2✔
914
        return self.interp_m[m-1](heights)
1✔
915

916
    def get_annual_mb(self, heights, year=None, add_climate=False, **kwargs):
9✔
917
        mb = self.interp_yr(heights)
6✔
918
        if add_climate:
6✔
919
            t, tmelt, prcp, prcpsol = self.get_annual_climate(heights)
1✔
920
            return mb, t, tmelt, prcp, prcpsol
1✔
921
        return mb
6✔
922

923

924
class RandomMassBalance(MassBalanceModel):
9✔
925
    """Random shuffle of all MB years within a given time period.
926

927
    This is useful for finding a possible past glacier state or for sensitivity
928
    experiments.
929

930
    Note that this is going to be sensitive to extreme years in certain
931
    periods, but it is by far more physically reasonable than other
932
    approaches based on gaussian assumptions.
933
    """
934

935
    def __init__(self, gdir, mb_model_class=MonthlyTIModel,
9✔
936
                 y0=None, halfsize=15, seed=None,
937
                 all_years=False, unique_samples=False,
938
                 prescribe_years=None,
939
                 **kwargs):
940
        """Initialize.
941

942
        Parameters
943
        ----------
944
        gdir : GlacierDirectory
945
            the glacier directory
946
        mb_model_class : MassBalanceModel class
947
            the MassBalanceModel to use for the random shuffle
948
        y0 : int, required
949
            the year at the center of the period of interest.
950
        halfsize : int, optional
951
            the half-size of the time window (window size = 2 * halfsize + 1)
952
        seed : int, optional
953
            Random seed used to initialize the pseudo-random number generator.
954
        all_years : bool
955
            if True, overwrites ``y0`` and ``halfsize`` to use all available
956
            years.
957
        unique_samples: bool
958
            if true, chosen random mass balance years will only be available
959
            once per random climate period-length
960
            if false, every model year will be chosen from the random climate
961
            period with the same probability
962
        prescribe_years : pandas Series
963
            instead of random samples, take a series of (i, y) pairs where
964
            (i) is the simulation year index and (y) is the year to pick in the
965
            original timeseries. Overrides `y0`, `halfsize`, `all_years`,
966
            `unique_samples` and `seed`.
967
        **kwargs:
968
            keyword arguments to pass to the mb_model_class
969
        """
970

971
        super().__init__()
5✔
972
        self.valid_bounds = [-1e4, 2e4]  # in m
5✔
973
        self.mbmod = mb_model_class(gdir, **kwargs)
5✔
974

975
        # Climate period
976
        self.prescribe_years = prescribe_years
5✔
977

978
        if self.prescribe_years is None:
5✔
979
            # Normal stuff
980
            self.rng = np.random.RandomState(seed)
5✔
981
            if all_years:
5✔
982
                self.years = self.mbmod.years
×
983
            else:
984
                if y0 is None:
5✔
985
                    raise InvalidParamsError('Please set `y0` explicitly')
×
986
                self.years = np.arange(y0 - halfsize, y0 + halfsize + 1)
5✔
987
        else:
988
            self.rng = None
1✔
989
            self.years = self.prescribe_years.index
1✔
990

991
        self.yr_range = (self.years[0], self.years[-1] + 1)
5✔
992
        self.ny = len(self.years)
5✔
993
        self.hemisphere = gdir.hemisphere
5✔
994

995
        self._state_yr = dict()
5✔
996

997
        # Sampling without replacement
998
        self.unique_samples = unique_samples
5✔
999
        if self.unique_samples:
5✔
1000
            self.sampling_years = self.years
1✔
1001

1002
    @property
9✔
1003
    def temp_bias(self):
9✔
1004
        """Temperature bias to add to the original series."""
1005
        return self.mbmod.temp_bias
1✔
1006

1007
    @temp_bias.setter
9✔
1008
    def temp_bias(self, value):
9✔
1009
        """Temperature bias to add to the original series."""
1010
        self.mbmod.temp_bias = value
1✔
1011

1012
    @property
9✔
1013
    def prcp_fac(self):
9✔
1014
        """Precipitation factor to apply to the original series."""
1015
        return self.mbmod.prcp_fac
1✔
1016

1017
    @prcp_fac.setter
9✔
1018
    def prcp_fac(self, value):
9✔
1019
        """Precipitation factor to apply to the original series."""
1020
        self.mbmod.prcp_fac = value
1✔
1021

1022
    @property
9✔
1023
    def bias(self):
9✔
1024
        """Residual bias to apply to the original series."""
1025
        return self.mbmod.bias
1✔
1026

1027
    @bias.setter
9✔
1028
    def bias(self, value):
9✔
1029
        """Residual bias to apply to the original series."""
1030
        self.mbmod.bias = value
1✔
1031

1032
    def is_year_valid(self, year):
9✔
1033
        return True
×
1034

1035
    def get_state_yr(self, year=None):
9✔
1036
        """For a given year, get the random year associated to it."""
1037
        year = int(year)
5✔
1038
        if year not in self._state_yr:
5✔
1039
            if self.prescribe_years is not None:
5✔
1040
                self._state_yr[year] = self.prescribe_years.loc[year]
1✔
1041
            else:
1042
                if self.unique_samples:
5✔
1043
                    # --- Sampling without replacement ---
1044
                    if self.sampling_years.size == 0:
1✔
1045
                        # refill sample pool when all years were picked once
1046
                        self.sampling_years = self.years
×
1047
                    # choose one year which was not used in the current period
1048
                    _sample = self.rng.choice(self.sampling_years)
1✔
1049
                    # write chosen year to dictionary
1050
                    self._state_yr[year] = _sample
1✔
1051
                    # update sample pool: remove the chosen year from it
1052
                    self.sampling_years = np.delete(
1✔
1053
                        self.sampling_years,
1054
                        np.where(self.sampling_years == _sample))
1055
                else:
1056
                    # --- Sampling with replacement ---
1057
                    self._state_yr[year] = self.rng.randint(*self.yr_range)
5✔
1058
        return self._state_yr[year]
5✔
1059

1060
    def get_monthly_mb(self, heights, year=None, **kwargs):
9✔
1061
        ryr, m = floatyear_to_date(year)
3✔
1062
        ryr = date_to_floatyear(self.get_state_yr(ryr), m)
3✔
1063
        return self.mbmod.get_monthly_mb(heights, year=ryr, **kwargs)
3✔
1064

1065
    def get_annual_mb(self, heights, year=None, **kwargs):
9✔
1066
        ryr = self.get_state_yr(int(year))
5✔
1067
        return self.mbmod.get_annual_mb(heights, year=ryr, **kwargs)
5✔
1068

1069

1070
class UncertainMassBalance(MassBalanceModel):
9✔
1071
    """Adding uncertainty to a mass balance model.
1072

1073
    There are three variables for which you can add uncertainty:
1074
    - temperature (additive bias)
1075
    - precipitation (multiplicative factor)
1076
    - residual (a bias in units of MB)
1077
    """
1078

1079
    def __init__(self, basis_model,
9✔
1080
                 rdn_temp_bias_seed=None, rdn_temp_bias_sigma=0.1,
1081
                 rdn_prcp_fac_seed=None, rdn_prcp_fac_sigma=0.1,
1082
                 rdn_bias_seed=None, rdn_bias_sigma=100):
1083
        """Initialize.
1084

1085
        Parameters
1086
        ----------
1087
        basis_model : MassBalanceModel
1088
            the model to which you want to add the uncertainty to
1089
        rdn_temp_bias_seed : int
1090
            the seed of the random number generator
1091
        rdn_temp_bias_sigma : float
1092
            the standard deviation of the random temperature error
1093
        rdn_prcp_fac_seed : int
1094
            the seed of the random number generator
1095
        rdn_prcp_fac_sigma : float
1096
            the standard deviation of the random precipitation error
1097
            (to be consistent this should be renamed prcp_fac as well)
1098
        rdn_bias_seed : int
1099
            the seed of the random number generator
1100
        rdn_bias_sigma : float
1101
            the standard deviation of the random MB error
1102
        """
1103
        super(UncertainMassBalance, self).__init__()
1✔
1104
        # the aim here is to change temp_bias and prcp_fac so
1105
        self.mbmod = basis_model
1✔
1106
        self.hemisphere = basis_model.hemisphere
1✔
1107
        self.valid_bounds = self.mbmod.valid_bounds
1✔
1108
        self.is_year_valid = self.mbmod.is_year_valid
1✔
1109
        self.rng_temp = np.random.RandomState(rdn_temp_bias_seed)
1✔
1110
        self.rng_prcp = np.random.RandomState(rdn_prcp_fac_seed)
1✔
1111
        self.rng_bias = np.random.RandomState(rdn_bias_seed)
1✔
1112
        self._temp_sigma = rdn_temp_bias_sigma
1✔
1113
        self._prcp_sigma = rdn_prcp_fac_sigma
1✔
1114
        self._bias_sigma = rdn_bias_sigma
1✔
1115
        self._state_temp = dict()
1✔
1116
        self._state_prcp = dict()
1✔
1117
        self._state_bias = dict()
1✔
1118

1119
    def is_year_valid(self, year):
9✔
1120
        return self.mbmod.is_year_valid(year)
×
1121

1122
    @property
9✔
1123
    def temp_bias(self):
9✔
1124
        """Temperature bias to add to the original series."""
1125
        return self.mbmod.temp_bias
×
1126

1127
    @temp_bias.setter
9✔
1128
    def temp_bias(self, value):
9✔
1129
        """Temperature bias to add to the original series."""
1130
        self.mbmod.temp_bias = value
×
1131

1132
    @property
9✔
1133
    def prcp_fac(self):
9✔
1134
        """Precipitation factor to apply to the original series."""
1135
        return self.mbmod.prcp_fac
×
1136

1137
    @prcp_fac.setter
9✔
1138
    def prcp_fac(self, value):
9✔
1139
        """Precipitation factor to apply to the original series."""
1140
        self.mbmod.prcp_fac = value
×
1141

1142
    def _get_state_temp(self, year):
9✔
1143
        year = int(year)
1✔
1144
        if year not in self._state_temp:
1✔
1145
            self._state_temp[year] = self.rng_temp.randn() * self._temp_sigma
1✔
1146
        return self._state_temp[year]
1✔
1147

1148
    def _get_state_prcp(self, year):
9✔
1149
        year = int(year)
1✔
1150
        if year not in self._state_prcp:
1✔
1151
            self._state_prcp[year] = self.rng_prcp.randn() * self._prcp_sigma
1✔
1152
        return self._state_prcp[year]
1✔
1153

1154
    def _get_state_bias(self, year):
9✔
1155
        year = int(year)
1✔
1156
        if year not in self._state_bias:
1✔
1157
            self._state_bias[year] = self.rng_bias.randn() * self._bias_sigma
1✔
1158
        return self._state_bias[year]
1✔
1159

1160
    def get_monthly_mb(self, heights, year=None, **kwargs):
9✔
1161
        raise NotImplementedError()
1162

1163
    def get_annual_mb(self, heights, year=None, fl_id=None, **kwargs):
9✔
1164

1165
        # Keep the original biases and add a random error
1166
        _t = self.mbmod.temp_bias
1✔
1167
        _p = self.mbmod.prcp_fac
1✔
1168
        _b = self.mbmod.bias
1✔
1169
        self.mbmod.temp_bias = self._get_state_temp(year) + _t
1✔
1170
        self.mbmod.prcp_fac = self._get_state_prcp(year) + _p
1✔
1171
        self.mbmod.bias = self._get_state_bias(year) + _b
1✔
1172
        try:
1✔
1173
            out = self.mbmod.get_annual_mb(heights, year=year, fl_id=fl_id)
1✔
1174
        except BaseException:
×
1175
            self.mbmod.temp_bias = _t
×
1176
            self.mbmod.prcp_fac = _p
×
1177
            self.mbmod.bias = _b
×
1178
            raise
×
1179
        # Back to normal
1180
        self.mbmod.temp_bias = _t
1✔
1181
        self.mbmod.prcp_fac = _p
1✔
1182
        self.mbmod.bias = _b
1✔
1183
        return out
1✔
1184

1185

1186
class MultipleFlowlineMassBalance(MassBalanceModel):
9✔
1187
    """Handle mass balance at the glacier level instead of flowline level.
1188

1189
    Convenience class doing not much more than wrapping a list of mass balance
1190
    models, one for each flowline.
1191

1192
    This is useful for real-case studies, where each flowline might have
1193
    different model parameters.
1194

1195
    Attributes
1196
    ----------
1197
    fls : list
1198
        list of flowline objects
1199

1200
    """
1201

1202
    def __init__(self, gdir, settings_filesuffix='',
9✔
1203
                 fls=None, mb_model_class=MonthlyTIModel,
1204
                 use_inversion_flowlines=False,
1205
                 input_filesuffix='',
1206
                 **kwargs):
1207
        """Initialize.
1208

1209
        Parameters
1210
        ----------
1211
        gdir : GlacierDirectory
1212
            the glacier directory
1213
        settings_filesuffix: str
1214
            You can use a different set of settings by providing a filesuffix.
1215
            This is useful for sensitivity experiments.
1216
        fls : list, optional
1217
            list of flowline objects to use (defaults to 'model_flowlines')
1218
        mb_model_class : MassBalanceModel class
1219
            the MassBalanceModel to use (default is MonthlyTIModel,
1220
            alternatives are e.g. ConstantMassBalance...)
1221
        use_inversion_flowlines: bool, optional
1222
            use 'inversion_flowlines' instead of 'model_flowlines'
1223
        kwargs : kwargs to pass to mb_model_class
1224
        """
1225

1226
        gdir.settings_filesuffix = settings_filesuffix
6✔
1227

1228
        # Read in the flowlines
1229
        if use_inversion_flowlines:
6✔
1230
            fls = gdir.read_pickle('inversion_flowlines')
6✔
1231

1232
        if fls is None:
6✔
1233
            try:
6✔
1234
                fls = gdir.read_pickle('model_flowlines')
6✔
1235
            except FileNotFoundError:
1✔
1236
                raise InvalidWorkflowError('Need a valid `model_flowlines` '
1✔
1237
                                           'file. If you explicitly want to '
1238
                                           'use `inversion_flowlines`, set '
1239
                                           'use_inversion_flowlines=True.')
1240

1241
        self.fls = fls
6✔
1242

1243
        # Initialise the mb models
1244
        self.flowline_mb_models = []
6✔
1245
        for fl in self.fls:
6✔
1246
            # Merged glaciers will need different climate files, use filesuffix
1247
            if (fl.rgi_id is not None) and (fl.rgi_id != gdir.rgi_id):
6✔
1248
                rgi_filesuffix = '_' + fl.rgi_id + input_filesuffix
×
1249
            else:
1250
                rgi_filesuffix = input_filesuffix
6✔
1251

1252
            self.flowline_mb_models.append(
6✔
1253
                mb_model_class(gdir, settings_filesuffix=settings_filesuffix,
1254
                               input_filesuffix=rgi_filesuffix,
1255
                               **kwargs))
1256

1257
        self.valid_bounds = self.flowline_mb_models[-1].valid_bounds
6✔
1258
        self.hemisphere = gdir.hemisphere
6✔
1259

1260
    @property
9✔
1261
    def temp_bias(self):
9✔
1262
        """Temperature bias to add to the original series."""
1263
        return self.flowline_mb_models[0].temp_bias
3✔
1264

1265
    @temp_bias.setter
9✔
1266
    def temp_bias(self, value):
9✔
1267
        """Temperature bias to add to the original series."""
1268
        for mbmod in self.flowline_mb_models:
4✔
1269
            mbmod.temp_bias = value
4✔
1270

1271
    @property
9✔
1272
    def prcp_fac(self):
9✔
1273
        """Precipitation factor to apply to the original series."""
1274
        return self.flowline_mb_models[0].prcp_fac
1✔
1275

1276
    @prcp_fac.setter
9✔
1277
    def prcp_fac(self, value):
9✔
1278
        """Precipitation factor to apply to the original series."""
1279
        for mbmod in self.flowline_mb_models:
1✔
1280
            mbmod.prcp_fac = value
1✔
1281

1282
    @property
9✔
1283
    def bias(self):
9✔
1284
        """Residual bias to apply to the original series."""
1285
        return self.flowline_mb_models[0].bias
5✔
1286

1287
    @bias.setter
9✔
1288
    def bias(self, value):
9✔
1289
        """Residual bias to apply to the original series."""
1290
        for mbmod in self.flowline_mb_models:
1✔
1291
            mbmod.bias = value
1✔
1292

1293
    def is_year_valid(self, year):
9✔
1294
        return self.flowline_mb_models[0].is_year_valid(year)
×
1295

1296
    def get_monthly_mb(self, heights, year=None, fl_id=None, **kwargs):
9✔
1297

1298
        if fl_id is None:
4✔
1299
            raise ValueError('`fl_id` is required for '
×
1300
                             'MultipleFlowlineMassBalance!')
1301

1302
        return self.flowline_mb_models[fl_id].get_monthly_mb(heights,
4✔
1303
                                                             year=year,
1304
                                                             **kwargs)
1305

1306
    def get_annual_mb(self, heights, year=None, fl_id=None, **kwargs):
9✔
1307

1308
        if fl_id is None:
6✔
1309
            raise ValueError('`fl_id` is required for '
×
1310
                             'MultipleFlowlineMassBalance!')
1311

1312
        return self.flowline_mb_models[fl_id].get_annual_mb(heights,
6✔
1313
                                                            year=year,
1314
                                                            **kwargs)
1315

1316
    def get_annual_mb_on_flowlines(self, fls=None, year=None):
9✔
1317
        """Get the MB on all points of the glacier at once.
1318

1319
        Parameters
1320
        ----------
1321
        fls: list, optional
1322
            the list of flowlines to get the mass balance from. Defaults
1323
            to self.fls
1324
        year: float, optional
1325
            the time (in the "floating year" convention)
1326
        Returns
1327
        -------
1328
        Tuple of (heights, widths, mass_balance) 1D arrays
1329
        """
1330

1331
        if fls is None:
4✔
1332
            fls = self.fls
4✔
1333

1334
        heights = []
4✔
1335
        widths = []
4✔
1336
        mbs = []
4✔
1337
        for i, fl in enumerate(fls):
4✔
1338
            h = fl.surface_h
4✔
1339
            heights = np.append(heights, h)
4✔
1340
            widths = np.append(widths, fl.widths)
4✔
1341
            mbs = np.append(mbs, self.get_annual_mb(h, year=year, fl_id=i))
4✔
1342

1343
        return heights, widths, mbs
4✔
1344

1345
    def get_annual_specific_mass_balance(self, fls: list, year: float) -> float:
9✔
1346
        """Get annual specific mass balance from multiple flowlines.
1347

1348
        Parameters
1349
        ----------
1350
        fls : list[oggm.Flowline]
1351
            Flowline instances.
1352
        year : float
1353
            Time in "floating year" convention.
1354

1355
        Returns
1356
        -------
1357
        float
1358
            Annual specific mass balance from multiple flowlines.
1359
        """
1360
        mbs = []
4✔
1361
        widths = []
4✔
1362
        for i, (fl, mb_mod) in enumerate(zip(fls, self.flowline_mb_models)):
4✔
1363
            _widths = fl.widths
4✔
1364
            try:
4✔
1365
                # For rect and parabola don't compute spec mb
1366
                _widths = np.where(fl.thick > 0, _widths, 0)
4✔
1367
            except AttributeError:
4✔
1368
                pass
4✔
1369
            widths.append(_widths)
4✔
1370
            mb = mb_mod.get_annual_mb(fl.surface_h, year=year, fls=fls, fl_id=i)
4✔
1371
            mbs.append(mb * SEC_IN_YEAR * mb_mod.rho)
4✔
1372
        widths = np.concatenate(widths, axis=0)  # 2x faster than np.append
4✔
1373
        mbs = np.concatenate(mbs, axis=0)
4✔
1374
        mbs = weighted_average_1d(mbs, widths)
4✔
1375

1376
        return mbs
4✔
1377

1378
    def get_specific_mb(self, heights=None, widths=None, fls=None, year=None):
9✔
1379
        """Specific mass balance for a given glacier geometry.
1380

1381
        Units: [mm w.e. yr-1], or millimeter water equivalent per year.
1382

1383
        Parameters
1384
        ----------
1385
        heights : ArrayLike, default None
1386
            Altitudes at which the mass balance will be computed.
1387
            Overridden by ``fls`` if provided.
1388
        widths : ArrayLike, default None
1389
            Widths of the flowline (necessary for the weighted average).
1390
            Overridden by ``fls`` if provided.
1391
        fls : list[oggm.Flowline], default None
1392
            List of flowline instances. Alternative to heights and
1393
            widths, and overrides them if provided.
1394
        year : ArrayLike[float] or float, default None
1395
            Year, or a range of years in "floating year" convention.
1396

1397
        Returns
1398
        -------
1399
        np.ndarray
1400
            Specific mass balance (units: mm w.e. yr-1).
1401
        """
1402
        if heights is not None or widths is not None:
4✔
1403
            raise ValueError(
×
1404
                "`heights` and `widths` kwargs do not work with "
1405
                "MultipleFlowlineMassBalance!"
1406
            )
1407

1408
        if fls is None:
4✔
1409
            fls = self.fls
4✔
1410

1411
        stack = []
4✔
1412
        year = np.atleast_1d(year)
4✔
1413
        for mb_yr in year:
4✔
1414
            mbs = self.get_annual_specific_mass_balance(fls=fls, year=mb_yr)
4✔
1415
            stack.append(mbs)
4✔
1416

1417
        return set_array_type(stack)
4✔
1418

1419
    def get_ela(self, year=None, **kwargs):
9✔
1420
        """Get the equilibrium line altitude for a given year.
1421

1422
        The ELA here is not without ambiguity: it computes a mean
1423
        weighted by area.
1424

1425
        Parameters
1426
        ----------
1427
        year : ArrayLike[float] or float, default None
1428
            Year, or a range of years in "floating year" convention.
1429

1430
        Returns
1431
        -------
1432
        float or np.ndarray
1433
            The equilibrium line altitude (ELA) in m.
1434
        """
1435
        stack = []
4✔
1436
        year = np.atleast_1d(year)
4✔
1437
        for mb_yr in year:
4✔
1438
            elas = []
4✔
1439
            areas = []
4✔
1440
            for fl_id, (fl, mb_mod) in enumerate(
4✔
1441
                zip(self.fls, self.flowline_mb_models)
1442
            ):
1443
                elas.append(
4✔
1444
                    mb_mod.get_ela(year=mb_yr, fl_id=fl_id, fls=self.fls)
1445
                )
1446
                areas.append(np.sum(fl.widths))
4✔
1447
            stack.append(weighted_average_1d(elas, areas))
4✔
1448

1449
        return set_array_type(stack)
4✔
1450

1451

1452
def calving_mb(gdir):
9✔
1453
    """Calving mass-loss in specific MB equivalent.
1454

1455
    This is necessary to calibrate the mass balance.
1456
    """
1457

1458
    if not gdir.is_tidewater:
7✔
1459
        return 0.
7✔
1460

1461
    # Ok. Just take the calving rate from cfg and change its units
1462
    # Original units: km3 a-1, to change to mm a-1 (units of specific MB)
1463
    rho = gdir.settings['ice_density']
5✔
1464
    return gdir.inversion_calving_rate * 1e9 * rho / gdir.rgi_area_m2
5✔
1465

1466

1467
def decide_winter_precip_factor(gdir):
9✔
1468
    """Utility function to decide on a precip factor based on winter precip."""
1469

1470
    # We have to decide on a precip factor
1471
    if 'W5E5' not in gdir.settings['baseline_climate']:
3✔
1472
        raise InvalidWorkflowError('prcp_fac from_winter_prcp is only '
×
1473
                                   'compatible with the W5E5 climate '
1474
                                   'dataset!')
1475

1476
    # get non-corrected winter daily mean prcp (kg m-2 day-1)
1477
    # it is easier to get this directly from the raw climate files
1478
    fp = gdir.get_filepath('climate_historical')
3✔
1479
    with xr.open_dataset(fp).prcp as ds_pr:
3✔
1480
        # just select winter months
1481
        if gdir.hemisphere == 'nh':
3✔
1482
            m_winter = [10, 11, 12, 1, 2, 3, 4]
3✔
1483
        else:
1484
            m_winter = [4, 5, 6, 7, 8, 9, 10]
×
1485

1486
        ds_pr_winter = ds_pr.where(ds_pr['time.month'].isin(m_winter), drop=True)
3✔
1487

1488
        # select the correct 41 year time period
1489
        ds_pr_winter = ds_pr_winter.sel(time=slice('1979-01-01', '2019-12-01'))
3✔
1490

1491
        # check if we have the full time period: 41 years * 7 months
1492
        text = ('the climate period has to go from 1979-01 to 2019-12,',
3✔
1493
                'use W5E5 or GSWP3_W5E5 as baseline climate and',
1494
                'repeat the climate processing')
1495
        assert len(ds_pr_winter.time) == 41 * 7, text
3✔
1496
        w_prcp = float((ds_pr_winter / ds_pr_winter.time.dt.daysinmonth).mean())
3✔
1497

1498
    # from MB sandbox calibration to winter MB
1499
    # using t_melt=-1, cte lapse rate, monthly resolution
1500
    a, b = gdir.settings['winter_prcp_fac_ab']
3✔
1501
    prcp_fac = a * np.log(w_prcp) + b
3✔
1502
    # don't allow extremely low/high prcp. factors!!!
1503
    return clip_scalar(prcp_fac,
3✔
1504
                       gdir.settings['prcp_fac_min'],
1505
                       gdir.settings['prcp_fac_max'])
1506

1507

1508
@entity_task(log, writes=['mb_calib'])
9✔
1509
def mb_calibration_from_wgms_mb(gdir, settings_filesuffix='',
9✔
1510
                                observations_filesuffix='',
1511
                                **kwargs):
1512
    """Calibrate for in-situ, annual MB.
1513

1514
    This only works for glaciers which have WGMS data!
1515

1516
    For now this just calls mb_calibration_from_scalar_mb internally,
1517
    but could be cleverer than that if someone wishes to implement it.
1518

1519
    Parameters
1520
    ----------
1521
    gdir : :py:class:`oggm.GlacierDirectory`
1522
        the glacier directory to process
1523
    settings_filesuffix: str
1524
        You can use a different set of settings by providing a filesuffix. This
1525
        is useful for sensitivity experiments. Code-wise the settings_filesuffix
1526
        is set in the @entity-task decorater.
1527
    observations_filesuffix: str
1528
        The observations filesuffix, where the used calibration data will be
1529
        stored. Code-wise the observations_filesuffix is set in the @entity-task
1530
        decorater.
1531
    **kwargs : any kwarg accepted by mb_calibration_from_scalar_mb
1532
    except `ref_mb` and `ref_mb_years`
1533
    """
1534

1535
    # Note that this currently does not work for hydro years (WGMS uses hydro)
1536
    # A way to go would be to teach the mb models to use calendar years
1537
    # internally but still output annual MB in hydro convention.
1538
    mbdf = gdir.get_ref_mb_data()
1✔
1539
    # Keep only valid values
1540
    mbdf = mbdf.loc[~mbdf['ANNUAL_BALANCE'].isnull()]
1✔
1541

1542
    gdir.observations['ref_mb'] = {
1✔
1543
        'value': mbdf['ANNUAL_BALANCE'].mean(),
1544
        'years': mbdf.index.values,
1545
    }
1546

1547
    return mb_calibration_from_scalar_mb(gdir,
1✔
1548
                                         settings_filesuffix=settings_filesuffix,
1549
                                         observations_filesuffix=observations_filesuffix,
1550
                                         **kwargs)
1551

1552

1553
@entity_task(log, writes=['mb_calib'])
9✔
1554
def mb_calibration_from_hugonnet_mb(gdir, *,
9✔
1555
                                    settings_filesuffix='',
1556
                                    observations_filesuffix='',
1557
                                    use_observations_file=False,
1558
                                    ref_mb_period=None,
1559
                                    write_to_gdir=True,
1560
                                    overwrite_gdir=False,
1561
                                    use_regional_avg=False,
1562
                                    override_missing=None,
1563
                                    use_2d_mb=False,
1564
                                    informed_threestep=False,
1565
                                    calibrate_param1='melt_f',
1566
                                    calibrate_param2=None,
1567
                                    calibrate_param3=None,
1568
                                    mb_model_class=MonthlyTIModel,
1569
                                    ):
1570
    """Calibrate for geodetic MB data from Hugonnet et al., 2021.
1571

1572
    The data table can be obtained with utils.get_geodetic_mb_dataframe().
1573
    It is equivalent to the original data from Hugonnet, but has some outlier
1574
    values filtered. See this notebook* for more details.
1575

1576
    https://nbviewer.org/urls/cluster.klima.uni-bremen.de/~oggm/geodetic_ref_mb/convert_vold1.ipynb
1577

1578
    This glacier-specific calibration can be replaced by a region-wide calibration
1579
    by using regional averages (same units: mm w.e.) instead of the glacier
1580
    specific averages.
1581

1582
    The problem of calibrating many unknown parameters on geodetic data is
1583
    currently unsolved. This is OGGM's current take, based on trial and
1584
    error and based on ideas from the literature.
1585

1586
    Parameters
1587
    ----------
1588
    gdir : :py:class:`oggm.GlacierDirectory`
1589
        the glacier directory to calibrate
1590
    settings_filesuffix: str
1591
        You can use a different set of settings by providing a filesuffix. This
1592
        is useful for sensitivity experiments. Code-wise the settings_filesuffix
1593
        is set in the @entity-task decorater.
1594
    observations_filesuffix: str
1595
        The observations filesuffix, where the used calibration data will be
1596
        stored. Code-wise the observations_filesuffix is set in the @entity-task
1597
        decorater.
1598
    use_observations_file : bool
1599
        By default this function reads the data from Hugonnet and adds it to the
1600
        observations file. If you want to use different observations within this
1601
        function you can set this to True. This can be useful for sensitivity
1602
        tests. Default is False.
1603
    ref_mb_period : str, default: PARAMS['geodetic_mb_period']
1604
        one of '2000-01-01_2010-01-01', '2010-01-01_2020-01-01',
1605
        '2000-01-01_2020-01-01'. If `ref_mb` is set, this should still match
1606
        the same format but can be any date.
1607
    write_to_gdir : bool
1608
        whether to write the results of the calibration to the glacier
1609
        directory. If True (the default), this will be saved as `mb_calib.json`
1610
        and be used by the MassBalanceModel class as parameters in subsequent
1611
        tasks.
1612
    overwrite_gdir : bool
1613
        if a `mb_calib.json` exists, this task won't overwrite it per default.
1614
        Set this to True to enforce overwriting (i.e. with consequences for the
1615
        future workflow).
1616
    use_regional_avg : bool
1617
        use the regional average instead of the glacier specific one.
1618
    override_missing : scalar
1619
        if the reference geodetic data is not available, use this value instead
1620
        (mostly for testing with exotic datasets, but could be used to open
1621
        the door to using other datasets).
1622
    use_2d_mb : bool
1623
        Set to True if the mass balance calibration has to be done of the 2D mask
1624
        of the glacier (for fully distributed runs only).
1625
    informed_threestep : bool
1626
        the magic method Fabi found out one day before release.
1627
        Overrides the calibrate_param order below.
1628
    calibrate_param1 : str
1629
        in the three-step calibration, the name of the first parameter
1630
        to calibrate (one of 'melt_f', 'temp_bias', 'prcp_fac').
1631
    calibrate_param2 : str
1632
        in the three-step calibration, the name of the second parameter
1633
        to calibrate (one of 'melt_f', 'temp_bias', 'prcp_fac'). If not
1634
        set and the algorithm cannot match observations, it will raise an
1635
        error.
1636
    calibrate_param3 : str
1637
        in the three-step calibration, the name of the third parameter
1638
        to calibrate (one of 'melt_f', 'temp_bias', 'prcp_fac'). If not
1639
        set and the algorithm cannot match observations, it will raise an
1640
        error.
1641
    mb_model_class : MassBalanceModel class
1642
        the MassBalanceModel to use for the calibration. Needs to use the
1643
        same parameters as MonthlyTIModel (the default): melt_f,
1644
        temp_bias, prcp_fac.
1645

1646
    Returns
1647
    -------
1648
    the calibrated parameters as dict
1649
    """
1650
    # instead of the given values by hugonnet use the once from the provided
1651
    # observations file
1652
    if use_observations_file:
6✔
1653
        ref_mb_use = gdir.observations['ref_mb']
2✔
1654
    else:
1655
        if not ref_mb_period:
6✔
1656
            ref_mb_period = gdir.settings['geodetic_mb_period']
6✔
1657

1658
        # Get the reference data
1659
        ref_mb_err = np.nan
6✔
1660
        if use_regional_avg:
6✔
1661
            ref_mb_df = 'table_hugonnet_regions_10yr_20yr_ar6period.csv'
1✔
1662
            ref_mb_df = pd.read_csv(get_demo_file(ref_mb_df))
1✔
1663
            ref_mb_df = ref_mb_df.loc[ref_mb_df.period == ref_mb_period].set_index('reg')
1✔
1664
            # dmdtda already in kg m-2 yr-1
1665
            ref_mb = ref_mb_df.loc[int(gdir.rgi_region), 'dmdtda']
1✔
1666
            ref_mb_err = ref_mb_df.loc[int(gdir.rgi_region), 'err_dmdtda']
1✔
1667
        else:
1668
            try:
6✔
1669
                ref_mb_df = get_geodetic_mb_dataframe().loc[gdir.rgi_id]
6✔
1670
                ref_mb_df = ref_mb_df.loc[ref_mb_df['period'] == ref_mb_period]
6✔
1671
                # dmdtda: in meters water-equivalent per year -> we convert to kg m-2 yr-1
1672
                ref_mb = ref_mb_df['dmdtda'].iloc[0] * 1000
6✔
1673
                ref_mb_err = ref_mb_df['err_dmdtda'].iloc[0] * 1000
6✔
1674
            except KeyError:
2✔
1675
                if override_missing is None:
2✔
NEW
1676
                    raise
×
1677
                ref_mb = override_missing
2✔
1678

1679
        ref_mb_use = {
6✔
1680
            'value': ref_mb,
1681
            'period': ref_mb_period,
1682
            'err': ref_mb_err,
1683
        }
1684

1685
    gdir.observations['ref_mb'] = ref_mb_use
6✔
1686

1687
    temp_bias = 0
6✔
1688
    if gdir.settings['use_temp_bias_from_file']:
6✔
1689
        climinfo = gdir.get_climate_info()
3✔
1690
        if 'w5e5' not in climinfo['baseline_climate_source'].lower():
3✔
1691
            raise InvalidWorkflowError('use_temp_bias_from_file currently '
×
1692
                                       'only available for W5E5 data.')
1693
        bias_df = get_temp_bias_dataframe()
3✔
1694
        ref_lon = climinfo['baseline_climate_ref_pix_lon']
3✔
1695
        ref_lat = climinfo['baseline_climate_ref_pix_lat']
3✔
1696
        # Take nearest
1697
        dis = ((bias_df.lon_val - ref_lon)**2 + (bias_df.lat_val - ref_lat)**2)**0.5
3✔
1698
        sel_df = bias_df.iloc[np.argmin(dis)]
3✔
1699
        temp_bias = sel_df['median_temp_bias_w_err_grouped']
3✔
1700
        assert np.isfinite(temp_bias), 'Temp bias not finite?'
3✔
1701

1702
    if informed_threestep:
6✔
1703
        if not gdir.settings['use_temp_bias_from_file']:
3✔
1704
            raise InvalidParamsError('With `informed_threestep` you need to '
×
1705
                                     'set `use_temp_bias_from_file`.')
1706
        if not gdir.settings['use_winter_prcp_fac']:
3✔
1707
            raise InvalidParamsError('With `informed_threestep` you need to '
×
1708
                                     'set `use_winter_prcp_fac`.')
1709

1710
        # Some magic heuristics - we just decide to calibrate
1711
        # precip -> melt_f -> temp but informed by previous data.
1712

1713
        # Temp bias was decided anyway, we keep as previous value and
1714
        # allow it to vary as last resort
1715

1716
        # We use the precip factor but allow it to vary between 0.8, 1.2 of
1717
        # the previous value (uncertainty).
1718
        prcp_fac = decide_winter_precip_factor(gdir)
3✔
1719
        mi, ma = gdir.settings['prcp_fac_min'], gdir.settings['prcp_fac_max']
3✔
1720
        prcp_fac_min = clip_scalar(prcp_fac * 0.8, mi, ma)
3✔
1721
        prcp_fac_max = clip_scalar(prcp_fac * 1.2, mi, ma)
3✔
1722

1723
        return mb_calibration_from_scalar_mb(gdir,
3✔
1724
                                             settings_filesuffix=settings_filesuffix,
1725
                                             observations_filesuffix=observations_filesuffix,
1726
                                             write_to_gdir=write_to_gdir,
1727
                                             overwrite_gdir=overwrite_gdir,
1728
                                             use_2d_mb=use_2d_mb,
1729
                                             calibrate_param1='prcp_fac',
1730
                                             calibrate_param2='melt_f',
1731
                                             calibrate_param3='temp_bias',
1732
                                             prcp_fac=prcp_fac,
1733
                                             prcp_fac_min=prcp_fac_min,
1734
                                             prcp_fac_max=prcp_fac_max,
1735
                                             temp_bias=temp_bias,
1736
                                             mb_model_class=mb_model_class,
1737
                                             )
1738

1739
    else:
1740
        return mb_calibration_from_scalar_mb(gdir,
6✔
1741
                                             settings_filesuffix=settings_filesuffix,
1742
                                             observations_filesuffix=observations_filesuffix,
1743
                                             write_to_gdir=write_to_gdir,
1744
                                             overwrite_gdir=overwrite_gdir,
1745
                                             use_2d_mb=use_2d_mb,
1746
                                             calibrate_param1=calibrate_param1,
1747
                                             calibrate_param2=calibrate_param2,
1748
                                             calibrate_param3=calibrate_param3,
1749
                                             temp_bias=temp_bias,
1750
                                             mb_model_class=mb_model_class,
1751
                                             )
1752

1753

1754
@entity_task(log, writes=['mb_calib'])
9✔
1755
def mb_calibration_from_scalar_mb(gdir, *,
9✔
1756
                                  settings_filesuffix='',
1757
                                  observations_filesuffix='',
1758
                                  overwrite_observations=False,
1759
                                  ref_mb=None,
1760
                                  ref_mb_err=None,
1761
                                  ref_mb_period=None,
1762
                                  ref_mb_years=None,
1763
                                  write_to_gdir=True,
1764
                                  overwrite_gdir=False,
1765
                                  use_2d_mb=False,
1766
                                  calibrate_param1='melt_f',
1767
                                  calibrate_param2=None,
1768
                                  calibrate_param3=None,
1769
                                  melt_f=None,
1770
                                  melt_f_min=None,
1771
                                  melt_f_max=None,
1772
                                  prcp_fac=None,
1773
                                  prcp_fac_min=None,
1774
                                  prcp_fac_max=None,
1775
                                  temp_bias=None,
1776
                                  temp_bias_min=None,
1777
                                  temp_bias_max=None,
1778
                                  mb_model_class=MonthlyTIModel,
1779
                                  ):
1780
    """Determine the mass balance parameters from a scalar mass-balance value.
1781

1782
    This calibrates the mass balance parameters using a reference average
1783
    MB data over a given period (e.g. average in-situ SMB or geodetic MB).
1784
    This flexible calibration allows to calibrate three parameters one after
1785
    another. The first parameter is varied between two chosen values (a range)
1786
    until the ref MB value is matched. If this fails, the second parameter
1787
    can be changed, etc.
1788

1789
    This can be used for example to apply the "three-step calibration"
1790
    introduced by Huss & Hock 2015, but you can choose any order of
1791
    calibration.
1792

1793
    This task can be called by other, "higher level" tasks, for example
1794
    :py:func:`oggm.core.massbalance.mb_calibration_from_geodetic_mb` or
1795
    :py:func:`oggm.core.massbalance.mb_calibration_from_wgms_mb`.
1796

1797
    Note that this does not compute the apparent mass balance at
1798
    the same time - users need to run `apparent_mb_from_any_mb after`
1799
    calibration.
1800

1801
    Parameters
1802
    ----------
1803
    gdir : :py:class:`oggm.GlacierDirectory`
1804
        the glacier directory to calibrate
1805
    settings_filesuffix: str
1806
        You can use a different set of settings by providing a filesuffix. This
1807
        is useful for sensitivity experiments. Code-wise the settings_filesuffix
1808
        is set in the @entity-task decorater.
1809
    observations_filesuffix: str
1810
        You can provide a filesuffix for the mb observations to use. If you
1811
        provide ref_mb, ref_mb_err, ref_period and/or ref_mb_years, then this
1812
        values will be stored in the observations file, if ref_mb is not already
1813
        present. If you want to force to use the provided values and override
1814
        the current ones, set overwrite_observations to True. Code-wise the
1815
        observations_filesuffix is set in the @entity-task decorater.
1816
    overwrite_observations : bool
1817
        If you want to overwrite already existing observation values in the
1818
        provided observations file set this to True. Default is False.
1819
    ref_mb : float, required
1820
        the reference mass balance to match (units: kg m-2 yr-1)
1821
        It is required here - if you want to use available observations,
1822
        use :py:func:`oggm.core.massbalance.mb_calibration_from_geodetic_mb`
1823
        or :py:func:`oggm.core.massbalance.mb_calibration_from_wgms_mb`.
1824
    ref_mb_err : float, optional
1825
        currently only used for logging - it is not used in the calibration.
1826
    ref_mb_period : str, optional
1827
        date format - for example '2000-01-01_2010-01-01'. If this is not
1828
        set, ref_mb_years needs to be set.
1829
    ref_mb_years : tuple of length 2 (range) or list of years.
1830
        convenience kwarg to override ref_period. If a tuple of length 2 is
1831
        given, all years between this range (excluding the last one) are used.
1832
        If a list  of years is given, all these will be used (useful for
1833
        data with gaps)
1834
    write_to_gdir : bool
1835
        whether to write the results of the calibration to the glacier
1836
        directory. If True (the default), this will be saved as `mb_calib.json`
1837
        and be used by the MassBalanceModel class as parameters in subsequent
1838
        tasks.
1839
    overwrite_gdir : bool
1840
        if mass balance parameters exists, this task won't overwrite it per
1841
        default. Set this to True to enforce overwriting (i.e. with consequences
1842
        for the future workflow).
1843
    use_2d_mb : bool
1844
        Set to True if the mass balance calibration has to be done of the 2D mask
1845
        of the glacier (for fully distributed runs only).
1846
    mb_model_class : MassBalanceModel class
1847
        the MassBalanceModel to use for the calibration. Needs to use the
1848
        same parameters as MonthlyTIModel (the default): melt_f,
1849
        temp_bias, prcp_fac.
1850
    calibrate_param1 : str
1851
        in the three-step calibration, the name of the first parameter
1852
        to calibrate (one of 'melt_f', 'temp_bias', 'prcp_fac').
1853
    calibrate_param2 : str
1854
        in the three-step calibration, the name of the second parameter
1855
        to calibrate (one of 'melt_f', 'temp_bias', 'prcp_fac'). If not
1856
        set and the algorithm cannot match observations, it will raise an
1857
        error.
1858
    calibrate_param3 : str
1859
        in the three-step calibration, the name of the third parameter
1860
        to calibrate (one of 'melt_f', 'temp_bias', 'prcp_fac'). If not
1861
        set and the algorithm cannot match observations, it will raise an
1862
        error.
1863
    melt_f: float
1864
        the default value to use as melt factor (or the starting value when
1865
        optimizing MB). Defaults to gdir.settings['melt_f'].
1866
    melt_f_min: float
1867
        the minimum accepted value for the melt factor during optimisation.
1868
        Defaults to gdir.settings['melt_f_min'].
1869
    melt_f_max: float
1870
        the maximum accepted value for the melt factor during optimisation.
1871
        Defaults to gdir.settings['melt_f_max'].
1872
    prcp_fac: float
1873
        the default value to use as precipitation scaling factor
1874
        (or the starting value when optimizing MB). Defaults to the method
1875
        chosen in `params.cfg` (winter prcp or global factor).
1876
    prcp_fac_min: float
1877
        the minimum accepted value for the precipitation scaling factor during
1878
        optimisation. Defaults to gdir.settings['prcp_fac_min'].
1879
    prcp_fac_max: float
1880
        the maximum accepted value for the precipitation scaling factor during
1881
        optimisation. Defaults to gdir.settings['prcp_fac_max'].
1882
    temp_bias: float
1883
        the default value to use as temperature bias (or the starting value when
1884
        optimizing MB). Defaults to 0.
1885
    temp_bias_min: float
1886
        the minimum accepted value for the temperature bias during optimisation.
1887
        Defaults to gdir.settings['temp_bias_min'].
1888
    temp_bias_max: float
1889
        the maximum accepted value for the temperature bias during optimisation.
1890
        Defaults to gdir.settings['temp_bias_max'].
1891
    """
1892

1893
    # Param constraints
1894
    if melt_f_min is None:
7✔
1895
        melt_f_min = gdir.settings['melt_f_min']
7✔
1896
    if melt_f_max is None:
7✔
1897
        melt_f_max = gdir.settings['melt_f_max']
7✔
1898
    if prcp_fac_min is None:
7✔
1899
        prcp_fac_min = gdir.settings['prcp_fac_min']
7✔
1900
    if prcp_fac_max is None:
7✔
1901
        prcp_fac_max = gdir.settings['prcp_fac_max']
7✔
1902
    if temp_bias_min is None:
7✔
1903
        temp_bias_min = gdir.settings['temp_bias_min']
7✔
1904
    if temp_bias_max is None:
7✔
1905
        temp_bias_max = gdir.settings['temp_bias_max']
7✔
1906
    if ref_mb_years is not None and ref_mb_period is not None:
7✔
1907
        raise InvalidParamsError('Cannot set `ref_mb_years` and `ref_period` '
×
1908
                                 'at the same time.')
1909

1910
    if not use_2d_mb:
7✔
1911
        fls = gdir.read_pickle('inversion_flowlines')
7✔
1912
    else:
1913
        # if the 2D data is used, the flowline is not needed.
1914
        fls = None
1✔
1915
        # get the 2D data
1916
        fp = gdir.get_filepath('gridded_data')
1✔
1917
        with xr.open_dataset(fp) as ds:
1✔
1918
            # 'topo' instead of 'topo_smoothed'?
1919
            heights = ds.topo_smoothed.data[ds.glacier_mask.data == 1]
1✔
1920
            widths = np.ones(len(heights))
1✔
1921

1922
    # handle which ref mb to use (provided or in observations file)
1923
    ref_mb_provided = {}
7✔
1924
    if ref_mb is not None:
7✔
1925
        ref_mb_provided['value'] = ref_mb
6✔
1926
    if ref_mb_err is not None:
7✔
1927
        ref_mb_provided['err'] = ref_mb_err
2✔
1928
    if ref_mb_period is not None:
7✔
1929
        ref_mb_provided['period'] = ref_mb_period
6✔
1930
    if ref_mb_years is not None:
7✔
1931
        ref_mb_provided['years'] = ref_mb_years
2✔
1932

1933
    if 'ref_mb' in gdir.observations:
7✔
1934
        ref_mb_in_file = gdir.observations['ref_mb']
6✔
1935
    else:
1936
        ref_mb_in_file = None
6✔
1937

1938
    # if nothing is provided raise an error
1939
    if (ref_mb_provided == {}) and (ref_mb_in_file is None):
7✔
1940
        raise InvalidWorkflowError(
2✔
1941
            'You have not provided an reference mass balance! Either add it to '
1942
            'the observations file '
1943
            f'({os.path.basename(gdir.observations.path)}), or pass it through '
1944
            f'kwargs (ref_mb, ref_mb_err, ref_mb_period/ref_mb_years.')
1945

1946
    # here handle different cases of provided values for the ref mb
1947
    if (ref_mb_in_file is None) or (ref_mb_in_file is not None and
7✔
1948
                                    overwrite_observations):
1949
        gdir.observations['ref_mb'] = ref_mb_provided
6✔
1950
        ref_mb_use = ref_mb_provided
6✔
1951
    elif ref_mb_in_file is not None and ref_mb_provided == {}:
6✔
1952
        # only provided in file, this is ok so continue
1953
        ref_mb_use = ref_mb_in_file
6✔
1954
    else:
1955
        # if the provided is the same as the one stored in the file it is fine
1956
        if ref_mb_in_file != ref_mb_provided:
3✔
1957
            raise InvalidWorkflowError(
2✔
1958
                'You provided a reference mass balance, but their is already '
1959
                'one stored in the current observation file '
1960
                f'({os.path.basename(gdir.observations.path)}). If you want to '
1961
                'overwrite set overwrite_observations = True.')
1962
        else:
1963
            ref_mb_use = ref_mb_in_file
3✔
1964

1965
    # now we can extract the actual values we want to use
1966
    ref_mb = ref_mb_use['value']
7✔
1967
    if 'err' in ref_mb_use:
7✔
1968
        ref_mb_err = ref_mb_use['err']
6✔
1969
    if 'period' in ref_mb_use:
7✔
1970
        ref_mb_period = ref_mb_use['period']
7✔
1971
    if 'years' in ref_mb_use:
7✔
1972
        ref_mb_years = ref_mb_use['years']
3✔
1973

1974
    # Let's go
1975
    # Climate period
1976
    if ref_mb_years is not None:
7✔
1977
        if len(ref_mb_years) > 2:
3✔
1978
            years = np.asarray(ref_mb_years)
1✔
1979
            ref_mb_period = 'custom'
1✔
1980
        else:
1981
            years = np.arange(*ref_mb_years)
2✔
1982
            ref_mb_period = f'{ref_mb_years[0]}-01-01_{ref_mb_years[1]}-01-01'
2✔
1983
        gdir.observations['ref_mb']['period'] = ref_mb_period
3✔
1984
    elif ref_mb_period is not None:
7✔
1985
        y0, y1 = ref_mb_period.split('_')
7✔
1986
        y0 = int(y0.split('-')[0])
7✔
1987
        y1 = int(y1.split('-')[0])
7✔
1988
        years = np.arange(y0, y1)
7✔
1989
    else:
1990
        raise InvalidParamsError('One of `ref_mb_years` or `ref_period` '
×
1991
                                 'is required for calibration.')
1992

1993
    # Do we have a calving glacier?
1994
    cmb = calving_mb(gdir)
7✔
1995
    if cmb != 0:
7✔
1996
        raise NotImplementedError('Calving with geodetic MB is not implemented '
1997
                                  'yet, but it should actually work. Well keep '
1998
                                  'you posted!')
1999

2000
    # Ok, regardless on how we want to calibrate, we start with defaults
2001
    if melt_f is None:
7✔
2002
        melt_f = gdir.settings.defaults['melt_f']
7✔
2003

2004
    if prcp_fac is None:
7✔
2005
        if gdir.settings['use_winter_prcp_fac']:
7✔
2006
            # Some sanity check
2007
            if gdir.settings['prcp_fac'] is not None:
×
2008
                raise InvalidWorkflowError("Set PARAMS['prcp_fac'] to None "
×
2009
                                           "if using PARAMS['winter_prcp_factor'].")
2010
            prcp_fac = decide_winter_precip_factor(gdir)
×
2011
        else:
2012
            prcp_fac = gdir.settings.defaults['prcp_fac']
7✔
2013
            if prcp_fac is None:
7✔
2014
                raise InvalidWorkflowError("Set either PARAMS['use_winter_prcp_fac'] "
×
2015
                                           "or PARAMS['winter_prcp_factor'].")
2016

2017
    if temp_bias is None:
7✔
2018
        temp_bias = 0
6✔
2019

2020
    # Create the MB model we will calibrate
2021
    mb_mod = mb_model_class(gdir,
7✔
2022
                            melt_f=melt_f,
2023
                            temp_bias=temp_bias,
2024
                            prcp_fac=prcp_fac,
2025
                            check_calib_params=False,
2026
                            settings_filesuffix=settings_filesuffix)
2027

2028
    # Check that the years are available
2029
    for y in years:
7✔
2030
        if not mb_mod.is_year_valid(y):
7✔
2031
            raise ValueError(f'year {y} out of the valid time bounds: '
×
2032
                             f'[{mb_mod.ys}, {mb_mod.ye}]')
2033

2034
    if calibrate_param1 == 'melt_f':
7✔
2035
        min_range, max_range = melt_f_min, melt_f_max
7✔
2036
    elif calibrate_param1 == 'prcp_fac':
5✔
2037
        min_range, max_range = prcp_fac_min, prcp_fac_max
4✔
2038
    elif calibrate_param1 == 'temp_bias':
2✔
2039
        min_range, max_range = temp_bias_min, temp_bias_max
2✔
2040
    else:
2041
        raise InvalidParamsError("calibrate_param1 must be one of "
×
2042
                                 "['melt_f', 'prcp_fac', 'temp_bias']")
2043

2044
    def to_minimize(x, model_attr):
7✔
2045
        # Set the new attr value
2046
        setattr(mb_mod, model_attr, x)
7✔
2047
        if use_2d_mb:
7✔
2048
            out = mb_mod.get_specific_mb(heights=heights, widths=widths, year=years).mean()
1✔
2049
        else:
2050
            out = mb_mod.get_specific_mb(fls=fls, year=years).mean()
7✔
2051
        return np.mean(out - ref_mb)
7✔
2052

2053
    try:
7✔
2054
        optim_param1 = optimize.brentq(to_minimize,
7✔
2055
                                       min_range, max_range,
2056
                                       args=(calibrate_param1,)
2057
                                       )
2058
    except ValueError:
2✔
2059
        if not calibrate_param2:
2✔
2060
            raise RuntimeError(f'{gdir.rgi_id}: ref mb not matched. '
1✔
2061
                               f'Try to set calibrate_param2.')
2062

2063
        # Check which direction we need to go
2064
        diff_1 = to_minimize(min_range, calibrate_param1)
2✔
2065
        diff_2 = to_minimize(max_range, calibrate_param1)
2✔
2066
        optim_param1 = min_range if abs(diff_1) < abs(diff_2) else max_range
2✔
2067
        setattr(mb_mod, calibrate_param1, optim_param1)
2✔
2068

2069
        # Second step
2070
        if calibrate_param2 == 'melt_f':
2✔
2071
            min_range, max_range = melt_f_min, melt_f_max
1✔
2072
        elif calibrate_param2 == 'prcp_fac':
1✔
2073
            min_range, max_range = prcp_fac_min, prcp_fac_max
×
2074
        elif calibrate_param2 == 'temp_bias':
1✔
2075
            min_range, max_range = temp_bias_min, temp_bias_max
1✔
2076
        else:
2077
            raise InvalidParamsError("calibrate_param2 must be one of "
×
2078
                                     "['melt_f', 'prcp_fac', 'temp_bias']")
2079
        try:
2✔
2080
            optim_param2 = optimize.brentq(to_minimize,
2✔
2081
                                           min_range, max_range,
2082
                                           args=(calibrate_param2,)
2083
                                           )
2084
        except ValueError:
1✔
2085
            # Third step
2086
            if not calibrate_param3:
1✔
2087
                raise RuntimeError(f'{gdir.rgi_id}: ref mb not matched. '
1✔
2088
                                   f'Try to set calibrate_param3.')
2089

2090
            # Check which direction we need to go
2091
            diff_1 = to_minimize(min_range, calibrate_param2)
1✔
2092
            diff_2 = to_minimize(max_range, calibrate_param2)
1✔
2093
            optim_param2 = min_range if abs(diff_1) < abs(diff_2) else max_range
1✔
2094
            setattr(mb_mod, calibrate_param2, optim_param2)
1✔
2095

2096
            # Third step
2097
            if calibrate_param3 == 'melt_f':
1✔
2098
                min_range, max_range = melt_f_min, melt_f_max
1✔
2099
            elif calibrate_param3 == 'prcp_fac':
×
2100
                min_range, max_range = prcp_fac_min, prcp_fac_max
×
2101
            elif calibrate_param3 == 'temp_bias':
×
2102
                min_range, max_range = temp_bias_min, temp_bias_max
×
2103
            else:
2104
                raise InvalidParamsError("calibrate_param3 must be one of "
×
2105
                                         "['melt_f', 'prcp_fac', 'temp_bias']")
2106
            try:
1✔
2107
                optim_param3 = optimize.brentq(to_minimize,
1✔
2108
                                               min_range, max_range,
2109
                                               args=(calibrate_param3,)
2110
                                               )
2111
            except ValueError:
1✔
2112
                raise RuntimeError(f'{gdir.rgi_id}: we tried very hard but we '
1✔
2113
                                   f'could not find a combination of '
2114
                                   f'parameters that works for this ref mb.')
2115

2116
            if calibrate_param3 == 'melt_f':
1✔
2117
                melt_f = optim_param3
1✔
2118
            elif calibrate_param3 == 'prcp_fac':
×
2119
                prcp_fac = optim_param3
×
2120
            elif calibrate_param3 == 'temp_bias':
×
2121
                temp_bias = optim_param3
×
2122

2123
        if calibrate_param2 == 'melt_f':
2✔
2124
            melt_f = optim_param2
1✔
2125
        elif calibrate_param2 == 'prcp_fac':
1✔
2126
            prcp_fac = optim_param2
×
2127
        elif calibrate_param2 == 'temp_bias':
1✔
2128
            temp_bias = optim_param2
1✔
2129

2130
    if calibrate_param1 == 'melt_f':
7✔
2131
        melt_f = optim_param1
7✔
2132
    elif calibrate_param1 == 'prcp_fac':
5✔
2133
        prcp_fac = optim_param1
4✔
2134
    elif calibrate_param1 == 'temp_bias':
2✔
2135
        temp_bias = optim_param1
2✔
2136

2137
    # Store parameters
2138
    df = {}
7✔
2139
    df['rgi_id'] = gdir.rgi_id
7✔
2140
    df['bias'] = 0
7✔
2141
    df['melt_f'] = melt_f
7✔
2142
    df['prcp_fac'] = prcp_fac
7✔
2143
    df['temp_bias'] = temp_bias
7✔
2144
    # What did we try to match?
2145
    df['reference_mb'] = ref_mb
7✔
2146
    df['reference_mb_err'] = ref_mb_err
7✔
2147
    df['reference_period'] = ref_mb_period
7✔
2148

2149
    # Add the climate related params to the GlacierDir to make sure
2150
    # other tools cannot fool around without re-calibration
2151
    df['mb_global_params'] = {k: cfg.PARAMS[k] for k in MB_GLOBAL_PARAMS}
7✔
2152
    df['baseline_climate_source'] = gdir.get_climate_info()['baseline_climate_source']
7✔
2153

2154
    # Write
2155
    if write_to_gdir:
7✔
2156
        if any(key in gdir.settings
7✔
2157
               for key in ['melt_f', 'prcp_fac', 'temp_bias']) and not overwrite_gdir:
2158
            raise InvalidWorkflowError('Their are already mass balance parameters '
2✔
2159
                                       'stored in the settings file. Set '
2160
                                       '`overwrite_gdir` to True if you want to '
2161
                                       'overwrite a previous calibration.')
2162
        for key in ['rgi_id', 'bias', 'melt_f', 'prcp_fac', 'temp_bias',
7✔
2163
                    'reference_mb', 'reference_mb_err', 'reference_period',
2164
                    'mb_global_params', 'baseline_climate_source']:
2165
            gdir.settings[key] = df[key]
7✔
2166

2167
    return df
7✔
2168

2169

2170
@entity_task(log, writes=['mb_calib'])
9✔
2171
def perturbate_mb_params(gdir, perturbation=None, reset_default=False, filesuffix=''):
9✔
2172
    """Replaces pre-calibrated MB params with perturbed ones for this glacier.
2173

2174
    It simply replaces the existing `mb_calib.json` file with an
2175
    updated one with perturbed parameters. The original ones
2176
    are stored in the file for re-use after perturbation.
2177

2178
    Users can change the following 4 parameters:
2179
    - 'melt_f': unit [kg m-2 day-1 K-1], the melt factor
2180
    - 'prcp_fac': unit [-], the precipitation factor
2181
    - 'temp_bias': unit [K], the temperature correction applied to the timeseries
2182
    - 'bias': unit [mm we yr-1], *substracted* from the computed MB. Rarely used.
2183

2184
    All parameter perturbations are additive, i.e. the value
2185
    provided by the user is added to the *precalibrated* value.
2186
    For example, `temp_bias=1` means that the temp_bias used by the
2187
    model will be the precalibrated one, plus 1 Kelvin.
2188

2189
    The only exception is prpc_fac, which is multiplicative.
2190
    For example prcp_fac=1 will leave the precalibrated prcp_fac unchanged,
2191
    while 2 will double it.
2192

2193
    Parameters
2194
    ----------
2195
    perturbation : dict
2196
        the parameters to change and the associated value (see doc above)
2197
    reset_default : bool
2198
        reset the parameters to their original value. This might be
2199
        unnecessary if using the filesuffix mechanism.
2200
    filesuffix : str
2201
        write the modified parameters in a separate mb_calib.json file
2202
        with the filesuffix appended. This can then be read by the
2203
        MassBalanceModel for example instead of the default one.
2204
        Note that it's always the default, precalibrated params
2205
        file which is read to start with.
2206
    """
2207
    df = gdir.read_yml('settings')
1✔
2208

2209
    # Save original params if not there
2210
    if 'bias_orig' not in df:
1✔
2211
        for k in ['bias', 'melt_f', 'prcp_fac', 'temp_bias']:
1✔
2212
            df[k + '_orig'] = df[k]
1✔
2213

2214
    if reset_default:
1✔
2215
        for k in ['bias', 'melt_f', 'prcp_fac', 'temp_bias']:
1✔
2216
            df[k] = df[k + '_orig']
1✔
2217
        gdir.write_yml(df, 'settings', filesuffix=filesuffix)
1✔
2218
        return df
1✔
2219

2220
    for k, v in perturbation.items():
1✔
2221
        if k == 'prcp_fac':
1✔
2222
            df[k] = df[k + '_orig'] * v
1✔
2223
        elif k in ['bias', 'melt_f', 'temp_bias']:
1✔
2224
            df[k] = df[k + '_orig'] + v
1✔
2225
        else:
2226
            raise InvalidParamsError(f'Perturbation not valid: {k}')
×
2227

2228
    gdir.write_yml(df, 'settings', filesuffix=filesuffix)
1✔
2229
    return df
1✔
2230

2231

2232
def _check_terminus_mass_flux(gdir, fls):
9✔
2233
    # Check that we have done this correctly
2234
    rho = gdir.settings['ice_density']
7✔
2235
    cmb = calving_mb(gdir)
7✔
2236

2237
    # This variable is in "sensible" units normalized by width
2238
    flux = fls[-1].flux_out
7✔
2239
    aflux = flux * (gdir.grid.dx ** 2) / rho * 1e-9  # km3 ice per year
7✔
2240

2241
    # If not marine and a bit far from zero, warning
2242
    if cmb == 0 and not np.allclose(flux, 0, atol=0.01):
7✔
2243
        log.info('(%s) flux should be zero, but is: '
×
2244
                 '%.4f km3 ice yr-1', gdir.rgi_id, aflux)
2245

2246
    # If not marine and quite far from zero, error
2247
    if cmb == 0 and not np.allclose(flux, 0, atol=1):
7✔
2248
        msg = ('({}) flux should be zero, but is: {:.4f} km3 ice yr-1'
×
2249
               .format(gdir.rgi_id, aflux))
2250
        raise MassBalanceCalibrationError(msg)
×
2251

2252

2253
@entity_task(log, writes=['inversion_flowlines', 'linear_mb_params'])
9✔
2254
def apparent_mb_from_linear_mb(gdir, settings_filesuffix='',
9✔
2255
                               mb_gradient=3., ela_h=None):
2256
    """Compute apparent mb from a linear mass balance assumption (for testing).
2257

2258
    This is for testing currently, but could be used as alternative method
2259
    for the inversion quite easily.
2260

2261
    Parameters
2262
    ----------
2263
    gdir : :py:class:`oggm.GlacierDirectory`
2264
        the glacier directory to process
2265
    settings_filesuffix: str
2266
        You can use a different set of settings by providing a filesuffix. This
2267
        is useful for sensitivity experiments. Code-wise the settings_filesuffix
2268
        is set in the @entity-task decorater.
2269
    """
2270

2271
    # Do we have a calving glacier?
2272
    cmb = calving_mb(gdir)
3✔
2273
    is_calving = cmb != 0.
3✔
2274

2275
    # Get the height and widths along the fls
2276
    h, w = gdir.get_inversion_flowline_hw()
3✔
2277

2278
    # Now find the ELA till the integrated mb is zero
2279
    from oggm.core.massbalance import LinearMassBalance
3✔
2280

2281
    def to_minimize(ela_h):
3✔
2282
        mbmod = LinearMassBalance(ela_h, grad=mb_gradient)
3✔
2283
        smb = mbmod.get_specific_mb(heights=h, widths=w)
3✔
2284
        return smb - cmb
3✔
2285

2286
    if ela_h is None:
3✔
2287
        ela_h = optimize.brentq(to_minimize, -1e5, 1e5)
3✔
2288

2289
    # For each flowline compute the apparent MB
2290
    rho = gdir.settings['ice_density']
3✔
2291
    fls = gdir.read_pickle('inversion_flowlines')
3✔
2292
    # Reset flux
2293
    for fl in fls:
3✔
2294
        fl.flux = np.zeros(len(fl.surface_h))
3✔
2295
    # Flowlines in order to be sure
2296
    mbmod = LinearMassBalance(ela_h, grad=mb_gradient)
3✔
2297
    for fl in fls:
3✔
2298
        mbz = mbmod.get_annual_mb(fl.surface_h) * cfg.SEC_IN_YEAR * rho
3✔
2299
        fl.set_apparent_mb(mbz, is_calving=is_calving)
3✔
2300

2301
    # Check and write
2302
    _check_terminus_mass_flux(gdir, fls)
3✔
2303
    gdir.write_pickle(fls, 'inversion_flowlines')
3✔
2304
    gdir.write_pickle({'ela_h': ela_h, 'grad': mb_gradient},
3✔
2305
                      'linear_mb_params')
2306

2307

2308
@entity_task(log, writes=['inversion_flowlines'])
9✔
2309
def apparent_mb_from_any_mb(gdir, settings_filesuffix='',
9✔
2310
                            input_filesuffix=None,
2311
                            output_filesuffix=None,
2312
                            mb_model=None,
2313
                            mb_model_class=MonthlyTIModel,
2314
                            mb_years=None):
2315
    """Compute apparent mb from an arbitrary mass balance profile.
2316

2317
    This searches for a mass balance residual to add to the mass balance
2318
    profile so that the average specific MB is zero.
2319

2320
    Parameters
2321
    ----------
2322
    gdir : :py:class:`oggm.GlacierDirectory`
2323
        the glacier directory to process
2324
    settings_filesuffix: str
2325
        You can use a different set of settings by providing a filesuffix. This
2326
        is useful for sensitivity experiments. Code-wise the settings_filesuffix
2327
        is set in the @entity-task decorater.
2328
    input_filesuffix: str
2329
        the filesuffix of the inversion flowlines which should be used (useful
2330
        for conducting multiple experiments in the same gdir)
2331
    output_filesuffix: str
2332
        the filesuffix of the final inversion flowlines which are saved back
2333
        into the gdir (useful for conducting multiple experiments in the same
2334
        gdir)
2335
    mb_model : :py:class:`oggm.core.massbalance.MassBalanceModel`
2336
        the mass balance model to use - if None, will use the
2337
        one given by mb_model_class
2338
    mb_model_class : MassBalanceModel class
2339
        the MassBalanceModel class to use, default is MonthlyTIModel
2340
    mb_years : array, or tuple of length 2 (range)
2341
        the array of years from which you want to average the MB for (for
2342
        mb_model only). If an array of length 2 is given, all years
2343
        between this range (excluding the last one) are used.
2344
        Default is to pick all years from the reference
2345
        geodetic MB period, i.e. PARAMS['geodetic_mb_period'].
2346
        It does not matter much for the final result, but it should be a
2347
        period long enough to have a representative MB gradient.
2348
    """
2349

2350
    if input_filesuffix is None:
7✔
2351
        input_filesuffix = settings_filesuffix
7✔
2352

2353
    if output_filesuffix is None:
7✔
2354
        output_filesuffix = settings_filesuffix
7✔
2355

2356
    # Do we have a calving glacier?
2357
    cmb = calving_mb(gdir)
7✔
2358
    is_calving = cmb != 0
7✔
2359

2360
    # For each flowline compute the apparent MB
2361
    fls = gdir.read_pickle('inversion_flowlines', filesuffix=input_filesuffix)
7✔
2362

2363
    if mb_model is None:
7✔
2364
        mb_model = mb_model_class(gdir, settings_filesuffix=settings_filesuffix)
7✔
2365

2366
    if mb_years is None:
7✔
2367
        mb_years = gdir.settings['geodetic_mb_period']
7✔
2368
        y0, y1 = mb_years.split('_')
7✔
2369
        y0 = int(y0.split('-')[0])
7✔
2370
        y1 = int(y1.split('-')[0])
7✔
2371
        mb_years = np.arange(y0, y1, 1)
7✔
2372

2373
    if len(mb_years) == 2:
7✔
2374
        # Range
2375
        mb_years = np.arange(*mb_years, 1)
7✔
2376

2377
    # Unchanged SMB
2378
    rho = gdir.settings['ice_density']
7✔
2379
    mb_on_fl = []
7✔
2380
    spec_mb = []
7✔
2381
    area_smb = []
7✔
2382
    for fl_id, fl in enumerate(fls):
7✔
2383
        widths = fl.widths
7✔
2384
        try:
7✔
2385
            # For rect and parabola don't compute spec mb
2386
            widths = np.where(fl.thick > 0, widths, 0)
7✔
2387
        except AttributeError:
7✔
2388
            pass
7✔
2389
        mbz = 0
7✔
2390
        smb = 0
7✔
2391
        for yr in mb_years:
7✔
2392
            amb = mb_model.get_annual_mb(fl.surface_h, fls=fls, fl_id=fl_id, year=yr)
7✔
2393
            amb *= cfg.SEC_IN_YEAR * rho
7✔
2394
            mbz += amb
7✔
2395
            smb += weighted_average_1d(amb, widths)
7✔
2396
        mb_on_fl.append(mbz / len(mb_years))
7✔
2397
        spec_mb.append(smb / len(mb_years))
7✔
2398
        area_smb.append(np.sum(widths))
7✔
2399

2400
    if len(mb_on_fl) == 1:
7✔
2401
        o_smb = spec_mb[0]
6✔
2402
    else:
2403
        o_smb = weighted_average_1d(spec_mb, area_smb)
7✔
2404

2405
    def to_minimize(residual_to_opt):
7✔
2406
        return o_smb + residual_to_opt - cmb
7✔
2407

2408
    residual = optimize.brentq(to_minimize, -1e5, 1e5)
7✔
2409

2410
    # Reset flux
2411
    for fl in fls:
7✔
2412
        fl.reset_flux()
7✔
2413

2414
    # Flowlines in order to be sure
2415
    for fl_id, (fl, mbz) in enumerate(zip(fls, mb_on_fl)):
7✔
2416
        fl.set_apparent_mb(mbz + residual, is_calving=is_calving)
7✔
2417
        if fl_id < len(fls) and fl.flux_out < -1e3:
7✔
2418
            log.warning('({}) a tributary has a strongly negative flux. '
5✔
2419
                        'Inversion works but is physically quite '
2420
                        'questionable.'.format(gdir.rgi_id))
2421

2422
    # Check and write
2423
    _check_terminus_mass_flux(gdir, fls)
7✔
2424
    gdir.settings['apparent_mb_from_any_mb_residual'] = residual
7✔
2425

2426
    # this is only for backwards compatibility
2427
    if settings_filesuffix == '':
7✔
2428
        gdir.add_to_diagnostics('apparent_mb_from_any_mb_residual', residual)
7✔
2429

2430
    gdir.write_pickle(fls, 'inversion_flowlines', filesuffix=output_filesuffix)
7✔
2431

2432

2433
@entity_task(log)
9✔
2434
def fixed_geometry_mass_balance(gdir, settings_filesuffix='',
9✔
2435
                                ys=None, ye=None, years=None,
2436
                                monthly_step=False,
2437
                                use_inversion_flowlines=True,
2438
                                climate_filename='climate_historical',
2439
                                climate_input_filesuffix='',
2440
                                temperature_bias=None,
2441
                                precipitation_factor=None,
2442
                                mb_model_class=MonthlyTIModel):
2443
    """Computes the mass balance with climate input from e.g. CRU or a GCM.
2444

2445
    Parameters
2446
    ----------
2447
    gdir : :py:class:`oggm.GlacierDirectory`
2448
        the glacier directory to process
2449
    settings_filesuffix: str
2450
        You can use a different set of settings by providing a filesuffix. This
2451
        is useful for sensitivity experiments. Code-wise the settings_filesuffix
2452
        is set in the @entity-task decorater.
2453
    ys : int
2454
        start year of the model run (default: from the climate file)
2455
        date)
2456
    ye : int
2457
        end year of the model run (default: from the climate file)
2458
    years : array of ints
2459
        override ys and ye with the years of your choice
2460
    monthly_step : bool
2461
        whether to store the diagnostic data at a monthly time step or not
2462
        (default is yearly)
2463
    use_inversion_flowlines : bool
2464
        whether to use the inversion flowlines or the model flowlines
2465
    climate_filename : str
2466
        name of the climate file, e.g. 'climate_historical' (default) or
2467
        'gcm_data'
2468
    climate_input_filesuffix: str
2469
        filesuffix for the input climate file
2470
    temperature_bias : float
2471
        add a bias to the temperature timeseries
2472
    precipitation_factor: float
2473
        multiply a factor to the precipitation time series
2474
        default is None and means that the precipitation factor from the
2475
        calibration is applied which is cfg.PARAMS['prcp_fac']
2476
    mb_model_class : MassBalanceModel class
2477
        the MassBalanceModel class to use, default is MonthlyTIModel
2478
    """
2479

2480
    if monthly_step:
3✔
2481
        raise NotImplementedError('monthly_step not implemented yet')
2482

2483
    mbmod = MultipleFlowlineMassBalance(gdir, mb_model_class=mb_model_class,
3✔
2484
                                        filename=climate_filename,
2485
                                        use_inversion_flowlines=use_inversion_flowlines,
2486
                                        input_filesuffix=climate_input_filesuffix,
2487
                                        settings_filesuffix=settings_filesuffix)
2488

2489
    if temperature_bias is not None:
3✔
2490
        mbmod.temp_bias = temperature_bias
×
2491
    if precipitation_factor is not None:
3✔
2492
        mbmod.prcp_fac = precipitation_factor
×
2493

2494
    if years is None:
3✔
2495
        if ys is None:
3✔
2496
            ys = mbmod.flowline_mb_models[0].ys
3✔
2497
        if ye is None:
3✔
2498
            ye = mbmod.flowline_mb_models[0].ye
3✔
2499
        years = np.arange(ys, ye + 1)
3✔
2500

2501
    odf = pd.Series(data=mbmod.get_specific_mb(year=years),
3✔
2502
                    index=years)
2503
    return odf
3✔
2504

2505

2506
@entity_task(log)
9✔
2507
def compute_ela(gdir, settings_filesuffix='',
9✔
2508
                ys=None, ye=None, years=None, climate_filename='climate_historical',
2509
                temperature_bias=None, precipitation_factor=None, climate_input_filesuffix='',
2510
                mb_model_class=MonthlyTIModel):
2511

2512
    """Computes the ELA of a glacier for a for given years and climate.
2513

2514
    Parameters
2515
    ----------
2516
    gdir : :py:class:`oggm.GlacierDirectory`
2517
        the glacier directory to process
2518
    settings_filesuffix: str
2519
        You can use a different set of settings by providing a filesuffix. This
2520
        is useful for sensitivity experiments. Code-wise the settings_filesuffix
2521
        is set in the @entity-task decorater.
2522
    ys : int
2523
        start year
2524
    ye : int
2525
        end year
2526
    years : array of ints
2527
        override ys and ye with the years of your choice
2528
    climate_filename : str
2529
        name of the climate file, e.g. 'climate_historical' (default) or
2530
        'gcm_data'
2531
    climate_input_filesuffix : str
2532
        filesuffix for the input climate file
2533
    temperature_bias : float
2534
        add a bias to the temperature timeseries
2535
    precipitation_factor: float
2536
        multiply a factor to the precipitation time series
2537
        default is None and means that the precipitation factor from the
2538
        calibration is applied which is cfg.PARAMS['prcp_fac']
2539
    mb_model_class : MassBalanceModel class
2540
        the MassBalanceModel class to use, default is MonthlyTIModel
2541
    """
2542

2543
    mbmod = mb_model_class(gdir, settings_filesuffix=settings_filesuffix,
1✔
2544
                           filename=climate_filename,
2545
                           input_filesuffix=climate_input_filesuffix)
2546

2547
    if temperature_bias is not None:
1✔
2548
        mbmod.temp_bias = temperature_bias
1✔
2549
    if precipitation_factor is not None:
1✔
2550
        mbmod.prcp_fac = precipitation_factor
×
2551

2552
    mbmod.valid_bounds = [-10000, 20000]
1✔
2553

2554
    if years is None:
1✔
2555
        years = np.arange(ys, ye+1)
1✔
2556

2557
    ela = []
1✔
2558
    for yr in years:
1✔
2559
        ela.append(mbmod.get_ela(year=yr))
1✔
2560

2561
    odf = pd.Series(data=ela, index=years)
1✔
2562
    return odf
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc