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

OGGM / oggm / 3836586642

pending completion
3836586642

Pull #1510

github

GitHub
Merge 2a19b5c72 into 8ab12c504
Pull Request #1510: Fix issue with TANDEM DEM and minor RGI7 issue

2 of 4 new or added lines in 2 files covered. (50.0%)

249 existing lines in 7 files now uncovered.

12099 of 13681 relevant lines covered (88.44%)

3.61 hits per line

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

86.4
/oggm/core/dynamic_spinup.py
1
"""Dynamic spinup functions for model initialisation"""
2
# Builtins
3
import logging
9✔
4
import copy
9✔
5
import os
9✔
6

7
# External libs
8
import numpy as np
9✔
9
from scipy import interpolate
9✔
10

11
# Locals
12
import oggm.cfg as cfg
9✔
13
from oggm import utils
9✔
14
from oggm import entity_task
9✔
15
from oggm.exceptions import InvalidParamsError, InvalidWorkflowError
9✔
16
from oggm.core.massbalance import (MultipleFlowlineMassBalance,
9✔
17
                                   ConstantMassBalance,
18
                                   PastMassBalance)
19
from oggm.core.climate import apparent_mb_from_any_mb
9✔
20
from oggm.core.flowline import (FluxBasedModel, FileModel,
9✔
21
                                init_present_time_glacier,
22
                                run_from_climate_data)
23

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

27

28
@entity_task(log)
9✔
29
def run_dynamic_spinup(gdir, init_model_filesuffix=None, init_model_yr=None,
9✔
30
                       init_model_fls=None,
31
                       climate_input_filesuffix='',
32
                       evolution_model=FluxBasedModel,
33
                       mb_model_historical=None, mb_model_spinup=None,
34
                       spinup_period=20, spinup_start_yr=None,
35
                       min_spinup_period=10, spinup_start_yr_max=None,
36
                       yr_rgi=None, minimise_for='area', precision_percent=1,
37
                       precision_absolute=1, min_ice_thickness=None,
38
                       first_guess_t_bias=-2, t_bias_max_step_length=2,
39
                       maxiter=30, output_filesuffix='_dynamic_spinup',
40
                       store_model_geometry=True, store_fl_diagnostics=None,
41
                       store_model_evolution=True, ignore_errors=False,
42
                       return_t_bias_best=False, ye=None,
43
                       model_flowline_filesuffix='', make_compatible=False,
44
                       add_fixed_geometry_spinup=False, **kwargs):
45
    """Dynamically spinup the glacier to match area or volume at the RGI date.
46

47
    This task allows to do simulations in the recent past (before the glacier
48
    inventory date), when the state of the glacier was unknown. This is a
49
    very difficult task the longer further back in time one goes
50
    (see publications by Eis et al. for a theoretical background), but can
51
    work quite well for short periods. Note that the solution is not unique.
52

53
    Parameters
54
    ----------
55
    gdir : :py:class:`oggm.GlacierDirectory`
56
        the glacier directory to process
57
    init_model_filesuffix : str or None
58
        if you want to start from a previous model run state. This state
59
        should be at time yr_rgi_date.
60
    init_model_yr : int or None
61
        the year of the initial run you want to start from. The default
62
        is to take the last year of the simulation.
63
    init_model_fls : []
64
        list of flowlines to use to initialise the model (the default is the
65
        present_time_glacier file from the glacier directory).
66
        Ignored if `init_model_filesuffix` is set
67
    climate_input_filesuffix : str
68
        filesuffix for the input climate file
69
    evolution_model : :class:oggm.core.FlowlineModel
70
        which evolution model to use. Default: FluxBasedModel
71
    mb_model_historical : :py:class:`core.MassBalanceModel`
72
        User-povided MassBalanceModel instance for the historical run. Default
73
        is to use a PastMassBalance model  together with the provided
74
        parameter climate_input_filesuffix.
75
    mb_model_spinup : :py:class:`core.MassBalanceModel`
76
        User-povided MassBalanceModel instance for the spinup before the
77
        historical run. Default is to use a ConstantMassBalance model together
78
        with the provided parameter climate_input_filesuffix and during the
79
        period of spinup_start_yr until rgi_year (e.g. 1979 - 2000).
80
    spinup_period : int
81
        The period how long the spinup should run. Start date of historical run
82
        is defined "yr_rgi - spinup_period". Minimum allowed value is 10. If
83
        the provided climate data starts at year later than
84
        (yr_rgi - spinup_period) the spinup_period is set to
85
        (yr_rgi - yr_climate_start). Caution if spinup_start_yr is set the
86
        spinup_period is ignored.
87
        Default is 20
88
    spinup_start_yr : int or None
89
        The start year of the dynamic spinup. If the provided year is before
90
        the provided climate data starts the year of the climate data is used.
91
        If set it overrides the spinup_period.
92
        Default is None
93
    min_spinup_period : int
94
        If the dynamic spinup function fails with the initial 'spinup_period'
95
        a shorter period is tried. Here you can define the minimum period to
96
        try.
97
        Default is 10
98
    spinup_start_yr_max : int or None
99
        Possibility to provide a maximum year where the dynamic spinup must
100
        start from at least. If set, this overrides the min_spinup_period if
101
        yr_rgi - spinup_start_yr_max > min_spinup_period.
102
        Default is None
103
    yr_rgi : int
104
        The rgi date, at which we want to match area or volume.
105
        If None, gdir.rgi_date + 1 is used (the default).
106
    minimise_for : str
107
        The variable we want to match at yr_rgi. Options are 'area' or 'volume'.
108
        Default is 'area'
109
    precision_percent : float
110
        Gives the precision we want to match in percent. The algorithm makes
111
        sure that the resulting relative mismatch is smaller than
112
        precision_percent, but also that the absolute value is smaller than
113
        precision_absolute.
114
        Default is 1., meaning the difference must be within 1% of the given
115
        value (area or volume).
116
    precision_absolute : float
117
        Gives an minimum absolute value to match. The algorithm makes sure that
118
        the resulting relative mismatch is smaller than precision_percent, but
119
        also that the absolute value is smaller than precision_absolute.
120
        The unit of precision_absolute depends on minimise_for (if 'area' in
121
        km2, if 'volume' in km3)
122
        Default is 1.
123
    min_ice_thickness : float
124
        Gives an minimum ice thickness for model grid points which are counted
125
        to the total model value. This could be useful to filter out seasonal
126
        'glacier growth', as OGGM do not differentiate between snow and ice in
127
        the forward model run. Therefore you could see quite fast changes
128
        (spikes) in the time-evolution (especially visible in length and area).
129
        If you set this value to 0 the filtering can be switched off.
130
        Default is 10.
131
    first_guess_t_bias : float
132
        The initial guess for the temperature bias for the spinup
133
        MassBalanceModel in °C.
134
        Default is -2.
135
    t_bias_max_step_length : float
136
        Defines the maximums allowed change of t_bias between two iterations. Is
137
        needed to avoid to large changes.
138
        Default is 2.
139
    maxiter : int
140
        Maximum number of minimisation iterations per spinup period. If reached
141
        and 'ignore_errors=False' an error is raised.
142
        Default is 30
143
    output_filesuffix : str
144
        for the output file
145
    store_model_geometry : bool
146
        whether to store the full model geometry run file to disk or not.
147
        Default is True
148
    store_fl_diagnostics : bool or None
149
        whether to store the model flowline diagnostics to disk or not.
150
        Default is None
151
    store_model_evolution : bool
152
        if True the complete dynamic spinup run is saved (complete evolution
153
        of the model during the dynamic spinup), if False only the final model
154
        state after the dynamic spinup run is saved. (Hint: if
155
        store_model_evolution = True and ignore_errors = True and an Error
156
        during the dynamic spinup occurs the stored model evolution is only one
157
        year long)
158
        Default is True
159
    ignore_errors : bool
160
        If True the function saves the model without a dynamic spinup using
161
        the 'output_filesuffix', if an error during the dynamic spinup occurs.
162
        This is useful if you want to keep glaciers for the following tasks.
163
        Default is True
164
    return_t_bias_best : bool
165
        If True the used temperature bias for the spinup is returned in
166
        addition to the final model. If an error occurs and ignore_error=True,
167
        the returned value is np.nan.
168
        Default is False
169
    ye : int
170
        end year of the model run, must be larger than yr_rgi. If nothing is
171
        given it is set to yr_rgi. It is not recommended to use it if only data
172
        until yr_rgi is needed for calibration as this increases the run time
173
        of each iteration during the iterative minimisation. Instead use
174
        run_from_climate_data afterwards and merge both outputs using
175
        merge_consecutive_run_outputs.
176
        Default is None
177
    model_flowline_filesuffix : str
178
        suffix to the model_flowlines filename to use (if no other flowlines
179
        are provided with init_model_filesuffix or init_model_fls).
180
        Default is ''
181
    make_compatible : bool
182
        if set to true this will add all variables to the resulting dataset
183
        so it could be combined with any other one. This is necessary if
184
        different spinup methods are used. For example if using the dynamic
185
        spinup and setting fixed geometry spinup as fallback, the variable
186
        'is_fixed_geometry_spinup' must be added to the dynamic spinup so
187
        it is possible to compile both glaciers together.
188
        Default is False
189
    add_fixed_geometry_spinup : bool
190
        If True and the original spinup_period must be shortened (due to
191
        ice-free or out-of-boundary error) a fixed geometry spinup is added at
192
        the beginning so that the resulting model run always starts from the
193
        defined start year (could be defined through spinup_period or
194
        spinup_start_yr). Only has an effect if store_model_evolution is True.
195
        Default is False
196
    kwargs : dict
197
        kwargs to pass to the evolution_model instance
198

199
    Returns
200
    -------
201
    :py:class:`oggm.core.flowline.evolution_model`
202
        The final dynamically spined-up model. Type depends on the selected
203
        evolution_model, by default a FluxBasedModel.
204
    """
205

206
    if yr_rgi is None:
2✔
207
        yr_rgi = gdir.rgi_date + 1  # + 1 converted to hydro years
1✔
208

209
    if ye is None:
2✔
210
        ye = yr_rgi
1✔
211

212
    if ye < yr_rgi:
2✔
213
        raise RuntimeError(f'The provided end year (ye = {ye}) must be larger'
1✔
214
                           f'than the rgi date (yr_rgi = {yr_rgi}!')
215

216
    yr_min = gdir.get_climate_info()['baseline_hydro_yr_0']
2✔
217

218
    if min_ice_thickness is None:
2✔
219
        min_ice_thickness = cfg.PARAMS['dynamic_spinup_min_ice_thick']
2✔
220

221
    # check provided maximum start year here, and change min_spinup_period
222
    if spinup_start_yr_max is not None:
2✔
223
        if spinup_start_yr_max < yr_min:
2✔
224
            raise RuntimeError(f'The provided maximum start year (= '
1✔
225
                               f'{spinup_start_yr_max}) must be larger than '
226
                               f'the start year of the provided climate data '
227
                               f'(= {yr_min})!')
228
        if spinup_start_yr is not None:
2✔
229
            if spinup_start_yr_max < spinup_start_yr:
2✔
230
                raise RuntimeError(f'The provided start year (= '
1✔
231
                                   f'{spinup_start_yr}) must be smaller than '
232
                                   f'the maximum start year '
233
                                   f'{spinup_start_yr_max}!')
234
        if (yr_rgi - spinup_start_yr_max) > min_spinup_period:
2✔
235
            min_spinup_period = (yr_rgi - spinup_start_yr_max)
1✔
236

237
    if init_model_filesuffix is not None:
2✔
238
        fp = gdir.get_filepath('model_geometry',
1✔
239
                               filesuffix=init_model_filesuffix)
240
        fmod = FileModel(fp)
1✔
241
        if init_model_yr is None:
1✔
UNCOV
242
            init_model_yr = fmod.last_yr
×
243
        fmod.run_until(init_model_yr)
1✔
244
        init_model_fls = fmod.fls
1✔
245

246
    if init_model_fls is None:
2✔
247
        fls_spinup = gdir.read_pickle('model_flowlines',
1✔
248
                                      filesuffix=model_flowline_filesuffix)
249
    else:
250
        fls_spinup = copy.deepcopy(init_model_fls)
2✔
251

252
    # MassBalance for actual run from yr_spinup to yr_rgi
253
    if mb_model_historical is None:
2✔
254
        mb_model_historical = MultipleFlowlineMassBalance(
2✔
255
            gdir, mb_model_class=PastMassBalance,
256
            filename='climate_historical',
257
            input_filesuffix=climate_input_filesuffix)
258

259
    # here we define the file-paths for the output
260
    if store_model_geometry:
2✔
261
        geom_path = gdir.get_filepath('model_geometry',
2✔
262
                                      filesuffix=output_filesuffix,
263
                                      delete=True)
264
    else:
265
        geom_path = False
1✔
266

267
    if store_fl_diagnostics is None:
2✔
268
        store_fl_diagnostics = cfg.PARAMS['store_fl_diagnostics']
2✔
269

270
    if store_fl_diagnostics:
2✔
UNCOV
271
        fl_diag_path = gdir.get_filepath('fl_diagnostics',
×
272
                                         filesuffix=output_filesuffix,
273
                                         delete=True)
274
    else:
275
        fl_diag_path = False
2✔
276

277
    diag_path = gdir.get_filepath('model_diagnostics',
2✔
278
                                  filesuffix=output_filesuffix,
279
                                  delete=True)
280

281
    if cfg.PARAMS['use_inversion_params_for_run']:
2✔
282
        diag = gdir.get_diagnostics()
2✔
283
        fs = diag.get('inversion_fs', cfg.PARAMS['fs'])
2✔
284
        glen_a = diag.get('inversion_glen_a', cfg.PARAMS['glen_a'])
2✔
285
    else:
286
        fs = cfg.PARAMS['fs']
1✔
287
        glen_a = cfg.PARAMS['glen_a']
1✔
288

289
    kwargs.setdefault('fs', fs)
2✔
290
    kwargs.setdefault('glen_a', glen_a)
2✔
291

292
    mb_elev_feedback = kwargs.get('mb_elev_feedback', 'annual')
2✔
293
    if mb_elev_feedback != 'annual':
2✔
294
        raise InvalidParamsError('Only use annual mb_elev_feedback with the '
1✔
295
                                 'dynamic spinup function!')
296

297
    if cfg.PARAMS['use_kcalving_for_run']:
2✔
298
        raise InvalidParamsError('Dynamic spinup not tested with '
1✔
299
                                 "cfg.PARAMS['use_kcalving_for_run'] is `True`!")
300

301
    # this function saves a model without conducting a dynamic spinup, but with
302
    # the provided output_filesuffix, so following tasks can find it.
303
    # This is necessary if yr_rgi < yr_min + 10 or if the dynamic spinup failed.
304
    def save_model_without_dynamic_spinup():
2✔
305
        gdir.add_to_diagnostics('run_dynamic_spinup_success', False)
1✔
306
        yr_use = np.clip(yr_rgi, yr_min, None)
1✔
307
        model_dynamic_spinup_end = evolution_model(fls_spinup,
1✔
308
                                                   mb_model_historical,
309
                                                   y0=yr_use,
310
                                                   **kwargs)
311

312
        with np.warnings.catch_warnings():
1✔
313
            if ye < yr_use:
1✔
UNCOV
314
                yr_run = yr_use
×
315
            else:
316
                yr_run = ye
1✔
317
            # For operational runs we ignore the warnings
318
            np.warnings.filterwarnings('ignore', category=RuntimeWarning)
1✔
319
            model_dynamic_spinup_end.run_until_and_store(
1✔
320
                yr_run,
321
                geom_path=geom_path,
322
                diag_path=diag_path,
323
                fl_diag_path=fl_diag_path,
324
                make_compatible=make_compatible)
325

326
        return model_dynamic_spinup_end
1✔
327

328
    if yr_rgi < yr_min + min_spinup_period:
2✔
329
        log.warning('The provided rgi_date is smaller than yr_climate_start + '
1✔
330
                    'min_spinup_period, therefore no dynamic spinup is '
331
                    'conducted and the original flowlines are saved at the '
332
                    'provided rgi_date or the start year of the provided '
333
                    'climate data (if yr_climate_start > yr_rgi)')
334
        if ignore_errors:
1✔
335
            model_dynamic_spinup_end = save_model_without_dynamic_spinup()
1✔
336
            if return_t_bias_best:
1✔
337
                return model_dynamic_spinup_end, np.nan
1✔
338
            else:
339
                return model_dynamic_spinup_end
1✔
340
        else:
341
            raise RuntimeError('The difference between the rgi_date and the '
1✔
342
                               'start year of the climate data is to small to '
343
                               'run a dynamic spinup!')
344

345
    # here we define the flowline we want to match, it is assumed that during
346
    # the inversion the volume was calibrated towards the consensus estimate
347
    # (as it is by default), but this means the volume is matched on a
348
    # regional scale, maybe we could use here the individual glacier volume
349
    fls_ref = copy.deepcopy(fls_spinup)
2✔
350
    if minimise_for == 'area':
2✔
351
        unit = 'km2'
2✔
352
        other_variable = 'volume'
2✔
353
        other_unit = 'km3'
2✔
354
    elif minimise_for == 'volume':
1✔
355
        unit = 'km3'
1✔
356
        other_variable = 'area'
1✔
357
        other_unit = 'km2'
1✔
358
    else:
359
        raise NotImplementedError
360
    cost_var = f'{minimise_for}_{unit}'
2✔
361
    reference_value = np.sum([getattr(f, cost_var) for f in fls_ref])
2✔
362
    other_reference_value = np.sum([getattr(f, f'{other_variable}_{other_unit}')
2✔
363
                                    for f in fls_ref])
364

365
    # if reference value is zero no dynamic spinup is possible
366
    if reference_value == 0.:
2✔
367
        if ignore_errors:
1✔
368
            model_dynamic_spinup_end = save_model_without_dynamic_spinup()
1✔
369
            if return_t_bias_best:
1✔
370
                return model_dynamic_spinup_end, np.nan
1✔
371
            else:
372
                return model_dynamic_spinup_end
1✔
373
        else:
374
            raise RuntimeError('The given reference value is Zero, no dynamic '
1✔
375
                               'spinup possible!')
376

377
    # here we adjust the used precision_percent to make sure the resulting
378
    # absolute mismatch is smaller than precision_absolute
379
    precision_percent = min(precision_percent,
2✔
380
                            precision_absolute / reference_value * 100)
381

382
    # only used to check performance of function
383
    forward_model_runs = [0]
2✔
384

385
    # the actual spinup run
386
    def run_model_with_spinup_to_rgi_date(t_bias):
2✔
387
        forward_model_runs.append(forward_model_runs[-1] + 1)
2✔
388

389
        # with t_bias the glacier state after spinup is changed between iterations
390
        mb_model_spinup.temp_bias = t_bias
2✔
391
        # run the spinup
392
        model_spinup = evolution_model(copy.deepcopy(fls_spinup),
2✔
393
                                       mb_model_spinup,
394
                                       y0=0,
395
                                       **kwargs)
396
        model_spinup.run_until(2 * halfsize_spinup)
2✔
397

398
        # if glacier is completely gone return information in ice-free
399
        ice_free = False
2✔
400
        if np.isclose(model_spinup.volume_km3, 0.):
2✔
401
            ice_free = True
1✔
402

403
        # Now conduct the actual model run to the rgi date
404
        model_historical = evolution_model(model_spinup.fls,
2✔
405
                                           mb_model_historical,
406
                                           y0=yr_spinup,
407
                                           **kwargs)
408
        if store_model_evolution:
2✔
409
            ds = model_historical.run_until_and_store(
2✔
410
                ye,
411
                geom_path=geom_path,
412
                diag_path=diag_path,
413
                fl_diag_path=fl_diag_path,
414
                dynamic_spinup_min_ice_thick=min_ice_thickness,
415
                fixed_geometry_spinup_yr=fixed_geometry_spinup_yr,
416
                make_compatible=make_compatible)
417
            if type(ds) == tuple:
2✔
418
                ds = ds[0]
2✔
419
            model_area_km2 = ds.area_m2_min_h.loc[yr_rgi].values * 1e-6
2✔
420
            model_volume_km3 = ds.volume_m3_min_h.loc[yr_rgi].values * 1e-9
2✔
421
        else:
422
            # only run to rgi date and extract values
423
            model_historical.run_until(yr_rgi)
1✔
424
            fls = model_historical.fls
1✔
425
            model_area_km2 = np.sum(
1✔
426
                [np.sum(fl.bin_area_m2[fl.thick > min_ice_thickness])
427
                 for fl in fls]) * 1e-6
428
            model_volume_km3 = np.sum(
1✔
429
                [np.sum((fl.section * fl.dx_meter)[fl.thick > min_ice_thickness])
430
                 for fl in fls]) * 1e-9
431
            # afterwards finish the complete run
432
            model_historical.run_until(ye)
1✔
433

434
        if cost_var == 'area_km2':
2✔
435
            return model_area_km2, model_volume_km3, model_historical, ice_free
2✔
436
        elif cost_var == 'volume_km3':
1✔
437
            return model_volume_km3, model_area_km2, model_historical, ice_free
1✔
438
        else:
439
            raise NotImplementedError(f'{cost_var}')
440

441
    def cost_fct(t_bias, model_dynamic_spinup_end_loc, other_variable_mismatch_loc):
2✔
442
        # actual model run
443
        model_value, other_value, model_dynamic_spinup, ice_free = \
2✔
444
            run_model_with_spinup_to_rgi_date(t_bias)
445

446
        # save the final model for later
447
        model_dynamic_spinup_end_loc.append(copy.deepcopy(model_dynamic_spinup))
2✔
448

449
        # calculate the mismatch in percent
450
        cost = (model_value - reference_value) / reference_value * 100
2✔
451
        other_variable_mismatch_loc.append(
2✔
452
            (other_value - other_reference_value) / other_reference_value * 100)
453

454
        return cost, ice_free
2✔
455

456
    def init_cost_fct():
2✔
457
        model_dynamic_spinup_end_loc = []
2✔
458
        other_variable_mismatch_loc = []
2✔
459

460
        def c_fct(t_bias):
2✔
461
            return cost_fct(t_bias, model_dynamic_spinup_end_loc,
2✔
462
                            other_variable_mismatch_loc)
463

464
        return c_fct, model_dynamic_spinup_end_loc, other_variable_mismatch_loc
2✔
465

466
    def minimise_with_spline_fit(fct_to_minimise):
2✔
467
        # defines limits of t_bias in accordance to maximal allowed change
468
        # between iterations
469
        t_bias_limits = [first_guess_t_bias - t_bias_max_step_length,
2✔
470
                         first_guess_t_bias + t_bias_max_step_length]
471
        t_bias_guess = []
2✔
472
        mismatch = []
2✔
473
        # this two variables indicate that the limits were already adapted to
474
        # avoid an ice_free or out_of_domain error
475
        was_ice_free = False
2✔
476
        was_out_of_domain = False
2✔
477
        was_errors = [was_out_of_domain, was_ice_free]
2✔
478

479
        def get_mismatch(t_bias):
2✔
480
            t_bias = copy.deepcopy(t_bias)
2✔
481
            # first check if the new t_bias is in limits
482
            if t_bias < t_bias_limits[0]:
2✔
483
                # was the smaller limit already executed, if not first do this
484
                if t_bias_limits[0] not in t_bias_guess:
1✔
485
                    t_bias = copy.deepcopy(t_bias_limits[0])
1✔
486
                else:
487
                    # smaller limit was already used, check if it was
488
                    # already newly defined with glacier exceeding domain
489
                    if was_errors[0]:
1✔
490
                        raise RuntimeError('Not able to minimise without '
1✔
491
                                           'exceeding the domain! Best '
492
                                           f'mismatch '
493
                                           f'{np.min(np.abs(mismatch))}%')
494
                    else:
495
                        # ok we set a new lower limit
496
                        t_bias_limits[0] = t_bias_limits[0] - \
1✔
497
                            t_bias_max_step_length
498
            elif t_bias > t_bias_limits[1]:
2✔
499
                # was the larger limit already executed, if not first do this
500
                if t_bias_limits[1] not in t_bias_guess:
1✔
501
                    t_bias = copy.deepcopy(t_bias_limits[1])
1✔
502
                else:
503
                    # larger limit was already used, check if it was
504
                    # already newly defined with ice free glacier
505
                    if was_errors[1]:
1✔
506
                        raise RuntimeError('Not able to minimise without ice '
1✔
507
                                           'free glacier after spinup! Best '
508
                                           'mismatch '
509
                                           f'{np.min(np.abs(mismatch))}%')
510
                    else:
511
                        # ok we set a new upper limit
512
                        t_bias_limits[1] = t_bias_limits[1] + \
1✔
513
                            t_bias_max_step_length
514

515
            # now clip t_bias with limits
516
            t_bias = np.clip(t_bias, t_bias_limits[0], t_bias_limits[1])
2✔
517

518
            # now start with mismatch calculation
519

520
            # if error during spinup (ice_free or out_of_domain) this defines
521
            # how much t_bias is changed to look for an error free glacier spinup
522
            t_bias_search_change = t_bias_max_step_length / 10
2✔
523
            # maximum number of changes to look for an error free glacier
524
            max_iterations = int(t_bias_max_step_length / t_bias_search_change)
2✔
525
            is_out_of_domain = True
2✔
526
            is_ice_free_spinup = True
2✔
527
            is_ice_free_end = True
2✔
528
            is_first_guess_ice_free = False
2✔
529
            is_first_guess_out_of_domain = False
2✔
530
            doing_first_guess = (len(mismatch) == 0)
2✔
531
            define_new_lower_limit = False
2✔
532
            define_new_upper_limit = False
2✔
533
            iteration = 0
2✔
534

535
            while ((is_out_of_domain | is_ice_free_spinup | is_ice_free_end) &
2✔
536
                   (iteration < max_iterations)):
537
                try:
2✔
538
                    tmp_mismatch, is_ice_free_spinup = fct_to_minimise(t_bias)
2✔
539

540
                    # no error occurred, so we are not outside the domain
541
                    is_out_of_domain = False
2✔
542

543
                    # check if we are ice_free after spinup, if so we search
544
                    # for a new upper limit for t_bias
545
                    if is_ice_free_spinup:
2✔
546
                        was_errors[1] = True
1✔
547
                        define_new_upper_limit = True
1✔
548
                        # special treatment if it is the first guess
549
                        if np.isclose(t_bias, first_guess_t_bias) & \
1✔
550
                                doing_first_guess:
UNCOV
551
                            is_first_guess_ice_free = True
×
552
                            # here directly jump to the smaller limit
UNCOV
553
                            t_bias = copy.deepcopy(t_bias_limits[0])
×
554
                        elif is_first_guess_ice_free & doing_first_guess:
1✔
555
                            # make large steps if it is first guess
UNCOV
556
                            t_bias = t_bias - t_bias_max_step_length
×
557
                        else:
558
                            t_bias = np.round(t_bias - t_bias_search_change,
1✔
559
                                              decimals=1)
560
                        if np.isclose(t_bias, t_bias_guess).any():
1✔
561
                            iteration = copy.deepcopy(max_iterations)
1✔
562

563
                    # check if we are ice_free at the end of the model run, if
564
                    # so we use the lower t_bias limit and change the limit if
565
                    # needed
566
                    elif np.isclose(tmp_mismatch, -100.):
2✔
567
                        is_ice_free_end = True
1✔
568
                        was_errors[1] = True
1✔
569
                        define_new_upper_limit = True
1✔
570
                        # special treatment if it is the first guess
571
                        if np.isclose(t_bias, first_guess_t_bias) & \
1✔
572
                                doing_first_guess:
573
                            is_first_guess_ice_free = True
1✔
574
                            # here directly jump to the smaller limit
575
                            t_bias = copy.deepcopy(t_bias_limits[0])
1✔
576
                        elif is_first_guess_ice_free & doing_first_guess:
1✔
577
                            # make large steps if it is first guess
578
                            t_bias = t_bias - t_bias_max_step_length
1✔
579
                        else:
580
                            # if lower limit was already used change it and use
UNCOV
581
                            if t_bias == t_bias_limits[0]:
×
UNCOV
582
                                t_bias_limits[0] = t_bias_limits[0] - \
×
583
                                    t_bias_max_step_length
UNCOV
584
                                t_bias = copy.deepcopy(t_bias_limits[0])
×
585
                            else:
586
                                # otherwise just try with a colder t_bias
UNCOV
587
                                t_bias = np.round(t_bias - t_bias_search_change,
×
588
                                                  decimals=1)
589

590
                    else:
591
                        is_ice_free_end = False
2✔
592

593
                except RuntimeError as e:
1✔
594
                    # check if glacier grow to large
595
                    if 'Glacier exceeds domain boundaries, at year:' in f'{e}':
1✔
596
                        # ok we where outside the domain, therefore we search
597
                        # for a new lower limit for t_bias in 0.1 °C steps
598
                        is_out_of_domain = True
1✔
599
                        define_new_lower_limit = True
1✔
600
                        was_errors[0] = True
1✔
601
                        # special treatment if it is the first guess
602
                        if np.isclose(t_bias, first_guess_t_bias) & \
1✔
603
                                doing_first_guess:
604
                            is_first_guess_out_of_domain = True
1✔
605
                            # here directly jump to the larger limit
606
                            t_bias = t_bias_limits[1]
1✔
607
                        elif is_first_guess_out_of_domain & doing_first_guess:
1✔
608
                            # make large steps if it is first guess
609
                            t_bias = t_bias + t_bias_max_step_length
1✔
610
                        else:
611
                            t_bias = np.round(t_bias + t_bias_search_change,
1✔
612
                                              decimals=1)
613
                        if np.isclose(t_bias, t_bias_guess).any():
1✔
UNCOV
614
                            iteration = copy.deepcopy(max_iterations)
×
615

616
                    else:
617
                        # otherwise this error can not be handled here
UNCOV
618
                        raise RuntimeError(e)
×
619

620
                iteration += 1
2✔
621

622
            if iteration >= max_iterations:
2✔
623
                # ok we were not able to find an mismatch without error
624
                # (ice_free or out of domain), so we try to raise an descriptive
625
                # RuntimeError
626
                if len(mismatch) == 0:
1✔
627
                    # unfortunately we were not able conduct one single error
628
                    # free run
629
                    msg = 'Not able to conduct one error free run. Error is '
1✔
630
                    if is_first_guess_ice_free:
1✔
631
                        msg += f'"ice_free" with last t_bias of {t_bias}.'
1✔
632
                    elif is_first_guess_out_of_domain:
1✔
633
                        msg += f'"out_of_domain" with last t_bias of {t_bias}.'
1✔
634
                    else:
UNCOV
635
                        raise RuntimeError('Something unexpected happened!')
×
636

637
                    raise RuntimeError(msg)
1✔
638

639
                elif define_new_lower_limit:
1✔
640
                    raise RuntimeError('Not able to minimise without '
×
641
                                       'exceeding the domain! Best '
642
                                       f'mismatch '
643
                                       f'{np.min(np.abs(mismatch))}%')
644
                elif define_new_upper_limit:
1✔
645
                    raise RuntimeError('Not able to minimise without ice '
1✔
646
                                       'free glacier after spinup! Best mismatch '
647
                                       f'{np.min(np.abs(mismatch))}%')
UNCOV
648
                elif is_ice_free_end:
×
UNCOV
649
                    raise RuntimeError('Not able to find a t_bias so that '
×
650
                                       'glacier is not ice free at the end! '
651
                                       '(Last t_bias '
652
                                       f'{t_bias + t_bias_max_step_length} °C)')
653
                else:
654
                    raise RuntimeError('Something unexpected happened during '
×
655
                                       'definition of new t_bias limits!')
656
            else:
657
                # if we found a new limit set it
658
                if define_new_upper_limit & define_new_lower_limit:
2✔
659
                    # we can end here if we are at the first guess and took
660
                    # a to large step
UNCOV
661
                    was_errors[0] = False
×
UNCOV
662
                    was_errors[1] = False
×
663
                    if t_bias <= t_bias_limits[0]:
×
664
                        t_bias_limits[0] = t_bias
×
665
                        t_bias_limits[1] = t_bias_limits[0] + \
×
666
                            t_bias_max_step_length
UNCOV
667
                    elif t_bias >= t_bias_limits[1]:
×
668
                        t_bias_limits[1] = t_bias
×
UNCOV
669
                        t_bias_limits[0] = t_bias_limits[1] - \
×
670
                            t_bias_max_step_length
671
                    else:
UNCOV
672
                        if is_first_guess_ice_free:
×
673
                            t_bias_limits[1] = t_bias
×
674
                        elif is_out_of_domain:
×
UNCOV
675
                            t_bias_limits[0] = t_bias
×
676
                        else:
UNCOV
677
                            raise RuntimeError('I have not expected to get here!')
×
678
                elif define_new_lower_limit:
2✔
679
                    t_bias_limits[0] = copy.deepcopy(t_bias)
1✔
680
                    if t_bias >= t_bias_limits[1]:
1✔
681
                        # this happens when the first guess was out of domain
UNCOV
682
                        was_errors[0] = False
×
UNCOV
683
                        t_bias_limits[1] = t_bias_limits[0] + \
×
684
                            t_bias_max_step_length
685
                elif define_new_upper_limit:
2✔
686
                    t_bias_limits[1] = copy.deepcopy(t_bias)
1✔
687
                    if t_bias <= t_bias_limits[0]:
1✔
688
                        # this happens when the first guess was ice free
689
                        was_errors[1] = False
1✔
690
                        t_bias_limits[0] = t_bias_limits[1] - \
1✔
691
                            t_bias_max_step_length
692

693
            return float(tmp_mismatch), float(t_bias)
2✔
694

695
        # first guess
696
        new_mismatch, new_t_bias = get_mismatch(first_guess_t_bias)
2✔
697
        t_bias_guess.append(new_t_bias)
2✔
698
        mismatch.append(new_mismatch)
2✔
699

700
        if abs(mismatch[-1]) < precision_percent:
2✔
701
            return t_bias_guess, mismatch
1✔
702

703
        # second (arbitrary) guess is given depending on the outcome of first
704
        # guess, when mismatch is 100% t_bias is changed for
705
        # t_bias_max_step_length, but at least the second guess is 0.2 °C away
706
        # from the first guess
707
        step = np.sign(mismatch[-1]) * max(np.abs(mismatch[-1]) *
2✔
708
                                           t_bias_max_step_length / 100,
709
                                           0.2)
710
        new_mismatch, new_t_bias = get_mismatch(t_bias_guess[0] + step)
2✔
711
        t_bias_guess.append(new_t_bias)
2✔
712
        mismatch.append(new_mismatch)
2✔
713

714
        if abs(mismatch[-1]) < precision_percent:
2✔
715
            return t_bias_guess, mismatch
2✔
716

717
        # Now start with splin fit for guessing
718
        while len(t_bias_guess) < maxiter:
1✔
719
            # get next guess from splin (fit partial linear function to previously
720
            # calculated (mismatch, t_bias) pairs and get t_bias value where
721
            # mismatch=0 from this fitted curve)
722
            sort_index = np.argsort(np.array(mismatch))
1✔
723
            tck = interpolate.splrep(np.array(mismatch)[sort_index],
1✔
724
                                     np.array(t_bias_guess)[sort_index],
725
                                     k=1)
726
            # here we catch interpolation errors (two different t_bias with
727
            # same mismatch), could happen if one t_bias was close to a newly
728
            # defined limit
729
            if np.isnan(tck[1]).any():
1✔
730
                if was_errors[0]:
1✔
UNCOV
731
                    raise RuntimeError('Not able to minimise without '
×
732
                                       'exceeding the domain! Best '
733
                                       f'mismatch '
734
                                       f'{np.min(np.abs(mismatch))}%')
735
                elif was_errors[1]:
1✔
UNCOV
736
                    raise RuntimeError('Not able to minimise without ice '
×
737
                                       'free glacier! Best mismatch '
738
                                       f'{np.min(np.abs(mismatch))}%')
739
                else:
740
                    raise RuntimeError('Not able to minimise! Problem is '
1✔
741
                                       'unknown, need to check by hand! Best '
742
                                       'mismatch '
743
                                       f'{np.min(np.abs(mismatch))}%')
744
            new_mismatch, new_t_bias = get_mismatch(float(interpolate.splev(0,
1✔
745
                                                                            tck)
746
                                                          ))
747
            t_bias_guess.append(new_t_bias)
1✔
748
            mismatch.append(new_mismatch)
1✔
749

750
            if abs(mismatch[-1]) < precision_percent:
1✔
751
                return t_bias_guess, mismatch
1✔
752

753
        # Ok when we end here the spinup could not find satisfying match after
754
        # maxiter(ations)
755
        raise RuntimeError(f'Could not find mismatch smaller '
1✔
756
                           f'{precision_percent}% (only '
757
                           f'{np.min(np.abs(mismatch))}%) in {maxiter}'
758
                           f'Iterations!')
759

760
    # define function for the actual minimisation
761
    c_fun, model_dynamic_spinup_end, other_variable_mismatch = init_cost_fct()
2✔
762

763
    # define the MassBalanceModels for different spinup periods and try to
764
    # minimise, if minimisation fails a shorter spinup period is used
765
    # (first a spinup period between initial period and 'min_spinup_period'
766
    # years and the second try is to use a period of 'min_spinup_period' years,
767
    # if it still fails the actual error is raised)
768
    if spinup_start_yr is not None:
2✔
769
        spinup_period_initial = min(yr_rgi - spinup_start_yr, yr_rgi - yr_min)
2✔
770
    else:
771
        spinup_period_initial = min(spinup_period, yr_rgi - yr_min)
1✔
772
    if spinup_period_initial <= min_spinup_period:
2✔
773
        spinup_periods_to_try = [min_spinup_period]
1✔
774
    else:
775
        # try out a maximum of three different spinup_periods
776
        spinup_periods_to_try = [spinup_period_initial,
2✔
777
                                 int((spinup_period_initial + min_spinup_period) / 2),
778
                                 min_spinup_period]
779
    # after defining the initial spinup period we can define the year for the
780
    # fixed_geometry_spinup
781
    if add_fixed_geometry_spinup:
2✔
782
        fixed_geometry_spinup_yr = yr_rgi - spinup_period_initial
2✔
783
    else:
784
        fixed_geometry_spinup_yr = None
1✔
785

786
    # check if the user provided an mb_model_spinup, otherwise we must define a
787
    # new one each iteration
788
    provided_mb_model_spinup = False
2✔
789
    if mb_model_spinup is not None:
2✔
UNCOV
790
        provided_mb_model_spinup = True
×
791

792
    for spinup_period in spinup_periods_to_try:
2✔
793
        yr_spinup = yr_rgi - spinup_period
2✔
794

795
        if not provided_mb_model_spinup:
2✔
796
            # define spinup MassBalance
797
            # spinup is running for 'yr_rgi - yr_spinup' years, using a
798
            # ConstantMassBalance
799
            y0_spinup = (yr_spinup + yr_rgi) / 2
2✔
800
            halfsize_spinup = yr_rgi - y0_spinup
2✔
801
            mb_model_spinup = MultipleFlowlineMassBalance(
2✔
802
                gdir, mb_model_class=ConstantMassBalance,
803
                filename='climate_historical',
804
                input_filesuffix=climate_input_filesuffix, y0=y0_spinup,
805
                halfsize=halfsize_spinup)
806

807
        # try to conduct minimisation, if an error occurred try shorter spinup
808
        # period
809
        try:
2✔
810
            final_t_bias_guess, final_mismatch = minimise_with_spline_fit(c_fun)
2✔
811
            # ok no error occurred so we succeeded
812
            break
2✔
813
        except RuntimeError as e:
1✔
814
            # if the last spinup period was min_spinup_period the dynamic
815
            # spinup failed
816
            if spinup_period == min_spinup_period:
1✔
817
                log.warning('No dynamic spinup could be conducted and the '
1✔
818
                            'original model with no spinup is saved using the '
819
                            f'provided output_filesuffix "{output_filesuffix}". '
820
                            f'The error message of the dynamic spinup is: {e}')
821
                if ignore_errors:
1✔
822
                    model_dynamic_spinup_end = save_model_without_dynamic_spinup()
1✔
823
                    if return_t_bias_best:
1✔
824
                        return model_dynamic_spinup_end, np.nan
1✔
825
                    else:
826
                        return model_dynamic_spinup_end
1✔
827
                else:
828
                    # delete all files which could be saved during the previous
829
                    # iterations
830
                    if geom_path and os.path.exists(geom_path):
1✔
831
                        os.remove(geom_path)
1✔
832

833
                    if fl_diag_path and os.path.exists(fl_diag_path):
1✔
UNCOV
834
                        os.remove(fl_diag_path)
×
835

836
                    if diag_path and os.path.exists(diag_path):
1✔
837
                        os.remove(diag_path)
1✔
838

839
                    raise RuntimeError(e)
1✔
840

841
    # hurray, dynamic spinup successfully
842
    gdir.add_to_diagnostics('run_dynamic_spinup_success', True)
2✔
843

844
    # also save some other stuff
845
    gdir.add_to_diagnostics('temp_bias_dynamic_spinup',
2✔
846
                            float(final_t_bias_guess[-1]))
847
    gdir.add_to_diagnostics('dynamic_spinup_period',
2✔
848
                            int(spinup_period))
849
    gdir.add_to_diagnostics('dynamic_spinup_forward_model_iterations',
2✔
850
                            int(forward_model_runs[-1]))
851
    gdir.add_to_diagnostics(f'{minimise_for}_mismatch_dynamic_spinup_{unit}_'
2✔
852
                            f'percent',
853
                            float(final_mismatch[-1]))
854
    gdir.add_to_diagnostics(f'reference_{minimise_for}_dynamic_spinup_{unit}',
2✔
855
                            float(reference_value))
856
    gdir.add_to_diagnostics('dynamic_spinup_other_variable_reference',
2✔
857
                            float(other_reference_value))
858
    gdir.add_to_diagnostics('dynamic_spinup_mismatch_other_variable_percent',
2✔
859
                            float(other_variable_mismatch[-1]))
860

861
    # here only save the final model state if store_model_evolution = False
862
    if not store_model_evolution:
2✔
863
        with np.warnings.catch_warnings():
1✔
864
            # For operational runs we ignore the warnings
865
            np.warnings.filterwarnings('ignore', category=RuntimeWarning)
1✔
866
            model_dynamic_spinup_end[-1].run_until_and_store(
1✔
867
                yr_rgi,
868
                geom_path=geom_path,
869
                diag_path=diag_path,
870
                fl_diag_path=fl_diag_path, )
871

872
    if return_t_bias_best:
2✔
873
        return model_dynamic_spinup_end[-1], final_t_bias_guess[-1]
2✔
874
    else:
875
        return model_dynamic_spinup_end[-1]
1✔
876

877

878
def define_new_mu_star_in_gdir(gdir, new_mu_star, bias=0):
9✔
879
    """
880
    Helper function to define a new mu star in an gdir. Is used inside the run
881
    functions of the dynamic mu star calibration.
882

883
    Parameters
884
    ----------
885
    gdir : :py:class:`oggm.GlacierDirectory`
886
        the glacier directory to change the mu star
887
    new_mu_star : float
888
        the new mu_star to save in the gdir
889
    bias : float
890
        an additional bias to add to the mass balance at the end (was
891
        originally used for the static mu star calibration)
892

893
    Returns
894
    -------
895

896
    """
897
    df = gdir.read_json('local_mustar')
1✔
898
    df['bias'] = bias
1✔
899
    df['mu_star_per_flowline'] = [new_mu_star] * len(df['mu_star_per_flowline'])
1✔
900
    df['mu_star_glacierwide'] = new_mu_star
1✔
901
    df['mu_star_flowline_avg'] = new_mu_star
1✔
902
    df['mu_star_allsame'] = True
1✔
903
    gdir.write_json(df, 'local_mustar')
1✔
904

905

906
def dynamic_mu_star_run_with_dynamic_spinup(
9✔
907
        gdir, mu_star, yr0_ref_mb, yr1_ref_mb, fls_init, ys, ye,
908
        output_filesuffix='', evolution_model=FluxBasedModel,
909
        mb_model_historical=None, mb_model_spinup=None,
910
        minimise_for='area', climate_input_filesuffix='', spinup_period=20,
911
        min_spinup_period=10, yr_rgi=None, precision_percent=1,
912
        precision_absolute=1, min_ice_thickness=None,
913
        first_guess_t_bias=-2, t_bias_max_step_length=2, maxiter=30,
914
        store_model_geometry=True, store_fl_diagnostics=None,
915
        local_variables=None, set_local_variables=False, do_inversion=True,
916
        spinup_start_yr_max=None, add_fixed_geometry_spinup=True,
917
        **kwargs):
918
    """
919
    This function is one option for a 'run_function' for the
920
    'run_dynamic_mu_star_calibration' function (the corresponding
921
    'fallback_function' is
922
    'dynamic_mu_star_run_with_dynamic_spinup_fallback'). This
923
    function defines a new mu_star in the glacier directory and conducts an
924
    inversion calibrating A to match '_vol_m3_ref' with this new mu_star
925
    ('calibrate_inversion_from_consensus'). Afterwards a dynamic spinup is
926
    conducted to match 'minimise_for' (for more info look at docstring of
927
    'run_dynamic_spinup'). And in the end the geodetic mass balance of the
928
    current run is calculated (between the period [yr0_ref_mb, yr1_ref_mb]) and
929
    returned.
930

931
    Parameters
932
    ----------
933
    gdir : :py:class:`oggm.GlacierDirectory`
934
        the glacier directory to process
935
    mu_star : float
936
        the mu_star used for this run
937
    yr0_ref_mb : int
938
        the start year of the geodetic mass balance
939
    yr1_ref_mb : int
940
        the end year of the geodetic mass balance
941
    fls_init : []
942
        List of flowlines to use to initialise the model
943
    ys : int
944
        start year of the complete run, must by smaller or equal y0_ref_mb
945
    ye : int
946
        end year of the complete run, must be smaller or equal y1_ref_mb
947
    output_filesuffix : str
948
        For the output file.
949
        Default is ''
950
    evolution_model : class:oggm.core.FlowlineModel
951
        Evolution model to use.
952
        Default is FluxBasedModel
953
    mb_model_historical : :py:class:`core.MassBalanceModel`
954
        User-povided MassBalanceModel instance for the historical run. Default
955
        is to use a PastMassBalance model  together with the provided
956
        parameter climate_input_filesuffix.
957
    mb_model_spinup : :py:class:`core.MassBalanceModel`
958
        User-povided MassBalanceModel instance for the spinup before the
959
        historical run. Default is to use a ConstantMassBalance model together
960
        with the provided parameter climate_input_filesuffix and during the
961
        period of spinup_start_yr until rgi_year (e.g. 1979 - 2000).
962
    minimise_for : str
963
        The variable we want to match at yr_rgi. Options are 'area' or 'volume'.
964
        Default is 'area'.
965
    climate_input_filesuffix : str
966
        filesuffix for the input climate file
967
        Default is ''
968
    spinup_period : int
969
        The period how long the spinup should run. Start date of historical run
970
        is defined "yr_rgi - spinup_period". Minimum allowed value is defined
971
        with 'min_spinup_period'. If the provided climate data starts at year
972
        later than (yr_rgi - spinup_period) the spinup_period is set to
973
        (yr_rgi - yr_climate_start). Caution if spinup_start_yr is set the
974
        spinup_period is ignored.
975
        Default is 20
976
    min_spinup_period : int
977
        If the dynamic spinup function fails with the initial 'spinup_period'
978
        a shorter period is tried. Here you can define the minimum period to
979
        try.
980
        Default is 10
981
    yr_rgi : int or None
982
        The rgi date, at which we want to match area or volume.
983
        If None, gdir.rgi_date + 1 is used (the default).
984
        Default is None
985
    precision_percent : float
986
        Gives the precision we want to match for the selected variable
987
        ('minimise_for') at rgi_date in percent. The algorithm makes sure that
988
        the resulting relative mismatch is smaller than precision_percent, but
989
        also that the absolute value is smaller than precision_absolute.
990
        Default is 1, meaning the difference must be within 1% of the given
991
        value (area or volume).
992
    precision_absolute : float
993
        Gives an minimum absolute value to match. The algorithm makes sure that
994
        the resulting relative mismatch is smaller than
995
        precision_percent, but also that the absolute value is
996
        smaller than precision_absolute.
997
        The unit of precision_absolute depends on minimise_for (if 'area' in
998
        km2, if 'volume' in km3)
999
        Default is 1.
1000
    min_ice_thickness : float
1001
        Gives an minimum ice thickness for model grid points which are counted
1002
        to the total model value. This could be useful to filter out seasonal
1003
        'glacier growth', as OGGM do not differentiate between snow and ice in
1004
        the forward model run. Therefore you could see quite fast changes
1005
        (spikes) in the time-evolution (especially visible in length and area).
1006
        If you set this value to 0 the filtering can be switched off.
1007
        Default is 10.
1008
    first_guess_t_bias : float
1009
        The initial guess for the temperature bias for the spinup
1010
        MassBalanceModel in °C.
1011
        Default is -2.
1012
    t_bias_max_step_length : float
1013
        Defines the maximums allowed change of t_bias between two iterations. Is
1014
        needed to avoid to large changes.
1015
        Default is 2
1016
    maxiter : int
1017
        Maximum number of minimisation iterations per dynamic spinup where area
1018
        or volume is tried to be matched. If reached and 'ignore_errors=False'
1019
        an error is raised.
1020
        Default is 30
1021
    store_model_geometry : bool
1022
        whether to store the full model geometry run file to disk or not.
1023
        Default is True
1024
    store_fl_diagnostics : bool or None
1025
        Whether to store the model flowline diagnostics to disk or not.
1026
        Default is None (-> cfg.PARAMS['store_fl_diagnostics'])
1027
    local_variables : dict
1028
        User MUST provide a dictionary here. This dictionary is used to save
1029
        the last temperature bias from the previous dynamic spinup run (for
1030
        optimisation) and the initial glacier volume. User must take care that
1031
        this variables are not changed outside this function!
1032
        Default is None (-> raise an error if no dict is provided)
1033
    set_local_variables : bool
1034
        If True this resets the local_variables to their initial state. It
1035
        sets the first_guess_t_bias with the key 't_bias' AND sets the current
1036
        glacier volume with the key 'vol_m3_ref' which is later used in the
1037
        calibration during inversion.
1038
        Default is False
1039
    do_inversion : bool
1040
        If True a complete inversion is conducted using the provided mu_star
1041
        before the actual calibration run.
1042
        Default is False
1043
    spinup_start_yr_max : int or None
1044
        Possibility to provide a maximum year where the dynamic spinup must
1045
        start from at least. If set, this overrides the min_spinup_period if
1046
        yr_rgi - spinup_start_yr_max > min_spinup_period. If None it is set to
1047
        yr0_ref_mb.
1048
        Default is None
1049
    add_fixed_geometry_spinup : bool
1050
        If True and the original spinup_period of the dynamical spinup must be
1051
        shortened (due to ice-free or out-of-boundary error) a
1052
        fixed-geometry-spinup is added at the beginning so that the resulting
1053
        model run always starts from ys.
1054
        Default is True
1055
    kwargs : dict
1056
        kwargs to pass to the evolution_model instance
1057

1058
    Returns
1059
    -------
1060
    :py:class:`oggm.core.flowline.evolution_model`, float
1061
        The final model after the run and the calculated geodetic mass balance
1062
    """
1063
    if not isinstance(local_variables, dict):
2✔
1064
        raise ValueError('You must provide a dict for local_variables!')
1✔
1065

1066
    from oggm.workflow import calibrate_inversion_from_consensus
2✔
1067

1068
    if set_local_variables:
2✔
1069
        # clear the provided dictionary and set the first elements
1070
        local_variables.clear()
2✔
1071
        local_variables['t_bias'] = [first_guess_t_bias]
2✔
1072
        # ATTENTION: it is assumed that the flowlines in gdir have the volume
1073
        # we want to match during calibrate_inversion_from_consensus when we
1074
        # set_local_variables
1075
        fls_ref = gdir.read_pickle('model_flowlines')
2✔
1076
        local_variables['vol_m3_ref'] = np.sum([f.volume_m3 for f in fls_ref])
2✔
1077

1078
        # we are done with preparing the local_variables for the upcoming iterations
1079
        return None
2✔
1080

1081
    if yr_rgi is None:
2✔
UNCOV
1082
        yr_rgi = gdir.rgi_date + 1  # + 1 converted to hydro years
×
1083
    if min_spinup_period > yr_rgi - ys:
2✔
UNCOV
1084
        log.workflow('The RGI year is closer to ys as the minimum spinup '
×
1085
                     'period -> therefore the minimum spinup period is '
1086
                     'adapted and it is the only period which is tried by the '
1087
                     'dynamic spinup function!')
UNCOV
1088
        min_spinup_period = yr_rgi - ys
×
UNCOV
1089
        spinup_period = yr_rgi - ys
×
UNCOV
1090
        min_ice_thickness = 0
×
1091

1092
    if spinup_start_yr_max is None:
2✔
1093
        spinup_start_yr_max = yr0_ref_mb
2✔
1094

1095
    if spinup_start_yr_max > yr0_ref_mb:
2✔
UNCOV
1096
        log.workflow('The provided maximum start year is larger then the '
×
1097
                     'start year of the geodetic period, therefore it will be '
1098
                     'set to the start year of the geodetic period!')
UNCOV
1099
        spinup_start_yr_max = yr0_ref_mb
×
1100

1101
    # check that inversion is only possible without providing own fls
1102
    if do_inversion:
2✔
1103
        if not np.all([np.all(getattr(fl_prov, 'surface_h') ==
2✔
1104
                              getattr(fl_orig, 'surface_h')) and
1105
                       np.all(getattr(fl_prov, 'bed_h') ==
1106
                              getattr(fl_orig, 'bed_h'))
1107
                       for fl_prov, fl_orig in
1108
                       zip(fls_init, gdir.read_pickle('model_flowlines'))]):
1109
            raise InvalidWorkflowError('If you want to perform a dynamic '
1✔
1110
                                       'mu_star calibration including an '
1111
                                       'inversion, it is not possible to '
1112
                                       'provide your own flowlines! (fls_init '
1113
                                       'should be None or '
1114
                                       'the original model_flowlines)')
1115

1116
    # Here we start with the actual model run
1117
    if mu_star == gdir.read_json('local_mustar')['mu_star_glacierwide']:
2✔
1118
        # we do not need to define a new mu_star or do an inversion
1119
        do_inversion = False
2✔
1120
    else:
1121
        define_new_mu_star_in_gdir(gdir, mu_star)
1✔
1122

1123
    if do_inversion:
2✔
1124
        with utils.DisableLogger():
1✔
1125
            apparent_mb_from_any_mb(gdir,
1✔
1126
                                    add_to_log_file=False,  # dont write to log
1127
                                    )
1128
            # do inversion with A calibration to current volume
1129
            calibrate_inversion_from_consensus(
1✔
1130
                [gdir], apply_fs_on_mismatch=True, error_on_mismatch=False,
1131
                filter_inversion_output=True,
1132
                volume_m3_reference=local_variables['vol_m3_ref'],
1133
                add_to_log_file=False)
1134

1135
    # this is used to keep the original model_flowline unchanged (-> to be able
1136
    # to conduct different dynamic calibration runs in the same gdir)
1137
    model_flowline_filesuffix = '_dyn_mu_calib'
2✔
1138
    init_present_time_glacier(gdir, filesuffix=model_flowline_filesuffix,
2✔
1139
                              add_to_log_file=False)
1140

1141
    # Now do a dynamic spinup to match area
1142
    # do not ignore errors in dynamic spinup, so all 'bad' files are
1143
    # deleted in run_dynamic_spinup function
1144
    try:
2✔
1145
        model, last_best_t_bias = run_dynamic_spinup(
2✔
1146
            gdir,
1147
            continue_on_error=False,  # force to raise an error in @entity_task
1148
            add_to_log_file=False,  # dont write to log file in @entity_task
1149
            init_model_fls=fls_init,
1150
            climate_input_filesuffix=climate_input_filesuffix,
1151
            evolution_model=evolution_model,
1152
            mb_model_historical=mb_model_historical,
1153
            mb_model_spinup=mb_model_spinup,
1154
            spinup_period=spinup_period,
1155
            spinup_start_yr=ys,
1156
            spinup_start_yr_max=spinup_start_yr_max,
1157
            min_spinup_period=min_spinup_period, yr_rgi=yr_rgi,
1158
            precision_percent=precision_percent,
1159
            precision_absolute=precision_absolute,
1160
            min_ice_thickness=min_ice_thickness,
1161
            t_bias_max_step_length=t_bias_max_step_length,
1162
            maxiter=maxiter,
1163
            minimise_for=minimise_for,
1164
            first_guess_t_bias=local_variables['t_bias'][-1],
1165
            output_filesuffix=output_filesuffix,
1166
            store_model_evolution=True, ignore_errors=False,
1167
            return_t_bias_best=True, ye=ye,
1168
            store_model_geometry=store_model_geometry,
1169
            store_fl_diagnostics=store_fl_diagnostics,
1170
            model_flowline_filesuffix=model_flowline_filesuffix,
1171
            make_compatible=True,
1172
            add_fixed_geometry_spinup=add_fixed_geometry_spinup,
1173
            **kwargs)
1174
        # save the temperature bias which was successful in the last iteration
1175
        # as we expect we are not so far away in the next iteration (only
1176
        # needed for optimisation, potentially need less iterations in
1177
        # run_dynamic_spinup)
1178
        local_variables['t_bias'].append(last_best_t_bias)
2✔
1179
    except RuntimeError as e:
1✔
1180
        raise RuntimeError(f'Dynamic spinup raised error! (Message: {e})')
1✔
1181

1182
    # calculate dmdtda from previous simulation here
1183
    with utils.DisableLogger():
2✔
1184
        ds = utils.compile_run_output(gdir, input_filesuffix=output_filesuffix,
2✔
1185
                                      path=False)
1186
    dmdtda_mdl = ((ds.volume.loc[yr1_ref_mb].values -
2✔
1187
                   ds.volume.loc[yr0_ref_mb].values) /
1188
                  gdir.rgi_area_m2 /
1189
                  (yr1_ref_mb - yr0_ref_mb) *
1190
                  cfg.PARAMS['ice_density'])
1191

1192
    return model, dmdtda_mdl
2✔
1193

1194

1195
def dynamic_mu_star_run_with_dynamic_spinup_fallback(
9✔
1196
        gdir, mu_star, fls_init, ys, ye, local_variables, output_filesuffix='',
1197
        evolution_model=FluxBasedModel, minimise_for='area',
1198
        mb_model_historical=None, mb_model_spinup=None,
1199
        climate_input_filesuffix='', spinup_period=20, min_spinup_period=10,
1200
        yr_rgi=None, precision_percent=1,
1201
        precision_absolute=1, min_ice_thickness=10,
1202
        first_guess_t_bias=-2, t_bias_max_step_length=2, maxiter=30,
1203
        store_model_geometry=True, store_fl_diagnostics=None,
1204
        do_inversion=True, spinup_start_yr_max=None,
1205
        add_fixed_geometry_spinup=True, **kwargs):
1206
    """
1207
    This is the fallback function corresponding to the function
1208
    'dynamic_mu_star_run_with_dynamic_spinup', which are provided
1209
    to 'run_dynamic_mu_star_calibration'. It is used if the run_function fails and
1210
    if 'ignore_error == True' in 'run_dynamic_mu_star_calibration'. First it resets
1211
    mu_star of gdir. Afterwards it tries to conduct a dynamic spinup. If this
1212
    also fails the last thing is to just do a run without a dynamic spinup.
1213

1214
    Parameters
1215
    ----------
1216
    gdir : :py:class:`oggm.GlacierDirectory`
1217
        the glacier directory to process
1218
    mu_star : float
1219
        the mu_star used for this run
1220
    fls_init : []
1221
        List of flowlines to use to initialise the model
1222
    ys : int
1223
        start year of the run
1224
    ye : int
1225
        end year of the run
1226
    local_variables : dict
1227
        Dict in which under the key 'vol_m3_ref' the volume which is used in
1228
        'calibrate_inversion_from_consensus'
1229
    output_filesuffix : str
1230
        For the output file.
1231
        Default is ''
1232
    evolution_model : class:oggm.core.FlowlineModel
1233
        Evolution model to use.
1234
        Default is FluxBasedModel
1235
    mb_model_historical : :py:class:`core.MassBalanceModel`
1236
        User-povided MassBalanceModel instance for the historical run. Default
1237
        is to use a PastMassBalance model  together with the provided
1238
        parameter climate_input_filesuffix.
1239
    mb_model_spinup : :py:class:`core.MassBalanceModel`
1240
        User-povided MassBalanceModel instance for the spinup before the
1241
        historical run. Default is to use a ConstantMassBalance model together
1242
        with the provided parameter climate_input_filesuffix and during the
1243
        period of spinup_start_yr until rgi_year (e.g. 1979 - 2000).
1244
    minimise_for : str
1245
        The variable we want to match at yr_rgi. Options are 'area' or 'volume'.
1246
        Default is 'area'.
1247
    climate_input_filesuffix : str
1248
        filesuffix for the input climate file
1249
        Default is ''
1250
    spinup_period : int
1251
        The period how long the spinup should run. Start date of historical run
1252
        is defined "yr_rgi - spinup_period". Minimum allowed value is defined
1253
        with 'min_spinup_period'. If the provided climate data starts at year
1254
        later than (yr_rgi - spinup_period) the spinup_period is set to
1255
        (yr_rgi - yr_climate_start). Caution if spinup_start_yr is set the
1256
        spinup_period is ignored.
1257
        Default is 20
1258
    min_spinup_period : int
1259
        If the dynamic spinup function fails with the initial 'spinup_period'
1260
        a shorter period is tried. Here you can define the minimum period to
1261
        try.
1262
        Default is 10
1263
    yr_rgi : int or None
1264
        The rgi date, at which we want to match area or volume.
1265
        If None, gdir.rgi_date + 1 is used (the default).
1266
        Default is None
1267
    precision_percent : float
1268
        Gives the precision we want to match for the selected variable
1269
        ('minimise_for') at rgi_date in percent. The algorithm makes sure that
1270
        the resulting relative mismatch is smaller than precision_percent, but
1271
        also that the absolute value is smaller than precision_absolute.
1272
        Default is 1, meaning the difference must be within 1% of the given
1273
        value (area or volume).
1274
    precision_absolute : float
1275
        Gives an minimum absolute value to match. The algorithm makes sure that
1276
        the resulting relative mismatch is smaller than
1277
        precision_percent, but also that the absolute value is
1278
        smaller than precision_absolute.
1279
        The unit of precision_absolute depends on minimise_for (if 'area' in
1280
        km2, if 'volume' in km3)
1281
        Default is 1.
1282
    min_ice_thickness : float
1283
        Gives an minimum ice thickness for model grid points which are counted
1284
        to the total model value. This could be useful to filter out seasonal
1285
        'glacier growth', as OGGM do not differentiate between snow and ice in
1286
        the forward model run. Therefore you could see quite fast changes
1287
        (spikes) in the time-evolution (especially visible in length and area).
1288
        If you set this value to 0 the filtering can be switched off.
1289
        Default is 10.
1290
    first_guess_t_bias : float
1291
        The initial guess for the temperature bias for the spinup
1292
        MassBalanceModel in °C.
1293
        Default is -2.
1294
    t_bias_max_step_length : float
1295
        Defines the maximums allowed change of t_bias between two iterations. Is
1296
        needed to avoid to large changes.
1297
        Default is 2
1298
    maxiter : int
1299
        Maximum number of minimisation iterations per dynamic spinup where area
1300
        or volume is tried to be matched. If reached and 'ignore_errors=False'
1301
        an error is raised.
1302
        Default is 30
1303
    store_model_geometry : bool
1304
        whether to store the full model geometry run file to disk or not.
1305
        Default is True
1306
    store_fl_diagnostics : bool or None
1307
        Whether to store the model flowline diagnostics to disk or not.
1308
        Default is None (-> cfg.PARAMS['store_fl_diagnostics'])
1309
    do_inversion : bool
1310
        If True a complete inversion is conducted using the provided mu_star
1311
        before the actual fallback run.
1312
        Default is False
1313
    spinup_start_yr_max : int or None
1314
        Possibility to provide a maximum year where the dynamic spinup must
1315
        start from at least. If set, this overrides the min_spinup_period if
1316
        yr_rgi - spinup_start_yr_max > min_spinup_period.
1317
        Default is None
1318
    add_fixed_geometry_spinup : bool
1319
        If True and the original spinup_period of the dynamical spinup must be
1320
        shortened (due to ice-free or out-of-boundary error) a
1321
        fixed-geometry-spinup is added at the beginning so that the resulting
1322
        model run always starts from ys.
1323
        Default is True
1324
    kwargs : dict
1325
        kwargs to pass to the evolution_model instance
1326

1327
    Returns
1328
    -------
1329
    :py:class:`oggm.core.flowline.evolution_model`
1330
        The final model after the run.
1331
    """
1332
    from oggm.workflow import calibrate_inversion_from_consensus
1✔
1333

1334
    if local_variables is None:
1✔
1335
        raise RuntimeError('Need the volume to do'
1✔
1336
                           'calibrate_inversion_from_consensus provided in '
1337
                           'local_variables!')
1338

1339
    # revert gdir to original state if necessary
1340
    if mu_star != gdir.read_json('local_mustar')['mu_star_glacierwide']:
1✔
1341
        define_new_mu_star_in_gdir(gdir, mu_star)
1✔
1342
        if do_inversion:
1✔
1343
            with utils.DisableLogger():
1✔
1344
                apparent_mb_from_any_mb(gdir,
1✔
1345
                                        add_to_log_file=False)
1346
                calibrate_inversion_from_consensus(
1✔
1347
                    [gdir], apply_fs_on_mismatch=True, error_on_mismatch=False,
1348
                    filter_inversion_output=True,
1349
                    volume_m3_reference=local_variables['vol_m3_ref'],
1350
                    add_to_log_file=False)
1351
    if os.path.isfile(os.path.join(gdir.dir,
1✔
1352
                                   'model_flowlines_dyn_mu_calib.pkl')):
1353
        os.remove(os.path.join(gdir.dir,
1✔
1354
                               'model_flowlines_dyn_mu_calib.pkl'))
1355

1356
    if yr_rgi is None:
1✔
1357
        yr_rgi = gdir.rgi_date + 1  # + 1 converted to hydro years
1✔
1358
    if min_spinup_period > yr_rgi - ys:
1✔
UNCOV
1359
        log.workflow('The RGI year is closer to ys as the minimum spinup '
×
1360
                     'period -> therefore the minimum spinup period is '
1361
                     'adapted and it is the only period which is tried by the '
1362
                     'dynamic spinup function!')
UNCOV
1363
        min_spinup_period = yr_rgi - ys
×
UNCOV
1364
        spinup_period = yr_rgi - ys
×
UNCOV
1365
        min_ice_thickness = 0
×
1366

1367
    yr_clim_min = gdir.get_climate_info()['baseline_hydro_yr_0']
1✔
1368
    try:
1✔
1369
        model_end = run_dynamic_spinup(
1✔
1370
            gdir,
1371
            continue_on_error=False,  # force to raise an error in @entity_task
1372
            add_to_log_file=False,
1373
            init_model_fls=fls_init,
1374
            climate_input_filesuffix=climate_input_filesuffix,
1375
            evolution_model=evolution_model,
1376
            mb_model_historical=mb_model_historical,
1377
            mb_model_spinup=mb_model_spinup,
1378
            spinup_period=spinup_period,
1379
            spinup_start_yr=ys,
1380
            min_spinup_period=min_spinup_period,
1381
            spinup_start_yr_max=spinup_start_yr_max,
1382
            yr_rgi=yr_rgi,
1383
            minimise_for=minimise_for,
1384
            precision_percent=precision_percent,
1385
            precision_absolute=precision_absolute,
1386
            min_ice_thickness=min_ice_thickness,
1387
            first_guess_t_bias=first_guess_t_bias,
1388
            t_bias_max_step_length=t_bias_max_step_length,
1389
            maxiter=maxiter,
1390
            output_filesuffix=output_filesuffix,
1391
            store_model_geometry=store_model_geometry,
1392
            store_fl_diagnostics=store_fl_diagnostics,
1393
            ignore_errors=False,
1394
            ye=ye,
1395
            make_compatible=True,
1396
            add_fixed_geometry_spinup=add_fixed_geometry_spinup,
1397
            **kwargs)
1398

1399
        gdir.add_to_diagnostics('used_spinup_option', 'dynamic spinup only')
1✔
1400

1401
    except RuntimeError:
1✔
1402
        log.warning('No dynamic spinup could be conducted by using the '
1✔
1403
                    f'original mu* ({mu_star}). Therefore the last '
1404
                    'try is to conduct a run until ye without a dynamic '
1405
                    'spinup.')
1406
        model_end = run_from_climate_data(
1✔
1407
            gdir,
1408
            add_to_log_file=False,
1409
            min_ys=yr_clim_min, ye=ye,
1410
            output_filesuffix=output_filesuffix,
1411
            climate_input_filesuffix=climate_input_filesuffix,
1412
            store_model_geometry=store_model_geometry,
1413
            store_fl_diagnostics=store_fl_diagnostics,
1414
            init_model_fls=fls_init, evolution_model=evolution_model,
1415
            fixed_geometry_spinup_yr=ys)
1416

1417
        gdir.add_to_diagnostics('used_spinup_option', 'fixed geometry spinup')
1✔
1418

1419
        # set all dynamic diagnostics to None if there where some successful
1420
        # runs
1421
        diag = gdir.get_diagnostics()
1✔
1422
        if minimise_for == 'area':
1✔
1423
            unit = 'km2'
1✔
1424
        elif minimise_for == 'volume':
1✔
1425
            unit = 'km3'
1✔
1426
        else:
1427
            raise NotImplementedError
1428
        for key in ['temp_bias_dynamic_spinup', 'dynamic_spinup_period',
1✔
1429
                    'dynamic_spinup_forward_model_iterations',
1430
                    f'{minimise_for}_mismatch_dynamic_spinup_{unit}_percent',
1431
                    f'reference_{minimise_for}_dynamic_spinup_{unit}',
1432
                    'dynamic_spinup_other_variable_reference',
1433
                    'dynamic_spinup_mismatch_other_variable_percent']:
1434
            if key in diag:
1✔
1435
                gdir.add_to_diagnostics(key, None)
1✔
1436

1437
        gdir.add_to_diagnostics('run_dynamic_spinup_success', False)
1✔
1438
    return model_end
1✔
1439

1440

1441
def dynamic_mu_star_run(
9✔
1442
        gdir, mu_star, yr0_ref_mb, yr1_ref_mb, fls_init, ys, ye,
1443
        output_filesuffix='', evolution_model=FluxBasedModel,
1444
        local_variables=None, set_local_variables=False, yr_rgi=None,
1445
        **kwargs):
1446
    """
1447
    This function is one option for a 'run_function' for the
1448
    'run_dynamic_mu_star_calibration' function (the corresponding
1449
    'fallback_function' is 'dynamic_mu_star_run_fallback'). It is meant to
1450
    define a new mu_star in the given gdir and conduct a
1451
    'run_from_climate_data' run between ys and ye and return the geodetic mass
1452
    balance (units: kg m-2 yr-1) of the period yr0_ref_mb and yr1_ref_mb.
1453

1454
    Parameters
1455
    ----------
1456
    gdir : :py:class:`oggm.GlacierDirectory`
1457
        the glacier directory to process
1458
    mu_star : float
1459
        the mu_star used for this run
1460
    yr0_ref_mb : int
1461
        the start year of the geodetic mass balance
1462
    yr1_ref_mb : int
1463
        the end year of the geodetic mass balance
1464
    fls_init : []
1465
        List of flowlines to use to initialise the model
1466
    ys : int
1467
        start year of the complete run, must by smaller or equal y0_ref_mb
1468
    ye : int
1469
        end year of the complete run, must be smaller or equal y1_ref_mb
1470
    output_filesuffix : str
1471
        For the output file.
1472
        Default is ''
1473
    evolution_model : class:oggm.core.FlowlineModel
1474
        Evolution model to use.
1475
        Default is FluxBasedModel
1476
    local_variables : None
1477
        Not needed in this function, just here to match with the function
1478
        call in run_dynamic_mu_star_calibration.
1479
    set_local_variables : bool
1480
        Not needed in this function. Only here to be confirm with the use of
1481
        this function in 'run_dynamic_mu_star_calibration'.
1482
    yr_rgi : int or None
1483
        The rgi year of the gdir.
1484
        Default is None
1485
    kwargs : dict
1486
        kwargs to pass to the evolution_model instance
1487

1488
    Returns
1489
    -------
1490
    :py:class:`oggm.core.flowline.evolution_model`, float
1491
        The final model after the run and the calculated geodetic mass balance
1492
    """
1493

1494
    if set_local_variables:
1✔
1495
        # No local variables needed in this function
1496
        return None
1✔
1497

1498
    # Here we start with the actual model run
1499
    define_new_mu_star_in_gdir(gdir, mu_star)
1✔
1500

1501
    # conduct model run
1502
    try:
1✔
1503
        model = run_from_climate_data(gdir,
1✔
1504
                                      # force to raise an error in @entity_task
1505
                                      continue_on_error=False,
1506
                                      add_to_log_file=False,
1507
                                      ys=ys, ye=ye,
1508
                                      output_filesuffix=output_filesuffix,
1509
                                      init_model_fls=fls_init,
1510
                                      evolution_model=evolution_model,
1511
                                      **kwargs)
1512
    except RuntimeError as e:
1✔
1513
        raise RuntimeError(f'run_from_climate_data raised error! '
1✔
1514
                           f'(Message: {e})')
1515

1516
    # calculate dmdtda from previous simulation here
1517
    with utils.DisableLogger():
1✔
1518
        ds = utils.compile_run_output(gdir, input_filesuffix=output_filesuffix,
1✔
1519
                                      path=False)
1520
    dmdtda_mdl = ((ds.volume.loc[yr1_ref_mb].values -
1✔
1521
                   ds.volume.loc[yr0_ref_mb].values) /
1522
                  gdir.rgi_area_m2 /
1523
                  (yr1_ref_mb - yr0_ref_mb) *
1524
                  cfg.PARAMS['ice_density'])
1525

1526
    return model, dmdtda_mdl
1✔
1527

1528

1529
def dynamic_mu_star_run_fallback(
9✔
1530
        gdir, mu_star, fls_init, ys, ye, local_variables, output_filesuffix='',
1531
        evolution_model=FluxBasedModel, yr_rgi=None, **kwargs):
1532
    """
1533
    This is the fallback function corresponding to the function
1534
    'dynamic_mu_star_run', which are provided to
1535
    'run_dynamic_mu_star_calibration'. It is used if the run_function fails and
1536
    if 'ignore_error=True' in 'run_dynamic_mu_star_calibration'. It sets
1537
    mu_star and conduct a run_from_climate_data run from ys to ye.
1538

1539
    Parameters
1540
    ----------
1541
    gdir : :py:class:`oggm.GlacierDirectory`
1542
        the glacier directory to process
1543
    mu_star : float
1544
        the mu_star used for this run
1545
    fls_init : []
1546
        List of flowlines to use to initialise the model
1547
    ys : int
1548
        start year of the run
1549
    ye : int
1550
        end year of the run
1551
    local_variables : dict
1552
        Not needed in this function, just here to match with the function
1553
        call in run_dynamic_mu_star_calibration.
1554
    output_filesuffix : str
1555
        For the output file.
1556
        Default is ''
1557
    evolution_model : class:oggm.core.FlowlineModel
1558
        Evolution model to use.
1559
        Default is FluxBasedModel
1560
    yr_rgi : int or None
1561
        The rgi year of the gdir.
1562
        Default is None
1563
    kwargs : dict
1564
        kwargs to pass to the evolution_model instance
1565

1566
     Returns
1567
    -------
1568
    :py:class:`oggm.core.flowline.evolution_model`
1569
        The final model after the run.
1570
    """
1571

1572
    define_new_mu_star_in_gdir(gdir, mu_star)
1✔
1573

1574
    # conduct model run
1575
    try:
1✔
1576
        model = run_from_climate_data(gdir,
1✔
1577
                                      # force to raise an error in @entity_task
1578
                                      continue_on_error=False,
1579
                                      add_to_log_file=False,
1580
                                      ys=ys, ye=ye,
1581
                                      output_filesuffix=output_filesuffix,
1582
                                      init_model_fls=fls_init,
1583
                                      evolution_model=evolution_model,
1584
                                      **kwargs)
1585
        gdir.add_to_diagnostics('used_spinup_option', 'no spinup')
1✔
UNCOV
1586
    except RuntimeError as e:
×
UNCOV
1587
        raise RuntimeError(f'In fallback function run_from_climate_data '
×
1588
                           f'raised error! (Message: {e})')
1589

1590
    return model
1✔
1591

1592

1593
@entity_task(log, writes=['inversion_flowlines'])
9✔
1594
def run_dynamic_mu_star_calibration(
9✔
1595
        gdir, ref_dmdtda=None, err_ref_dmdtda=None, err_dmdtda_scaling_factor=1,
1596
        ref_period='', ignore_hydro_months=False, min_mu_star=None,
1597
        max_mu_star=None, mu_star_max_step_length=5, maxiter=20,
1598
        ignore_errors=False, output_filesuffix='_dynamic_mu_star',
1599
        ys=None, ye=None,
1600
        run_function=dynamic_mu_star_run_with_dynamic_spinup,
1601
        kwargs_run_function=None,
1602
        fallback_function=dynamic_mu_star_run_with_dynamic_spinup_fallback,
1603
        kwargs_fallback_function=None, init_model_filesuffix=None,
1604
        init_model_yr=None, init_model_fls=None,
1605
        first_guess_diagnostic_msg='dynamic spinup only'):
1606
    """Calibrate mu_star to match a geodetic mass balance incorporating a
1607
    dynamic model run.
1608

1609
    This task iteratively search for a mu_star to match a provided geodetic
1610
    mass balance. How one model run looks like is defined in the 'run_function'.
1611
    This function should take a new mu_star guess, conducts a dynamic run and
1612
    calculate the geodetic mass balance. The goal is to match the geodetic mass
1613
    blanance 'ref_dmdtda' inside the provided error 'err_ref_dmdtda'. If the
1614
    minimisation of the mismatch between the provided and modeled geodetic mass
1615
    balance is not working the 'fallback_function' is called. In there it is
1616
    decided what run should be conducted in such a failing case. Further if
1617
    'ignore_error' is set to True and we could not find a satisfying mismatch
1618
    the best run so far is saved (if not one successful run with 'run_function'
1619
    the 'fallback_function' is called).
1620

1621
    Parameters
1622
    ----------
1623
    gdir : :py:class:`oggm.GlacierDirectory`
1624
        the glacier directory to process
1625
    ref_dmdtda : float or None
1626
        The reference geodetic mass balance to match (units: kg m-2 yr-1). If
1627
        None the data from Hugonnet 2021 is used.
1628
        Default is None
1629
    err_ref_dmdtda : float or None
1630
        The error of the reference geodetic mass balance to match (unit: kg m-2
1631
        yr-1). Must always be a positive number. If None the data from Hugonett
1632
        2021 is used.
1633
        Default is None
1634
    err_dmdtda_scaling_factor : float
1635
        The error of the geodetic mass balance is multiplied by this factor.
1636
        When looking at more glaciers you should set this factor smaller than
1637
        1 (Default), but the smaller this factor the more glaciers will fail
1638
        during calibration. The factor is only used if ref_dmdtda = None and
1639
        err_ref_dmdtda = None.
1640
        The idea is that we reduce the uncertainty of individual observations
1641
        to count for correlated uncertainties when looking at regional or
1642
        global scales. If err_scaling_factor is 1 (Default) and you look at the
1643
        results of more than one glacier this equals that all errors are
1644
        uncorrelated. Therefore the result will be outside the uncertainty
1645
        boundaries given in Hugonett 2021 e.g. for the global estimate, because
1646
        some correlation of the individual errors is assumed during aggregation
1647
        of glaciers to regions (for more details see paper Hugonett 2021).
1648
    ref_period : str
1649
        If ref_dmdtda is None one of '2000-01-01_2010-01-01',
1650
        '2010-01-01_2020-01-01', '2000-01-01_2020-01-01'. If ref_dmdtda is
1651
        set, this should still match the same format but can be any date.
1652
        Default is '' (-> PARAMS['geodetic_mb_period'])
1653
    ignore_hydro_months : bool
1654
        Do not raise an  error if we are working on calendar years.
1655
        Default is False
1656
    min_mu_star : float or None
1657
        Lower absolute limit for mu*.
1658
        Default is None (-> cfg.PARAMS['min_mu_star'])
1659
    max_mu_star : float or None
1660
        Upper absolute limit for mu*
1661
        Default is None (-> cfg.PARAMS['max_mu_star'])
1662
    mu_star_max_step_length : float
1663
        Defines the maximum allowed change of mu_star between two iterations.
1664
        Is needed to avoid to large changes.
1665
        Default is 5
1666
    maxiter : int
1667
        Maximum number of minimisation iterations of minimising mismatch to
1668
        dmdtda by changing mu_star. Each of this iterations conduct a complete
1669
        run defined in the 'run_function'. If maxiter_mu_star reached and
1670
        'ignore_errors=False' an error is raised.
1671
        Default is 20
1672
    ignore_errors : bool
1673
        If True and the 'run_function' with mu* star calibration is not working
1674
        to match dmdtda inside the provided uncertainty first, if their where
1675
        some successful runs with the 'run_function' they are saved as part
1676
        success, and if not a single run was successful the 'fallback_function'
1677
        is called. If False and the 'run_function' with mu* star calibration is
1678
        not working an Error is raised.
1679
        Default is True
1680
    output_filesuffix : str
1681
        For the output file.
1682
        Default is '_historical_dynamic_mu_star'
1683
    ys : int or None
1684
        The start year of the conducted run. If None the first year of the
1685
        provided climate file.
1686
        Default is None
1687
    ye : int or None
1688
        The end year of the conducted run. If None the last year of the
1689
        provided climate file.
1690
        Default is None
1691
    run_function : function
1692
        This function defines how a new defined mu_star is used to conduct the
1693
        next model run. This function must contain the arguments 'gdir',
1694
        'mu_star', 'yr0_ref_mb', 'yr1_ref_mb', 'fls_init', 'ys', 'ye' and
1695
        'output_filesuffix'. Further this function must return the final model
1696
        and the calculated geodetic mass balance dmdtda in kg m-2 yr-1.
1697
    kwargs_run_function : None or dict
1698
        Can provide additional keyword arguments to the run_function as a
1699
        dictionary.
1700
    fallback_function : function
1701
        This is a fallback function if the calibration is not working using
1702
        'run_function' it is called. This function must contain the arguments
1703
        'gdir', 'mu_star', 'fls_init', 'ys', 'ye', 'local_variables' and
1704
        'output_filesuffix'. Further this function should return the final
1705
        model.
1706
    kwargs_fallback_function : None or dict
1707
        Can provide additional keyword arguments to the fallback_function as a
1708
        dictionary.
1709
    init_model_filesuffix : str or None
1710
        If you want to start from a previous model run state. This state
1711
        should be at time yr_rgi_date.
1712
        Default is None
1713
    init_model_yr : int or None
1714
        the year of the initial run you want to start from. The default
1715
        is to take the last year of the simulation.
1716
    init_model_fls : []
1717
        List of flowlines to use to initialise the model (the default is the
1718
        present_time_glacier file from the glacier directory).
1719
        Ignored if `init_model_filesuffix` is set.
1720
    first_guess_diagnostic_msg : str
1721
        This message will be added to the glacier diagnostics if only the
1722
        default mu* resulted in a successful 'run_function' run.
1723
        Default is 'dynamic spinup only'
1724

1725
    Returns
1726
    -------
1727
    :py:class:`oggm.core.flowline.evolution_model`
1728
        The final dynamically spined-up model. Type depends on the selected
1729
        evolution_model, by default a FluxBasedModel.
1730
    """
1731
    # mu* constraints
1732
    if min_mu_star is None:
2✔
1733
        min_mu_star = cfg.PARAMS['min_mu_star']
2✔
1734
    if max_mu_star is None:
2✔
UNCOV
1735
        max_mu_star = cfg.PARAMS['max_mu_star']
×
1736

1737
    if kwargs_run_function is None:
2✔
1738
        kwargs_run_function = {}
1✔
1739
    if kwargs_fallback_function is None:
2✔
1740
        kwargs_fallback_function = {}
1✔
1741

1742
    sm = cfg.PARAMS['hydro_month_' + gdir.hemisphere]
2✔
1743
    if sm != 1 and not ignore_hydro_months:
2✔
UNCOV
1744
        raise InvalidParamsError('run_dynamic_mu_star_calibration makes '
×
1745
                                 'more sense when applied on calendar years '
1746
                                 "(PARAMS['hydro_month_nh']=1 and "
1747
                                 "`PARAMS['hydro_month_sh']=1). If you want "
1748
                                 "to ignore this error, set "
1749
                                 "ignore_hydro_months to True")
1750

1751
    if max_mu_star > 1000:
2✔
UNCOV
1752
        raise InvalidParamsError('You seem to have set a very high '
×
1753
                                 'max_mu_star for this run. This is not '
1754
                                 'how this task is supposed to work, and '
1755
                                 'we recommend a value lower than 1000 '
1756
                                 '(or even 600). You can directly provide a '
1757
                                 'value for max_mu_star or set '
1758
                                 "cfg.PARAMS['max_mu_star'] to a smaller value!")
1759

1760
    # geodetic mb stuff
1761
    if not ref_period:
2✔
1762
        ref_period = cfg.PARAMS['geodetic_mb_period']
2✔
1763
    # if a reference geodetic mb is specified also the error of it must be
1764
    # specified, and vice versa
1765
    if ((ref_dmdtda is None and err_ref_dmdtda is not None) or
2✔
1766
            (ref_dmdtda is not None and err_ref_dmdtda is None)):
1767
        raise RuntimeError('If you provide a reference geodetic mass balance '
1✔
1768
                           '(ref_dmdtda) you must also provide an error for it '
1769
                           '(err_ref_dmdtda), and vice versa!')
1770
    # Get the reference geodetic mb and error if not given
1771
    if ref_dmdtda is None:
2✔
1772
        df_ref_dmdtda = utils.get_geodetic_mb_dataframe().loc[gdir.rgi_id]
2✔
1773
        # reference geodetic mass balance from Hugonnet 2021
1774
        ref_dmdtda = float(
2✔
1775
            df_ref_dmdtda.loc[df_ref_dmdtda['period'] == ref_period]['dmdtda'])
1776
        # dmdtda: in meters water-equivalent per year -> we convert
1777
        ref_dmdtda *= 1000  # kg m-2 yr-1
2✔
1778
        # error of reference geodetic mass balance from Hugonnet 2021
1779
        err_ref_dmdtda = float(df_ref_dmdtda.loc[df_ref_dmdtda['period'] ==
2✔
1780
                                                 ref_period]['err_dmdtda'])
1781
        err_ref_dmdtda *= 1000  # kg m-2 yr-1
2✔
1782
        err_ref_dmdtda *= err_dmdtda_scaling_factor
2✔
1783

1784
    if err_ref_dmdtda <= 0:
2✔
1785
        raise RuntimeError('The provided error for the geodetic mass-balance '
1✔
1786
                           '(err_ref_dmdtda) must be positive and non zero! But'
1787
                           f'given was {err_ref_dmdtda}!')
1788
    # get start and end year of geodetic mb
1789
    yr0_ref_mb, yr1_ref_mb = ref_period.split('_')
2✔
1790
    yr0_ref_mb = int(yr0_ref_mb.split('-')[0])
2✔
1791
    yr1_ref_mb = int(yr1_ref_mb.split('-')[0])
2✔
1792

1793
    if ye is None:
2✔
1794
        # One adds 1 because the run ends at the end of the year
1795
        ye = gdir.get_climate_info()['baseline_hydro_yr_1'] + 1
1✔
1796

1797
    if ye < yr1_ref_mb:
2✔
1798
        raise RuntimeError('The provided ye is smaller than the end year of '
1✔
1799
                           'the given geodetic_mb_period!')
1800

1801
    if ys is None:
2✔
1802
        ys = gdir.get_climate_info()['baseline_hydro_yr_0']
1✔
1803

1804
    if ys > yr0_ref_mb:
2✔
1805
        raise RuntimeError('The provided ys is larger than the start year of '
1✔
1806
                           'the given geodetic_mb_period!')
1807

1808
    yr_rgi = gdir.rgi_date + 1  # + 1 converted to hydro years
2✔
1809
    if yr_rgi < ys:
2✔
UNCOV
1810
        if ignore_errors:
×
1811
            log.workflow('The rgi year is smaller than the provided start year '
×
1812
                         'ys -> setting the rgi year to ys to continue!')
UNCOV
1813
            yr_rgi = ys
×
1814
        else:
UNCOV
1815
            raise RuntimeError('The rgi year is smaller than the provided '
×
1816
                               'start year ys!')
1817
    kwargs_run_function['yr_rgi'] = yr_rgi
2✔
1818
    kwargs_fallback_function['yr_rgi'] = yr_rgi
2✔
1819

1820
    # get initial flowlines from which we want to start from
1821
    if init_model_filesuffix is not None:
2✔
1822
        fp = gdir.get_filepath('model_geometry',
1✔
1823
                               filesuffix=init_model_filesuffix)
1824
        fmod = FileModel(fp)
1✔
1825
        if init_model_yr is None:
1✔
1826
            init_model_yr = fmod.last_yr
1✔
1827
        fmod.run_until(init_model_yr)
1✔
1828
        init_model_fls = fmod.fls
1✔
1829

1830
    if init_model_fls is None:
2✔
1831
        fls_init = gdir.read_pickle('model_flowlines')
2✔
1832
    else:
1833
        fls_init = copy.deepcopy(init_model_fls)
1✔
1834

1835
    # save original mu star for later to be able to recreate original gdir
1836
    # (using the fallback function) if an error occurs
1837
    mu_star_initial = gdir.read_json('local_mustar')['mu_star_glacierwide']
2✔
1838

1839
    # only used to check performance of minimisation
1840
    dynamic_mu_star_calibration_runs = [0]
2✔
1841

1842
    # this function is called if the actual dynamic mu star calibration fails
1843
    def fallback_run(mu_star, reset, best_mismatch=None, initial_mismatch=None,
2✔
1844
                     only_first_guess=None):
1845
        if reset:
1✔
1846
            # unfortunately we could not conduct an error free run using the
1847
            # provided run_function, so we us the fallback_function
1848

1849
            # this diagnostics should be overwritten inside the fallback_function
1850
            gdir.add_to_diagnostics('used_spinup_option', 'fallback_function')
1✔
1851

1852
            model = fallback_function(gdir=gdir, mu_star=mu_star,
1✔
1853
                                      fls_init=fls_init, ys=ys, ye=ye,
1854
                                      local_variables=local_variables_run_function,
1855
                                      output_filesuffix=output_filesuffix,
1856
                                      **kwargs_fallback_function)
1857
        else:
1858
            # we were not able to reach the desired precision during the
1859
            # minimisation, but at least we conducted a few error free runs
1860
            # using the run_function, and therefore we save the best guess we
1861
            # found so far
1862
            if only_first_guess:
1✔
UNCOV
1863
                gdir.add_to_diagnostics('used_spinup_option',
×
1864
                                        first_guess_diagnostic_msg)
1865
            else:
1866
                gdir.add_to_diagnostics('used_spinup_option',
1✔
1867
                                        'dynamic mu_star calibration (part '
1868
                                        'success)')
1869
            model, dmdtda_mdl = run_function(gdir=gdir, mu_star=mu_star,
1✔
1870
                                             yr0_ref_mb=yr0_ref_mb,
1871
                                             yr1_ref_mb=yr1_ref_mb,
1872
                                             fls_init=fls_init, ys=ys, ye=ye,
1873
                                             output_filesuffix=output_filesuffix,
1874
                                             local_variables=local_variables_run_function,
1875
                                             **kwargs_run_function)
1876

1877
            gdir.add_to_diagnostics(
1✔
1878
                'dmdtda_mismatch_dynamic_calibration_reference',
1879
                float(ref_dmdtda))
1880
            gdir.add_to_diagnostics(
1✔
1881
                'dmdtda_dynamic_calibration_given_error',
1882
                float(err_ref_dmdtda))
1883
            gdir.add_to_diagnostics(
1✔
1884
                'dmdtda_mismatch_dynamic_calibration',
1885
                float(best_mismatch))
1886
            gdir.add_to_diagnostics(
1✔
1887
                'dmdtda_mismatch_with_initial_mu_star',
1888
                float(initial_mismatch))
1889
            gdir.add_to_diagnostics('mu_star_dynamic_calibration',
1✔
1890
                                    float(mu_star))
1891
            gdir.add_to_diagnostics('mu_star_before_dynamic_calibration',
1✔
1892
                                    float(mu_star_initial))
1893
            gdir.add_to_diagnostics('run_dynamic_mu_star_calibration_iterations',
1✔
1894
                                    int(dynamic_mu_star_calibration_runs[-1]))
1895

1896
        return model
1✔
1897

1898
    # here we define the local variables which are used in the run_function,
1899
    # for some run_functions this is useful to save parameters from a previous
1900
    # run to be faster in the upcoming runs
1901
    local_variables_run_function = {}
2✔
1902
    run_function(gdir=gdir, mu_star=None, yr0_ref_mb=None, yr1_ref_mb=None,
2✔
1903
                 fls_init=None, ys=None, ye=None,
1904
                 local_variables=local_variables_run_function,
1905
                 set_local_variables=True, **kwargs_run_function)
1906

1907
    # this is the actual model run which is executed each iteration in order to
1908
    # minimise the mismatch of dmdtda of model and observation
1909
    def model_run(mu_star):
2✔
1910
        # to check performance of minimisation
1911
        dynamic_mu_star_calibration_runs.append(
2✔
1912
            dynamic_mu_star_calibration_runs[-1] + 1)
1913

1914
        model, dmdtda_mdl = run_function(gdir=gdir, mu_star=mu_star,
2✔
1915
                                         yr0_ref_mb=yr0_ref_mb,
1916
                                         yr1_ref_mb=yr1_ref_mb,
1917
                                         fls_init=fls_init, ys=ys, ye=ye,
1918
                                         output_filesuffix=output_filesuffix,
1919
                                         local_variables=local_variables_run_function,
1920
                                         **kwargs_run_function)
1921
        return model, dmdtda_mdl
2✔
1922

1923
    def cost_fct(mu_star, model_dynamic_spinup_end):
2✔
1924

1925
        # actual model run
1926
        model_dynamic_spinup, dmdtda_mdl = model_run(mu_star)
2✔
1927

1928
        # save final model for later
1929
        model_dynamic_spinup_end.append(copy.deepcopy(model_dynamic_spinup))
2✔
1930

1931
        # calculate the mismatch of dmdtda
1932
        cost = float(dmdtda_mdl - ref_dmdtda)
2✔
1933

1934
        return cost
2✔
1935

1936
    def init_cost_fun():
2✔
1937
        model_dynamic_spinup_end = []
2✔
1938

1939
        def c_fun(mu_star):
2✔
1940
            return cost_fct(mu_star, model_dynamic_spinup_end)
2✔
1941

1942
        return c_fun, model_dynamic_spinup_end
2✔
1943

1944
    # Here start with own spline minimisation algorithm
1945
    def minimise_with_spline_fit(fct_to_minimise, mu_star_guess, mismatch):
2✔
1946
        # defines limits of mu in accordance to maximal allowed change
1947
        # between iterations
1948
        mu_star_limits = [mu_star_initial - mu_star_max_step_length,
2✔
1949
                          mu_star_initial + mu_star_max_step_length]
1950

1951
        # this two variables indicate that the limits were already adapted to
1952
        # avoid an error
1953
        was_min_error = False
2✔
1954
        was_max_error = False
2✔
1955
        was_errors = [was_min_error, was_max_error]
2✔
1956

1957
        def get_mismatch(mu_star):
2✔
1958
            mu_star = copy.deepcopy(mu_star)
2✔
1959
            # first check if the mu_star is inside limits
1960
            if mu_star < mu_star_limits[0]:
2✔
1961
                # was the smaller limit already executed, if not first do this
1962
                if mu_star_limits[0] not in mu_star_guess:
1✔
1963
                    mu_star = copy.deepcopy(mu_star_limits[0])
1✔
1964
                else:
1965
                    # smaller limit was already used, check if it was
1966
                    # already newly defined with error
1967
                    if was_errors[0]:
1✔
UNCOV
1968
                        raise RuntimeError('Not able to minimise without '
×
1969
                                           'raising an error at lower limit of '
1970
                                           'mu star!')
1971
                    else:
1972
                        # ok we set a new lower limit, consider also minimum
1973
                        # limit
1974
                        mu_star_limits[0] = max(min_mu_star,
1✔
1975
                                                mu_star_limits[0] -
1976
                                                mu_star_max_step_length)
1977
            elif mu_star > mu_star_limits[1]:
2✔
1978
                # was the larger limit already executed, if not first do this
1979
                if mu_star_limits[1] not in mu_star_guess:
1✔
1980
                    mu_star = copy.deepcopy(mu_star_limits[1])
1✔
1981
                else:
1982
                    # larger limit was already used, check if it was
1983
                    # already newly defined with ice free glacier
1984
                    if was_errors[1]:
×
UNCOV
1985
                        raise RuntimeError('Not able to minimise without '
×
1986
                                           'raising an error at upper limit of '
1987
                                           'mu star!')
1988
                    else:
1989
                        # ok we set a new upper limit, consider also maximum
1990
                        # limit
UNCOV
1991
                        mu_star_limits[1] = min(max_mu_star,
×
1992
                                                mu_star_limits[1] +
1993
                                                mu_star_max_step_length)
1994

1995
            # now clip mu_star with limits (to be sure)
1996
            mu_star = np.clip(mu_star, mu_star_limits[0], mu_star_limits[1])
2✔
1997
            if mu_star in mu_star_guess:
2✔
UNCOV
1998
                raise RuntimeError('This mu star was already tried. Probably '
×
1999
                                   'we are at one of the max or min limit and '
2000
                                   'still have no satisfactory mismatch '
2001
                                   'found!')
2002

2003
            # if error during dynamic calibration this defines how much
2004
            # mu_star is changed in the upcoming iteratoins to look for an
2005
            # error free run
2006
            mu_star_search_change = mu_star_max_step_length / 10
2✔
2007
            # maximum number of changes to look for an error free run
2008
            max_iterations = int(mu_star_max_step_length /
2✔
2009
                                 mu_star_search_change)
2010

2011
            current_min_error = False
2✔
2012
            current_max_error = False
2✔
2013
            doing_first_guess = (len(mismatch) == 0)
2✔
2014
            iteration = 0
2✔
2015
            current_mu_star = copy.deepcopy(mu_star)
2✔
2016

2017
            # in this loop if an error at the limits is raised we go step by
2018
            # step away from the limits until we are at the initial guess or we
2019
            # found an error free run
2020
            tmp_mismatch = None
2✔
2021
            while ((current_min_error | current_max_error | iteration == 0) &
2✔
2022
                   (iteration < max_iterations)):
2023
                try:
2✔
2024
                    tmp_mismatch = fct_to_minimise(mu_star)
2✔
2025
                except RuntimeError as e:
1✔
2026
                    # check if we are at the lower limit
2027
                    if mu_star == mu_star_limits[0]:
1✔
2028
                        # check if there was already an error at the lower limit
UNCOV
2029
                        if was_errors[0]:
×
UNCOV
2030
                            raise RuntimeError('Second time with error at '
×
2031
                                               'lower limit of mu star! '
2032
                                               'Error message of model run: '
2033
                                               f'{e}')
2034
                        else:
2035
                            was_errors[0] = True
×
UNCOV
2036
                            current_min_error = True
×
2037

2038
                    # check if we are at the upperlimit
2039
                    elif mu_star == mu_star_limits[1]:
1✔
2040
                        # check if there was already an error at the lower limit
UNCOV
2041
                        if was_errors[1]:
×
UNCOV
2042
                            raise RuntimeError('Second time with error at '
×
2043
                                               'upper limit of mu star! '
2044
                                               'Error message of model run: '
2045
                                               f'{e}')
2046
                        else:
UNCOV
2047
                            was_errors[1] = True
×
2048
                            current_max_error = True
×
2049

2050
                    if current_min_error:
1✔
2051
                        # currently we searching for a new lower limit with no
2052
                        # error
UNCOV
2053
                        mu_star = np.round(mu_star + mu_star_search_change,
×
2054
                                           decimals=1)
2055
                    elif current_max_error:
1✔
2056
                        # currently we searching for a new upper limit with no
2057
                        # error
UNCOV
2058
                        mu_star = np.round(mu_star - mu_star_search_change,
×
2059
                                           decimals=1)
2060

2061
                    # if we end close to an already executed guess while
2062
                    # searching for a new limit we quite
2063
                    if np.isclose(mu_star, mu_star_guess).any():
1✔
UNCOV
2064
                        raise RuntimeError('Not able to further minimise, '
×
2065
                                           'return the best we have so far!'
2066
                                           f'Error message: {e}')
2067

2068
                    if doing_first_guess:
1✔
2069
                        # unfortunately first guess is not working
2070
                        raise RuntimeError('Dynamic calibration is not working '
1✔
2071
                                           'with first guess! Error '
2072
                                           f'message: {e}')
2073

UNCOV
2074
                    if np.isclose(mu_star, current_mu_star):
×
2075
                        # something unexpected happen so we end here
UNCOV
2076
                        raise RuntimeError('Unexpected error not at the limits'
×
2077
                                           f' of mu star. Error Message: {e}')
2078

2079
                iteration += 1
2✔
2080

2081
            if iteration >= max_iterations:
2✔
2082
                # ok we were not able to find an mismatch without error
UNCOV
2083
                if current_min_error:
×
UNCOV
2084
                    raise RuntimeError('Not able to find new lower limit for '
×
2085
                                       'mu star!')
UNCOV
2086
                elif current_max_error:
×
UNCOV
2087
                    raise RuntimeError('Not able to find new upper limit for '
×
2088
                                       'mu star!')
2089
                else:
UNCOV
2090
                    raise RuntimeError('Something unexpected happened during '
×
2091
                                       'definition of new mu star limits!')
2092
            else:
2093
                # if we found a new limit set it
2094
                if current_min_error:
2✔
2095
                    mu_star_limits[0] = copy.deepcopy(mu_star)
×
2096
                elif current_max_error:
2✔
UNCOV
2097
                    mu_star_limits[1] = copy.deepcopy(mu_star)
×
2098

2099
            if tmp_mismatch is None:
2✔
UNCOV
2100
                raise RuntimeError('Not able to find a new mismatch for '
×
2101
                                   'dmdtda!')
2102

2103
            return float(tmp_mismatch), float(mu_star)
2✔
2104

2105
        # first guess
2106
        new_mismatch, new_mu_star = get_mismatch(mu_star_initial)
2✔
2107
        mu_star_guess.append(new_mu_star)
2✔
2108
        mismatch.append(new_mismatch)
2✔
2109

2110
        if abs(mismatch[-1]) < err_ref_dmdtda:
2✔
2111
            return mismatch[-1], new_mu_star
2✔
2112

2113
        # second (arbitrary) guess is given depending on the outcome of first
2114
        # guess, mu_star is changed for percent of mismatch relative to
2115
        # err_ref_dmdtda times mu_star_max_step_length (if
2116
        # mismatch = 2 * err_ref_dmdtda this corresponds to 100%; for 100% or
2117
        # 150% the next step is (-1) * mu_star_max_step_length; if mismatch
2118
        # -40%, next step is 0.4 * mu_star_max_step_length; but always at least
2119
        # a change of 0.5 is imposed to prevent too close guesses). (-1) as if
2120
        # mismatch is negative we need a larger mu_star to get closer to 0
2121
        step = (-1) * np.sign(mismatch[-1]) * \
1✔
2122
            max((np.abs(mismatch[-1]) - err_ref_dmdtda) / err_ref_dmdtda *
2123
                mu_star_max_step_length, 0.5)
2124
        new_mismatch, new_mu_star = get_mismatch(mu_star_guess[0] + step)
1✔
2125
        mu_star_guess.append(new_mu_star)
1✔
2126
        mismatch.append(new_mismatch)
1✔
2127

2128
        if abs(mismatch[-1]) < err_ref_dmdtda:
1✔
UNCOV
2129
            return mismatch[-1], new_mu_star
×
2130

2131
        # Now start with splin fit for guessing
2132
        while len(mu_star_guess) < maxiter:
1✔
2133
            # get next guess from splin (fit partial linear function to
2134
            # previously calculated (mismatch, mu_star) pairs and get mu_star
2135
            # value where mismatch=0 from this fitted curve)
2136
            sort_index = np.argsort(np.array(mismatch))
1✔
2137
            tck = interpolate.splrep(np.array(mismatch)[sort_index],
1✔
2138
                                     np.array(mu_star_guess)[sort_index],
2139
                                     k=1)
2140
            # here we catch interpolation errors (two different mu_star with
2141
            # same mismatch), could happen if one mu_star was close to a newly
2142
            # defined limit
2143
            if np.isnan(tck[1]).any():
1✔
UNCOV
2144
                if was_errors[0]:
×
UNCOV
2145
                    raise RuntimeError('Second time with error at lower '
×
2146
                                       'limit of mu star! (nan in splin fit)')
UNCOV
2147
                elif was_errors[1]:
×
UNCOV
2148
                    raise RuntimeError('Second time with error at upper '
×
2149
                                       'limit of mu star! (nan in splin fit)')
2150
                else:
UNCOV
2151
                    raise RuntimeError('Not able to minimise! Problem is '
×
2152
                                       'unknown. (nan in splin fit)')
2153
            new_mismatch, new_mu_star = get_mismatch(
1✔
2154
                float(interpolate.splev(0, tck)))
2155
            mu_star_guess.append(new_mu_star)
1✔
2156
            mismatch.append(new_mismatch)
1✔
2157

2158
            if abs(mismatch[-1]) < err_ref_dmdtda:
1✔
2159
                return mismatch[-1], new_mu_star
1✔
2160

2161
        # Ok when we end here the spinup could not find satisfying match after
2162
        # maxiter(ations)
2163
        raise RuntimeError(f'Could not find mismatch smaller '
1✔
2164
                           f'{err_ref_dmdtda} kg m-2 yr-1 (only '
2165
                           f'{np.min(np.abs(mismatch))} kg m-2 yr-1) in '
2166
                           f'{maxiter} Iterations!')
2167

2168
    # wrapper to get values for intermediate (mismatch, mu_star) guesses if an
2169
    # error is raised
2170
    def init_minimiser():
2✔
2171
        mu_star_guess = []
2✔
2172
        mismatch = []
2✔
2173

2174
        def minimiser(fct_to_minimise):
2✔
2175
            return minimise_with_spline_fit(fct_to_minimise, mu_star_guess,
2✔
2176
                                            mismatch)
2177

2178
        return minimiser, mu_star_guess, mismatch
2✔
2179

2180
    # define function for the actual minimisation
2181
    c_fun, models_dynamic_spinup_end = init_cost_fun()
2✔
2182

2183
    # define minimiser
2184
    minimise_given_fct, mu_star_guesses, mismatch_dmdtda = init_minimiser()
2✔
2185

2186
    try:
2✔
2187
        final_mismatch, final_mu_star = minimise_given_fct(c_fun)
2✔
2188
    except RuntimeError as e:
1✔
2189
        # something happened during minimisation, if there where some
2190
        # successful runs we return the one with the best mismatch, otherwise
2191
        # we conduct just a run with no dynamic spinup
2192
        if len(mismatch_dmdtda) == 0:
1✔
2193
            # we conducted no successful run, so run without dynamic spinup
2194
            if ignore_errors:
1✔
2195
                log.workflow('Dynamic mu star calibration not successful. '
1✔
2196
                             f'Error message: {e}')
2197
                model_return = fallback_run(mu_star=mu_star_initial,
1✔
2198
                                            reset=True)
2199
                return model_return
1✔
2200
            else:
UNCOV
2201
                raise RuntimeError('Dynamic mu star calibration was not '
×
2202
                                   f'successful! Error Message: {e}')
2203
        else:
2204
            if ignore_errors:
1✔
2205
                log.workflow('Dynamic mu star calibration not successful. Error '
1✔
2206
                             f'message: {e}')
2207

2208
                # there where some successful runs so we return the one with the
2209
                # smallest mismatch of dmdtda
2210
                min_mismatch_index = np.argmin(np.abs(mismatch_dmdtda))
1✔
2211
                mu_star_best = np.array(mu_star_guesses)[min_mismatch_index]
1✔
2212

2213
                # check if the first guess was the best guess
2214
                only_first_guess = False
1✔
2215
                if min_mismatch_index == 1:
1✔
UNCOV
2216
                    only_first_guess = True
×
2217

2218
                model_return = fallback_run(
1✔
2219
                    mu_star=mu_star_best, reset=False,
2220
                    best_mismatch=np.array(mismatch_dmdtda)[min_mismatch_index],
2221
                    initial_mismatch=mismatch_dmdtda[0],
2222
                    only_first_guess=only_first_guess)
2223

2224
                return model_return
1✔
2225
            else:
2226
                raise RuntimeError('Dynamic mu star calibration not successful. '
1✔
2227
                                   f'Error message: {e}')
2228

2229
    # check that new mu star is correctly saved in gdir
2230
    assert final_mu_star == gdir.read_json('local_mustar')['mu_star_glacierwide']
2✔
2231

2232
    # hurray, dynamic mu star calibration successful
2233
    gdir.add_to_diagnostics('used_spinup_option',
2✔
2234
                            'dynamic mu_star calibration (full success)')
2235
    gdir.add_to_diagnostics('dmdtda_mismatch_dynamic_calibration_reference',
2✔
2236
                            float(ref_dmdtda))
2237
    gdir.add_to_diagnostics('dmdtda_dynamic_calibration_given_error',
2✔
2238
                            float(err_ref_dmdtda))
2239
    gdir.add_to_diagnostics('dmdtda_mismatch_dynamic_calibration',
2✔
2240
                            float(final_mismatch))
2241
    gdir.add_to_diagnostics('dmdtda_mismatch_with_initial_mu_star',
2✔
2242
                            float(mismatch_dmdtda[0]))
2243
    gdir.add_to_diagnostics('mu_star_dynamic_calibration', float(final_mu_star))
2✔
2244
    gdir.add_to_diagnostics('mu_star_before_dynamic_calibration',
2✔
2245
                            float(mu_star_initial))
2246
    gdir.add_to_diagnostics('run_dynamic_mu_star_calibration_iterations',
2✔
2247
                            int(dynamic_mu_star_calibration_runs[-1]))
2248

2249
    log.workflow(f'Dynamic mu star calibration worked for {gdir.rgi_id}!')
2✔
2250

2251
    return models_dynamic_spinup_end[-1]
2✔
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