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

OGGM / oggm / 4510759150

pending completion
4510759150

Pull #1549

github

GitHub
Merge f8c800840 into 71e58edf5
Pull Request #1549: Update test image

11167 of 13069 relevant lines covered (85.45%)

3.56 hits per line

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

79.46
/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
import warnings
9✔
7

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

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

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

28

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

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

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

201
    Returns
202
    -------
203
    :py:class:`oggm.core.flowline.evolution_model`
204
        The final dynamically spined-up model. Type depends on the selected
205
        evolution_model.
206
    """
207

208
    evolution_model = decide_evolution_model(evolution_model)
2✔
209

210
    if yr_rgi is None:
2✔
211
        # Even in calendar dates, we prefer to set rgi_year in the next year
212
        # as the rgi is often from snow free images the year before (e.g. Aug)
213
        yr_rgi = gdir.rgi_date + 1
1✔
214

215
    if ye is None:
2✔
216
        ye = yr_rgi
1✔
217

218
    if ye < yr_rgi:
2✔
219
        raise RuntimeError(f'The provided end year (ye = {ye}) must be larger'
1✔
220
                           f'than the rgi date (yr_rgi = {yr_rgi}!')
221

222
    yr_min = gdir.get_climate_info()['baseline_yr_0']
2✔
223

224
    if min_ice_thickness is None:
2✔
225
        min_ice_thickness = cfg.PARAMS['dynamic_spinup_min_ice_thick']
2✔
226

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

243
    if init_model_filesuffix is not None:
2✔
244
        fp = gdir.get_filepath('model_geometry',
1✔
245
                               filesuffix=init_model_filesuffix)
246
        fmod = FileModel(fp)
1✔
247
        if init_model_yr is None:
1✔
248
            init_model_yr = fmod.last_yr
×
249
        fmod.run_until(init_model_yr)
1✔
250
        init_model_fls = fmod.fls
1✔
251

252
    if init_model_fls is None:
2✔
253
        fls_spinup = gdir.read_pickle('model_flowlines',
1✔
254
                                      filesuffix=model_flowline_filesuffix)
255
    else:
256
        fls_spinup = copy.deepcopy(init_model_fls)
2✔
257

258
    # MassBalance for actual run from yr_spinup to yr_rgi
259
    if mb_model_historical is None:
2✔
260
        mb_model_historical = MultipleFlowlineMassBalance(
2✔
261
            gdir, mb_model_class=MonthlyTIModel,
262
            filename='climate_historical',
263
            input_filesuffix=climate_input_filesuffix)
264

265
    # here we define the file-paths for the output
266
    if store_model_geometry:
2✔
267
        geom_path = gdir.get_filepath('model_geometry',
2✔
268
                                      filesuffix=output_filesuffix,
269
                                      delete=True)
270
    else:
271
        geom_path = False
1✔
272

273
    if store_fl_diagnostics is None:
2✔
274
        store_fl_diagnostics = cfg.PARAMS['store_fl_diagnostics']
2✔
275

276
    if store_fl_diagnostics:
2✔
277
        fl_diag_path = gdir.get_filepath('fl_diagnostics',
×
278
                                         filesuffix=output_filesuffix,
279
                                         delete=True)
280
    else:
281
        fl_diag_path = False
2✔
282

283
    diag_path = gdir.get_filepath('model_diagnostics',
2✔
284
                                  filesuffix=output_filesuffix,
285
                                  delete=True)
286

287
    if cfg.PARAMS['use_inversion_params_for_run']:
2✔
288
        diag = gdir.get_diagnostics()
2✔
289
        fs = diag.get('inversion_fs', cfg.PARAMS['fs'])
2✔
290
        glen_a = diag.get('inversion_glen_a', cfg.PARAMS['glen_a'])
2✔
291
    else:
292
        fs = cfg.PARAMS['fs']
1✔
293
        glen_a = cfg.PARAMS['glen_a']
1✔
294

295
    kwargs.setdefault('fs', fs)
2✔
296
    kwargs.setdefault('glen_a', glen_a)
2✔
297

298
    mb_elev_feedback = kwargs.get('mb_elev_feedback', 'annual')
2✔
299
    if mb_elev_feedback != 'annual':
2✔
300
        raise InvalidParamsError('Only use annual mb_elev_feedback with the '
1✔
301
                                 'dynamic spinup function!')
302

303
    if cfg.PARAMS['use_kcalving_for_run']:
2✔
304
        raise InvalidParamsError('Dynamic spinup not tested with '
1✔
305
                                 "cfg.PARAMS['use_kcalving_for_run'] is `True`!")
306

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

318
        with warnings.catch_warnings():
1✔
319
            if ye < yr_use:
1✔
320
                yr_run = yr_use
×
321
            else:
322
                yr_run = ye
1✔
323
            # For operational runs we ignore the warnings
324
            warnings.filterwarnings('ignore', category=RuntimeWarning)
1✔
325
            model_dynamic_spinup_end.run_until_and_store(
1✔
326
                yr_run,
327
                geom_path=geom_path,
328
                diag_path=diag_path,
329
                fl_diag_path=fl_diag_path,
330
                make_compatible=make_compatible)
331

332
        return model_dynamic_spinup_end
1✔
333

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

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

371
    # if reference value is zero no dynamic spinup is possible
372
    if reference_value == 0.:
2✔
373
        if ignore_errors:
1✔
374
            model_dynamic_spinup_end = save_model_without_dynamic_spinup()
1✔
375
            if return_t_bias_best:
1✔
376
                return model_dynamic_spinup_end, np.nan
×
377
            else:
378
                return model_dynamic_spinup_end
1✔
379
        else:
380
            raise RuntimeError('The given reference value is Zero, no dynamic '
1✔
381
                               'spinup possible!')
382

383
    # here we adjust the used precision_percent to make sure the resulting
384
    # absolute mismatch is smaller than precision_absolute
385
    precision_percent = min(precision_percent,
2✔
386
                            precision_absolute / reference_value * 100)
387

388
    # only used to check performance of function
389
    forward_model_runs = [0]
2✔
390

391
    # the actual spinup run
392
    def run_model_with_spinup_to_rgi_date(t_bias):
2✔
393
        forward_model_runs.append(forward_model_runs[-1] + 1)
2✔
394

395
        # with t_bias the glacier state after spinup is changed between iterations
396
        mb_model_spinup.temp_bias = t_bias
2✔
397
        # run the spinup
398
        model_spinup = evolution_model(copy.deepcopy(fls_spinup),
2✔
399
                                       mb_model_spinup,
400
                                       y0=0,
401
                                       **kwargs)
402
        model_spinup.run_until(2 * halfsize_spinup)
2✔
403

404
        # if glacier is completely gone return information in ice-free
405
        ice_free = False
2✔
406
        if np.isclose(model_spinup.volume_km3, 0.):
2✔
407
            ice_free = True
1✔
408

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

440
        if cost_var == 'area_km2':
2✔
441
            return model_area_km2, model_volume_km3, model_historical, ice_free
2✔
442
        elif cost_var == 'volume_km3':
1✔
443
            return model_volume_km3, model_area_km2, model_historical, ice_free
1✔
444
        else:
445
            raise NotImplementedError(f'{cost_var}')
446

447
    def cost_fct(t_bias, model_dynamic_spinup_end_loc, other_variable_mismatch_loc):
2✔
448
        # actual model run
449
        model_value, other_value, model_dynamic_spinup, ice_free = \
2✔
450
            run_model_with_spinup_to_rgi_date(t_bias)
451

452
        # save the final model for later
453
        model_dynamic_spinup_end_loc.append(copy.deepcopy(model_dynamic_spinup))
2✔
454

455
        # calculate the mismatch in percent
456
        cost = (model_value - reference_value) / reference_value * 100
2✔
457
        other_variable_mismatch_loc.append(
2✔
458
            (other_value - other_reference_value) / other_reference_value * 100)
459

460
        return cost, ice_free
2✔
461

462
    def init_cost_fct():
2✔
463
        model_dynamic_spinup_end_loc = []
2✔
464
        other_variable_mismatch_loc = []
2✔
465

466
        def c_fct(t_bias):
2✔
467
            return cost_fct(t_bias, model_dynamic_spinup_end_loc,
2✔
468
                            other_variable_mismatch_loc)
469

470
        return c_fct, model_dynamic_spinup_end_loc, other_variable_mismatch_loc
2✔
471

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

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

521
            # now clip t_bias with limits
522
            t_bias = np.clip(t_bias, t_bias_limits[0], t_bias_limits[1])
2✔
523

524
            # now start with mismatch calculation
525

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

541
            while ((is_out_of_domain | is_ice_free_spinup | is_ice_free_end) &
2✔
542
                   (iteration < max_iterations)):
543
                try:
2✔
544
                    tmp_mismatch, is_ice_free_spinup = fct_to_minimise(t_bias)
2✔
545

546
                    # no error occurred, so we are not outside the domain
547
                    is_out_of_domain = False
2✔
548

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

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

596
                    else:
597
                        is_ice_free_end = False
2✔
598

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

622
                    else:
623
                        # otherwise this error can not be handled here
624
                        raise RuntimeError(e)
×
625

626
                iteration += 1
2✔
627

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

643
                    raise RuntimeError(msg)
1✔
644

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

699
            return float(tmp_mismatch), float(t_bias)
2✔
700

701
        # first guess
702
        new_mismatch, new_t_bias = get_mismatch(first_guess_t_bias)
2✔
703
        t_bias_guess.append(new_t_bias)
2✔
704
        mismatch.append(new_mismatch)
2✔
705

706
        if abs(mismatch[-1]) < precision_percent:
2✔
707
            return t_bias_guess, mismatch
1✔
708

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

720
        if abs(mismatch[-1]) < precision_percent:
2✔
721
            return t_bias_guess, mismatch
1✔
722

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

756
            if abs(mismatch[-1]) < precision_percent:
2✔
757
                return t_bias_guess, mismatch
2✔
758

759
        # Ok when we end here the spinup could not find satisfying match after
760
        # maxiter(ations)
761
        raise RuntimeError(f'Could not find mismatch smaller '
1✔
762
                           f'{precision_percent}% (only '
763
                           f'{np.min(np.abs(mismatch))}%) in {maxiter}'
764
                           f'Iterations!')
765

766
    # define function for the actual minimisation
767
    c_fun, model_dynamic_spinup_end, other_variable_mismatch = init_cost_fct()
2✔
768

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

792
    # check if the user provided an mb_model_spinup, otherwise we must define a
793
    # new one each iteration
794
    provided_mb_model_spinup = False
2✔
795
    if mb_model_spinup is not None:
2✔
796
        provided_mb_model_spinup = True
×
797

798
    for spinup_period in spinup_periods_to_try:
2✔
799
        yr_spinup = yr_rgi - spinup_period
2✔
800

801
        if not provided_mb_model_spinup:
2✔
802
            # define spinup MassBalance
803
            # spinup is running for 'yr_rgi - yr_spinup' years, using a
804
            # ConstantMassBalance
805
            y0_spinup = (yr_spinup + yr_rgi) / 2
2✔
806
            halfsize_spinup = yr_rgi - y0_spinup
2✔
807
            mb_model_spinup = MultipleFlowlineMassBalance(
2✔
808
                gdir, mb_model_class=ConstantMassBalance,
809
                filename='climate_historical',
810
                input_filesuffix=climate_input_filesuffix, y0=y0_spinup,
811
                halfsize=halfsize_spinup)
812

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

839
                    if fl_diag_path and os.path.exists(fl_diag_path):
1✔
840
                        os.remove(fl_diag_path)
×
841

842
                    if diag_path and os.path.exists(diag_path):
1✔
843
                        os.remove(diag_path)
1✔
844

845
                    raise RuntimeError(e)
1✔
846

847
    # hurray, dynamic spinup successfully
848
    gdir.add_to_diagnostics('run_dynamic_spinup_success', True)
2✔
849

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

867
    # here only save the final model state if store_model_evolution = False
868
    if not store_model_evolution:
2✔
869
        with warnings.catch_warnings():
1✔
870
            # For operational runs we ignore the warnings
871
            warnings.filterwarnings('ignore', category=RuntimeWarning)
1✔
872
            model_dynamic_spinup_end[-1].run_until_and_store(
1✔
873
                yr_rgi,
874
                geom_path=geom_path,
875
                diag_path=diag_path,
876
                fl_diag_path=fl_diag_path, )
877

878
    if return_t_bias_best:
2✔
879
        return model_dynamic_spinup_end[-1], final_t_bias_guess[-1]
2✔
880
    else:
881
        return model_dynamic_spinup_end[-1]
1✔
882

883

884
def define_new_melt_f_in_gdir(gdir, new_melt_f):
9✔
885
    """
886
    Helper function to define a new melt_f in a gdir. Is used inside the run
887
    functions of the dynamic melt_f calibration.
888

889
    Parameters
890
    ----------
891
    gdir : :py:class:`oggm.GlacierDirectory`
892
        the glacier directory to change the melt_f
893
    new_melt_f : float
894
        the new melt_f to save in the gdir
895

896
    Returns
897
    -------
898

899
    """
900
    try:
1✔
901
        df = gdir.read_json('mb_calib')
1✔
902
    except FileNotFoundError:
×
903
        raise InvalidWorkflowError('`mb_calib.json` does not exist in gdir. '
×
904
                                   'You first need to calibrate the whole '
905
                                   'MassBalanceModel before changing melt_f '
906
                                   'alone!')
907
    df['melt_f'] = new_melt_f
1✔
908
    gdir.write_json(df, 'mb_calib')
1✔
909

910

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

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

1063
    Returns
1064
    -------
1065
    :py:class:`oggm.core.flowline.evolution_model`, float
1066
        The final model after the run and the calculated geodetic mass balance
1067
    """
1068

1069
    evolution_model = decide_evolution_model(evolution_model)
2✔
1070

1071
    if not isinstance(local_variables, dict):
2✔
1072
        raise ValueError('You must provide a dict for local_variables!')
1✔
1073

1074
    from oggm.workflow import calibrate_inversion_from_consensus
2✔
1075

1076
    if set_local_variables:
2✔
1077
        # clear the provided dictionary and set the first elements
1078
        local_variables.clear()
2✔
1079
        local_variables['t_bias'] = [first_guess_t_bias]
2✔
1080
        # ATTENTION: it is assumed that the flowlines in gdir have the volume
1081
        # we want to match during calibrate_inversion_from_consensus when we
1082
        # set_local_variables
1083
        fls_ref = gdir.read_pickle('model_flowlines')
2✔
1084
        local_variables['vol_m3_ref'] = np.sum([f.volume_m3 for f in fls_ref])
2✔
1085

1086
        # we are done with preparing the local_variables for the upcoming iterations
1087
        return None
2✔
1088

1089
    if yr_rgi is None:
2✔
1090
        yr_rgi = gdir.rgi_date + 1  # + 1 converted to hydro years
×
1091
    if min_spinup_period > yr_rgi - ys:
2✔
1092
        log.info('The RGI year is closer to ys as the minimum spinup '
×
1093
                 'period -> therefore the minimum spinup period is '
1094
                 'adapted and it is the only period which is tried by the '
1095
                 'dynamic spinup function!')
1096
        min_spinup_period = yr_rgi - ys
×
1097
        spinup_period = yr_rgi - ys
×
1098
        min_ice_thickness = 0
×
1099

1100
    if spinup_start_yr_max is None:
2✔
1101
        spinup_start_yr_max = yr0_ref_mb
2✔
1102

1103
    if spinup_start_yr_max > yr0_ref_mb:
2✔
1104
        log.info('The provided maximum start year is larger then the '
×
1105
                 'start year of the geodetic period, therefore it will be '
1106
                 'set to the start year of the geodetic period!')
1107
        spinup_start_yr_max = yr0_ref_mb
×
1108

1109
    # check that inversion is only possible without providing own fls
1110
    if do_inversion:
2✔
1111
        if not np.all([np.all(getattr(fl_prov, 'surface_h') ==
2✔
1112
                              getattr(fl_orig, 'surface_h')) and
1113
                       np.all(getattr(fl_prov, 'bed_h') ==
1114
                              getattr(fl_orig, 'bed_h'))
1115
                       for fl_prov, fl_orig in
1116
                       zip(fls_init, gdir.read_pickle('model_flowlines'))]):
1117
            raise InvalidWorkflowError('If you want to perform a dynamic '
×
1118
                                       'melt_f calibration including an '
1119
                                       'inversion, it is not possible to '
1120
                                       'provide your own flowlines! (fls_init '
1121
                                       'should be None or '
1122
                                       'the original model_flowlines)')
1123

1124
    # Here we start with the actual model run
1125
    if melt_f == gdir.read_json('mb_calib')['melt_f']:
2✔
1126
        # we do not need to define a new melt_f or do an inversion
1127
        do_inversion = False
2✔
1128
    else:
1129
        define_new_melt_f_in_gdir(gdir, melt_f)
1✔
1130

1131
    if do_inversion:
2✔
1132
        with utils.DisableLogger():
1✔
1133
            apparent_mb_from_any_mb(gdir,
1✔
1134
                                    add_to_log_file=False,  # dont write to log
1135
                                    )
1136
            # do inversion with A calibration to current volume
1137
            calibrate_inversion_from_consensus(
1✔
1138
                [gdir], apply_fs_on_mismatch=True, error_on_mismatch=False,
1139
                filter_inversion_output=True,
1140
                volume_m3_reference=local_variables['vol_m3_ref'],
1141
                add_to_log_file=False)
1142

1143
    # this is used to keep the original model_flowline unchanged (-> to be able
1144
    # to conduct different dynamic calibration runs in the same gdir)
1145
    model_flowline_filesuffix = '_dyn_melt_f_calib'
2✔
1146
    init_present_time_glacier(gdir, filesuffix=model_flowline_filesuffix,
2✔
1147
                              add_to_log_file=False)
1148

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

1190
    # calculate dmdtda from previous simulation here
1191
    with utils.DisableLogger():
2✔
1192
        ds = utils.compile_run_output(gdir, input_filesuffix=output_filesuffix,
2✔
1193
                                      path=False)
1194
    dmdtda_mdl = ((ds.volume.loc[yr1_ref_mb].values -
2✔
1195
                   ds.volume.loc[yr0_ref_mb].values) /
1196
                  gdir.rgi_area_m2 /
1197
                  (yr1_ref_mb - yr0_ref_mb) *
1198
                  cfg.PARAMS['ice_density'])
1199

1200
    return model, dmdtda_mdl
2✔
1201

1202

1203
def dynamic_melt_f_run_with_dynamic_spinup_fallback(
9✔
1204
        gdir, melt_f, fls_init, ys, ye, local_variables, output_filesuffix='',
1205
        evolution_model=None, minimise_for='area',
1206
        mb_model_historical=None, mb_model_spinup=None,
1207
        climate_input_filesuffix='', spinup_period=20, min_spinup_period=10,
1208
        yr_rgi=None, precision_percent=1,
1209
        precision_absolute=1, min_ice_thickness=10,
1210
        first_guess_t_bias=-2, t_bias_max_step_length=2, maxiter=30,
1211
        store_model_geometry=True, store_fl_diagnostics=None,
1212
        do_inversion=True, spinup_start_yr_max=None,
1213
        add_fixed_geometry_spinup=True, **kwargs):
1214
    """
1215
    This is the fallback function corresponding to the function
1216
    'dynamic_melt_f_run_with_dynamic_spinup', which are provided
1217
    to 'run_dynamic_melt_f_calibration'. It is used if the run_function fails and
1218
    if 'ignore_error == True' in 'run_dynamic_melt_f_calibration'. First it resets
1219
    melt_f of gdir. Afterwards it tries to conduct a dynamic spinup. If this
1220
    also fails the last thing is to just do a run without a dynamic spinup
1221
    (only a fixed geometry spinup).
1222

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

1336
    Returns
1337
    -------
1338
    :py:class:`oggm.core.flowline.evolution_model`
1339
        The final model after the run.
1340
    """
1341
    from oggm.workflow import calibrate_inversion_from_consensus
1✔
1342

1343
    evolution_model = decide_evolution_model(evolution_model)
1✔
1344

1345
    if local_variables is None:
1✔
1346
        raise RuntimeError('Need the volume to do'
1✔
1347
                           'calibrate_inversion_from_consensus provided in '
1348
                           'local_variables!')
1349

1350
    # revert gdir to original state if necessary
1351
    if melt_f != gdir.read_json('mb_calib')['melt_f']:
×
1352
        define_new_melt_f_in_gdir(gdir, melt_f)
×
1353
        if do_inversion:
×
1354
            with utils.DisableLogger():
×
1355
                apparent_mb_from_any_mb(gdir,
×
1356
                                        add_to_log_file=False)
1357
                calibrate_inversion_from_consensus(
×
1358
                    [gdir], apply_fs_on_mismatch=True, error_on_mismatch=False,
1359
                    filter_inversion_output=True,
1360
                    volume_m3_reference=local_variables['vol_m3_ref'],
1361
                    add_to_log_file=False)
1362
    if os.path.isfile(os.path.join(gdir.dir,
×
1363
                                   'model_flowlines_dyn_melt_f_calib.pkl')):
1364
        os.remove(os.path.join(gdir.dir,
×
1365
                               'model_flowlines_dyn_melt_f_calib.pkl'))
1366

1367
    if yr_rgi is None:
×
1368
        yr_rgi = gdir.rgi_date + 1  # + 1 converted to hydro years
×
1369
    if min_spinup_period > yr_rgi - ys:
×
1370
        log.info('The RGI year is closer to ys as the minimum spinup '
×
1371
                 'period -> therefore the minimum spinup period is '
1372
                 'adapted and it is the only period which is tried by the '
1373
                 'dynamic spinup function!')
1374
        min_spinup_period = yr_rgi - ys
×
1375
        spinup_period = yr_rgi - ys
×
1376
        min_ice_thickness = 0
×
1377

1378
    yr_clim_min = gdir.get_climate_info()['baseline_yr_0']
×
1379
    try:
×
1380
        model_end = run_dynamic_spinup(
×
1381
            gdir,
1382
            continue_on_error=False,  # force to raise an error in @entity_task
1383
            add_to_log_file=False,
1384
            init_model_fls=fls_init,
1385
            climate_input_filesuffix=climate_input_filesuffix,
1386
            evolution_model=evolution_model,
1387
            mb_model_historical=mb_model_historical,
1388
            mb_model_spinup=mb_model_spinup,
1389
            spinup_period=spinup_period,
1390
            spinup_start_yr=ys,
1391
            min_spinup_period=min_spinup_period,
1392
            spinup_start_yr_max=spinup_start_yr_max,
1393
            yr_rgi=yr_rgi,
1394
            minimise_for=minimise_for,
1395
            precision_percent=precision_percent,
1396
            precision_absolute=precision_absolute,
1397
            min_ice_thickness=min_ice_thickness,
1398
            first_guess_t_bias=first_guess_t_bias,
1399
            t_bias_max_step_length=t_bias_max_step_length,
1400
            maxiter=maxiter,
1401
            output_filesuffix=output_filesuffix,
1402
            store_model_geometry=store_model_geometry,
1403
            store_fl_diagnostics=store_fl_diagnostics,
1404
            ignore_errors=False,
1405
            ye=ye,
1406
            make_compatible=True,
1407
            add_fixed_geometry_spinup=add_fixed_geometry_spinup,
1408
            **kwargs)
1409

1410
        gdir.add_to_diagnostics('used_spinup_option', 'dynamic spinup only')
×
1411

1412
    except RuntimeError:
×
1413
        log.warning('No dynamic spinup could be conducted by using the '
×
1414
                    f'original melt factor ({melt_f}). Therefore the last '
1415
                    'try is to conduct a run until ye without a dynamic '
1416
                    'spinup.')
1417
        model_end = run_from_climate_data(
×
1418
            gdir,
1419
            add_to_log_file=False,
1420
            min_ys=yr_clim_min, ye=ye,
1421
            output_filesuffix=output_filesuffix,
1422
            climate_input_filesuffix=climate_input_filesuffix,
1423
            store_model_geometry=store_model_geometry,
1424
            store_fl_diagnostics=store_fl_diagnostics,
1425
            init_model_fls=fls_init, evolution_model=evolution_model,
1426
            fixed_geometry_spinup_yr=ys)
1427

1428
        gdir.add_to_diagnostics('used_spinup_option', 'fixed geometry spinup')
×
1429

1430
        # set all dynamic diagnostics to None if there where some successful
1431
        # runs
1432
        diag = gdir.get_diagnostics()
×
1433
        if minimise_for == 'area':
×
1434
            unit = 'km2'
×
1435
        elif minimise_for == 'volume':
×
1436
            unit = 'km3'
×
1437
        else:
1438
            raise NotImplementedError
1439
        for key in ['temp_bias_dynamic_spinup', 'dynamic_spinup_period',
×
1440
                    'dynamic_spinup_forward_model_iterations',
1441
                    f'{minimise_for}_mismatch_dynamic_spinup_{unit}_percent',
1442
                    f'reference_{minimise_for}_dynamic_spinup_{unit}',
1443
                    'dynamic_spinup_other_variable_reference',
1444
                    'dynamic_spinup_mismatch_other_variable_percent']:
1445
            if key in diag:
×
1446
                gdir.add_to_diagnostics(key, None)
×
1447

1448
        gdir.add_to_diagnostics('run_dynamic_spinup_success', False)
×
1449
    return model_end
×
1450

1451

1452
def dynamic_melt_f_run(
9✔
1453
        gdir, melt_f, yr0_ref_mb, yr1_ref_mb, fls_init, ys, ye,
1454
        output_filesuffix='', evolution_model=None,
1455
        local_variables=None, set_local_variables=False, yr_rgi=None,
1456
        **kwargs):
1457
    """
1458
    This function is one option for a 'run_function' for the
1459
    'run_dynamic_melt_f_calibration' function (the corresponding
1460
    'fallback_function' is 'dynamic_melt_f_run_fallback'). It is meant to
1461
    define a new melt_f in the given gdir and conduct a
1462
    'run_from_climate_data' run between ys and ye and return the geodetic mass
1463
    balance (units: kg m-2 yr-1) of the period yr0_ref_mb and yr1_ref_mb.
1464

1465
    Parameters
1466
    ----------
1467
    gdir : :py:class:`oggm.GlacierDirectory`
1468
        the glacier directory to process
1469
    melt_f : float
1470
        the melt_f used for this run
1471
    yr0_ref_mb : int
1472
        the start year of the geodetic mass balance
1473
    yr1_ref_mb : int
1474
        the end year of the geodetic mass balance
1475
    fls_init : []
1476
        List of flowlines to use to initialise the model
1477
    ys : int
1478
        start year of the complete run, must by smaller or equal y0_ref_mb
1479
    ye : int
1480
        end year of the complete run, must be smaller or equal y1_ref_mb
1481
    output_filesuffix : str
1482
        For the output file.
1483
        Default is ''
1484
    evolution_model : :class:oggm.core.FlowlineModel
1485
        which evolution model to use. Default: cfg.PARAMS['evolution_model']
1486
        Not all models work in all circumstances!
1487
    local_variables : None
1488
        Not needed in this function, just here to match with the function
1489
        call in run_dynamic_melt_f_calibration.
1490
    set_local_variables : bool
1491
        Not needed in this function. Only here to be confirm with the use of
1492
        this function in 'run_dynamic_melt_f_calibration'.
1493
    yr_rgi : int or None
1494
        The rgi year of the gdir.
1495
        Default is None
1496
    kwargs : dict
1497
        kwargs to pass to the evolution_model instance
1498

1499
    Returns
1500
    -------
1501
    :py:class:`oggm.core.flowline.evolution_model`, float
1502
        The final model after the run and the calculated geodetic mass balance
1503
    """
1504

1505
    evolution_model = decide_evolution_model(evolution_model)
1✔
1506

1507
    if set_local_variables:
1✔
1508
        # No local variables needed in this function
1509
        return None
1✔
1510

1511
    # Here we start with the actual model run
1512
    define_new_melt_f_in_gdir(gdir, melt_f)
1✔
1513

1514
    # conduct model run
1515
    try:
1✔
1516
        model = run_from_climate_data(gdir,
1✔
1517
                                      # force to raise an error in @entity_task
1518
                                      continue_on_error=False,
1519
                                      add_to_log_file=False,
1520
                                      ys=ys, ye=ye,
1521
                                      output_filesuffix=output_filesuffix,
1522
                                      init_model_fls=fls_init,
1523
                                      evolution_model=evolution_model,
1524
                                      **kwargs)
1525
    except RuntimeError as e:
1✔
1526
        raise RuntimeError(f'run_from_climate_data raised error! '
1✔
1527
                           f'(Message: {e})')
1528

1529
    # calculate dmdtda from previous simulation here
1530
    with utils.DisableLogger():
1✔
1531
        ds = utils.compile_run_output(gdir, input_filesuffix=output_filesuffix,
1✔
1532
                                      path=False)
1533
    dmdtda_mdl = ((ds.volume.loc[yr1_ref_mb].values -
1✔
1534
                   ds.volume.loc[yr0_ref_mb].values) /
1535
                  gdir.rgi_area_m2 /
1536
                  (yr1_ref_mb - yr0_ref_mb) *
1537
                  cfg.PARAMS['ice_density'])
1538

1539
    return model, dmdtda_mdl
1✔
1540

1541

1542
def dynamic_melt_f_run_fallback(
9✔
1543
        gdir, melt_f, fls_init, ys, ye, local_variables, output_filesuffix='',
1544
        evolution_model=None, yr_rgi=None, **kwargs):
1545
    """
1546
    This is the fallback function corresponding to the function
1547
    'dynamic_melt_f_run', which are provided to
1548
    'run_dynamic_melt_f_calibration'. It is used if the run_function fails and
1549
    if 'ignore_error=True' in 'run_dynamic_melt_f_calibration'. It sets
1550
    melt_f and conduct a run_from_climate_data run from ys to ye.
1551

1552
    Parameters
1553
    ----------
1554
    gdir : :py:class:`oggm.GlacierDirectory`
1555
        the glacier directory to process
1556
    melt_f : float
1557
        the melt_f used for this run
1558
    fls_init : []
1559
        List of flowlines to use to initialise the model
1560
    ys : int
1561
        start year of the run
1562
    ye : int
1563
        end year of the run
1564
    local_variables : dict
1565
        Not needed in this function, just here to match with the function
1566
        call in run_dynamic_melt_f_calibration.
1567
    output_filesuffix : str
1568
        For the output file.
1569
        Default is ''
1570
    evolution_model : :class:oggm.core.FlowlineModel
1571
        which evolution model to use. Default: cfg.PARAMS['evolution_model']
1572
        Not all models work in all circumstances!
1573
    yr_rgi : int or None
1574
        The rgi year of the gdir.
1575
        Default is None
1576
    kwargs : dict
1577
        kwargs to pass to the evolution_model instance
1578

1579
     Returns
1580
    -------
1581
    :py:class:`oggm.core.flowline.evolution_model`
1582
        The final model after the run.
1583
    """
1584

1585
    evolution_model = decide_evolution_model(evolution_model)
1✔
1586

1587
    define_new_melt_f_in_gdir(gdir, melt_f)
1✔
1588

1589
    # conduct model run
1590
    try:
1✔
1591
        model = run_from_climate_data(gdir,
1✔
1592
                                      # force to raise an error in @entity_task
1593
                                      continue_on_error=False,
1594
                                      add_to_log_file=False,
1595
                                      ys=ys, ye=ye,
1596
                                      output_filesuffix=output_filesuffix,
1597
                                      init_model_fls=fls_init,
1598
                                      evolution_model=evolution_model,
1599
                                      **kwargs)
1600
        gdir.add_to_diagnostics('used_spinup_option', 'no spinup')
1✔
1601
    except RuntimeError as e:
×
1602
        raise RuntimeError(f'In fallback function run_from_climate_data '
×
1603
                           f'raised error! (Message: {e})')
1604

1605
    return model
1✔
1606

1607

1608
@entity_task(log, writes=['inversion_flowlines'])
9✔
1609
def run_dynamic_melt_f_calibration(
9✔
1610
        gdir, ref_dmdtda=None, err_ref_dmdtda=None, err_dmdtda_scaling_factor=1,
1611
        ref_period='', melt_f_min=None,
1612
        melt_f_max=None, melt_f_max_step_length_minimum=0.1, maxiter=20,
1613
        ignore_errors=False, output_filesuffix='_dynamic_melt_f',
1614
        ys=None, ye=None,
1615
        run_function=dynamic_melt_f_run_with_dynamic_spinup,
1616
        kwargs_run_function=None,
1617
        fallback_function=dynamic_melt_f_run_with_dynamic_spinup_fallback,
1618
        kwargs_fallback_function=None, init_model_filesuffix=None,
1619
        init_model_yr=None, init_model_fls=None,
1620
        first_guess_diagnostic_msg='dynamic spinup only'):
1621
    """Calibrate melt_f to match a geodetic mass balance incorporating a
1622
    dynamic model run.
1623

1624
    This task iteratively search for a melt_f to match a provided geodetic
1625
    mass balance. How one model run looks like is defined in the 'run_function'.
1626
    This function should take a new melt_f guess, conducts a dynamic run and
1627
    calculate the geodetic mass balance. The goal is to match the geodetic mass
1628
    blanance 'ref_dmdtda' inside the provided error 'err_ref_dmdtda'. If the
1629
    minimisation of the mismatch between the provided and modeled geodetic mass
1630
    balance is not working the 'fallback_function' is called. In there it is
1631
    decided what run should be conducted in such a failing case. Further if
1632
    'ignore_error' is set to True and we could not find a satisfying mismatch
1633
    the best run so far is saved (if not one successful run with 'run_function'
1634
    the 'fallback_function' is called).
1635

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

1739
    Returns
1740
    -------
1741
    :py:class:`oggm.core.flowline.evolution_model`
1742
        The final dynamically spined-up model. Type depends on the selected
1743
        evolution_model.
1744
    """
1745
    # melt_f constraints
1746
    if melt_f_min is None:
2✔
1747
        melt_f_min = cfg.PARAMS['melt_f_min']
2✔
1748
    if melt_f_max is None:
2✔
1749
        melt_f_max = cfg.PARAMS['melt_f_max']
×
1750

1751
    if kwargs_run_function is None:
2✔
1752
        kwargs_run_function = {}
1✔
1753
    if kwargs_fallback_function is None:
2✔
1754
        kwargs_fallback_function = {}
1✔
1755

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

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

1789
    clim_info = gdir.get_climate_info()
2✔
1790

1791
    if ye is None:
2✔
1792
        # One adds 1 because the run ends at the end of the year
1793
        ye = clim_info['baseline_yr_1'] + 1
1✔
1794

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

1799
    if ys is None:
2✔
1800
        ys = clim_info['baseline_yr_0']
1✔
1801

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

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

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

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

1833
    # save original melt_f for later to be able to recreate original gdir
1834
    # (using the fallback function) if an error occurs
1835
    melt_f_initial = gdir.read_json('mb_calib')['melt_f']
2✔
1836

1837
    # define maximum allowed change of melt_f between two iterations. Is needed
1838
    # to avoid to large changes (=likely lead to an error). It is defined in a
1839
    # way that in maxiter steps the further away limit can be reached
1840
    melt_f_max_step_length = np.max(
2✔
1841
        [np.max(np.abs(np.array([melt_f_min, melt_f_min]) - melt_f_initial)) /
1842
         maxiter,
1843
         melt_f_max_step_length_minimum])
1844

1845
    # only used to check performance of minimisation
1846
    dynamic_melt_f_calibration_runs = [0]
2✔
1847

1848
    # this function is called if the actual dynamic melt_f calibration fails
1849
    def fallback_run(melt_f, reset, best_mismatch=None, initial_mismatch=None,
2✔
1850
                     only_first_guess=None):
1851
        if reset:
1✔
1852
            # unfortunately we could not conduct an error free run using the
1853
            # provided run_function, so we us the fallback_function
1854

1855
            # this diagnostics should be overwritten inside the fallback_function
1856
            gdir.add_to_diagnostics('used_spinup_option', 'fallback_function')
1✔
1857

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

1883
            gdir.add_to_diagnostics(
1✔
1884
                'dmdtda_mismatch_dynamic_calibration_reference',
1885
                float(ref_dmdtda))
1886
            gdir.add_to_diagnostics(
1✔
1887
                'dmdtda_dynamic_calibration_given_error',
1888
                float(err_ref_dmdtda))
1889
            gdir.add_to_diagnostics('dmdtda_dynamic_calibration_error_scaling_factor',
1✔
1890
                                    float(err_dmdtda_scaling_factor))
1891
            gdir.add_to_diagnostics(
1✔
1892
                'dmdtda_mismatch_dynamic_calibration',
1893
                float(best_mismatch))
1894
            gdir.add_to_diagnostics(
1✔
1895
                'dmdtda_mismatch_with_initial_melt_f',
1896
                float(initial_mismatch))
1897
            gdir.add_to_diagnostics('melt_f_dynamic_calibration',
1✔
1898
                                    float(melt_f))
1899
            gdir.add_to_diagnostics('melt_f_before_dynamic_calibration',
1✔
1900
                                    float(melt_f_initial))
1901
            gdir.add_to_diagnostics('run_dynamic_melt_f_calibration_iterations',
1✔
1902
                                    int(dynamic_melt_f_calibration_runs[-1]))
1903

1904
        return model
1✔
1905

1906
    # here we define the local variables which are used in the run_function,
1907
    # for some run_functions this is useful to save parameters from a previous
1908
    # run to be faster in the upcoming runs
1909
    local_variables_run_function = {}
2✔
1910
    run_function(gdir=gdir, melt_f=None, yr0_ref_mb=None, yr1_ref_mb=None,
2✔
1911
                 fls_init=None, ys=None, ye=None,
1912
                 local_variables=local_variables_run_function,
1913
                 set_local_variables=True, **kwargs_run_function)
1914

1915
    # this is the actual model run which is executed each iteration in order to
1916
    # minimise the mismatch of dmdtda of model and observation
1917
    def model_run(melt_f):
2✔
1918
        # to check performance of minimisation
1919
        dynamic_melt_f_calibration_runs.append(
2✔
1920
            dynamic_melt_f_calibration_runs[-1] + 1)
1921

1922
        model, dmdtda_mdl = run_function(gdir=gdir, melt_f=melt_f,
2✔
1923
                                         yr0_ref_mb=yr0_ref_mb,
1924
                                         yr1_ref_mb=yr1_ref_mb,
1925
                                         fls_init=fls_init, ys=ys, ye=ye,
1926
                                         output_filesuffix=output_filesuffix,
1927
                                         local_variables=local_variables_run_function,
1928
                                         **kwargs_run_function)
1929
        return model, dmdtda_mdl
2✔
1930

1931
    def cost_fct(melt_f, model_dynamic_spinup_end):
2✔
1932

1933
        # actual model run
1934
        model_dynamic_spinup, dmdtda_mdl = model_run(melt_f)
2✔
1935

1936
        # save final model for later
1937
        model_dynamic_spinup_end.append(copy.deepcopy(model_dynamic_spinup))
2✔
1938

1939
        # calculate the mismatch of dmdtda
1940
        cost = float(dmdtda_mdl - ref_dmdtda)
2✔
1941

1942
        return cost
2✔
1943

1944
    def init_cost_fun():
2✔
1945
        model_dynamic_spinup_end = []
2✔
1946

1947
        def c_fun(melt_f):
2✔
1948
            return cost_fct(melt_f, model_dynamic_spinup_end)
2✔
1949

1950
        return c_fun, model_dynamic_spinup_end
2✔
1951

1952
    # Here start with own spline minimisation algorithm
1953
    def minimise_with_spline_fit(fct_to_minimise, melt_f_guess, mismatch):
2✔
1954
        # defines limits of melt_f in accordance to maximal allowed change
1955
        # between iterations
1956
        melt_f_limits = [melt_f_initial - melt_f_max_step_length,
2✔
1957
                         melt_f_initial + melt_f_max_step_length]
1958

1959
        # this two variables indicate that the limits were already adapted to
1960
        # avoid an error
1961
        was_min_error = False
2✔
1962
        was_max_error = False
2✔
1963
        was_errors = [was_min_error, was_max_error]
2✔
1964

1965
        def get_mismatch(melt_f):
2✔
1966
            melt_f = copy.deepcopy(melt_f)
2✔
1967
            # first check if the melt_f is inside limits
1968
            if melt_f < melt_f_limits[0]:
2✔
1969
                # was the smaller limit already executed, if not first do this
1970
                if melt_f_limits[0] not in melt_f_guess:
1✔
1971
                    melt_f = copy.deepcopy(melt_f_limits[0])
1✔
1972
                else:
1973
                    # smaller limit was already used, check if it was
1974
                    # already newly defined with error
1975
                    if was_errors[0]:
×
1976
                        raise RuntimeError('Not able to minimise without '
×
1977
                                           'raising an error at lower limit of '
1978
                                           'melt_f!')
1979
                    else:
1980
                        # ok we set a new lower limit, consider also minimum
1981
                        # limit
1982
                        melt_f_limits[0] = max(melt_f_min,
×
1983
                                               melt_f_limits[0] -
1984
                                               melt_f_max_step_length)
1985
            elif melt_f > melt_f_limits[1]:
2✔
1986
                # was the larger limit already executed, if not first do this
1987
                if melt_f_limits[1] not in melt_f_guess:
1✔
1988
                    melt_f = copy.deepcopy(melt_f_limits[1])
1✔
1989
                else:
1990
                    # larger limit was already used, check if it was
1991
                    # already newly defined with ice free glacier
1992
                    if was_errors[1]:
×
1993
                        raise RuntimeError('Not able to minimise without '
×
1994
                                           'raising an error at upper limit of '
1995
                                           'melt_f!')
1996
                    else:
1997
                        # ok we set a new upper limit, consider also maximum
1998
                        # limit
1999
                        melt_f_limits[1] = min(melt_f_max,
×
2000
                                               melt_f_limits[1] +
2001
                                               melt_f_max_step_length)
2002

2003
            # now clip melt_f with limits (to be sure)
2004
            melt_f = np.clip(melt_f, melt_f_limits[0], melt_f_limits[1])
2✔
2005
            if melt_f in melt_f_guess:
2✔
2006
                raise RuntimeError('This melt_f was already tried. Probably '
×
2007
                                   'we are at one of the max or min limit and '
2008
                                   'still have no satisfactory mismatch '
2009
                                   'found!')
2010

2011
            # if error during dynamic calibration this defines how much
2012
            # melt_f is changed in the upcoming iterations to look for an
2013
            # error free run
2014
            melt_f_search_change = melt_f_max_step_length / 10
2✔
2015
            # maximum number of changes to look for an error free run
2016
            max_iterations = int(melt_f_max_step_length /
2✔
2017
                                 melt_f_search_change)
2018

2019
            current_min_error = False
2✔
2020
            current_max_error = False
2✔
2021
            doing_first_guess = (len(mismatch) == 0)
2✔
2022
            iteration = 0
2✔
2023
            current_melt_f = copy.deepcopy(melt_f)
2✔
2024

2025
            # in this loop if an error at the limits is raised we go step by
2026
            # step away from the limits until we are at the initial guess or we
2027
            # found an error free run
2028
            tmp_mismatch = None
2✔
2029
            while ((current_min_error | current_max_error | iteration == 0) &
2✔
2030
                   (iteration < max_iterations)):
2031
                try:
2✔
2032
                    tmp_mismatch = fct_to_minimise(melt_f)
2✔
2033
                except RuntimeError as e:
1✔
2034
                    # check if we are at the lower limit
2035
                    if melt_f == melt_f_limits[0]:
1✔
2036
                        # check if there was already an error at the lower limit
2037
                        if was_errors[0]:
×
2038
                            raise RuntimeError('Second time with error at '
×
2039
                                               'lower limit of melt_f! '
2040
                                               'Error message of model run: '
2041
                                               f'{e}')
2042
                        else:
2043
                            was_errors[0] = True
×
2044
                            current_min_error = True
×
2045

2046
                    # check if we are at the upperlimit
2047
                    elif melt_f == melt_f_limits[1]:
1✔
2048
                        # check if there was already an error at the lower limit
2049
                        if was_errors[1]:
1✔
2050
                            raise RuntimeError('Second time with error at '
×
2051
                                               'upper limit of melt_f! '
2052
                                               'Error message of model run: '
2053
                                               f'{e}')
2054
                        else:
2055
                            was_errors[1] = True
1✔
2056
                            current_max_error = True
1✔
2057

2058
                    if current_min_error:
1✔
2059
                        # currently we searching for a new lower limit with no
2060
                        # error
2061
                        melt_f = np.round(melt_f + melt_f_search_change,
×
2062
                                          decimals=1)
2063
                    elif current_max_error:
1✔
2064
                        # currently we searching for a new upper limit with no
2065
                        # error
2066
                        melt_f = np.round(melt_f - melt_f_search_change,
1✔
2067
                                          decimals=1)
2068

2069
                    # if we end close to an already executed guess while
2070
                    # searching for a new limit we quite
2071
                    if np.isclose(melt_f, melt_f_guess).any():
1✔
2072
                        raise RuntimeError('Not able to further minimise, '
×
2073
                                           'return the best we have so far!'
2074
                                           f'Error message: {e}')
2075

2076
                    if doing_first_guess:
1✔
2077
                        # unfortunately first guess is not working
2078
                        raise RuntimeError('Dynamic calibration is not working '
1✔
2079
                                           'with first guess! Error '
2080
                                           f'message: {e}')
2081

2082
                    if np.isclose(melt_f, current_melt_f):
1✔
2083
                        # something unexpected happen so we end here
2084
                        raise RuntimeError('Unexpected error not at the limits'
×
2085
                                           f' of melt_f. Error Message: {e}')
2086

2087
                iteration += 1
2✔
2088

2089
            if iteration >= max_iterations:
2✔
2090
                # ok we were not able to find an mismatch without error
2091
                if current_min_error:
×
2092
                    raise RuntimeError('Not able to find new lower limit for '
×
2093
                                       'melt_f!')
2094
                elif current_max_error:
×
2095
                    raise RuntimeError('Not able to find new upper limit for '
×
2096
                                       'melt_f!')
2097
                else:
2098
                    raise RuntimeError('Something unexpected happened during '
×
2099
                                       'definition of new melt_f limits!')
2100
            else:
2101
                # if we found a new limit set it
2102
                if current_min_error:
2✔
2103
                    melt_f_limits[0] = copy.deepcopy(melt_f)
×
2104
                elif current_max_error:
2✔
2105
                    melt_f_limits[1] = copy.deepcopy(melt_f)
1✔
2106

2107
            if tmp_mismatch is None:
2✔
2108
                raise RuntimeError('Not able to find a new mismatch for '
1✔
2109
                                   'dmdtda!')
2110

2111
            return float(tmp_mismatch), float(melt_f)
2✔
2112

2113
        # first guess
2114
        new_mismatch, new_melt_f = get_mismatch(melt_f_initial)
2✔
2115
        melt_f_guess.append(new_melt_f)
2✔
2116
        mismatch.append(new_mismatch)
2✔
2117

2118
        if abs(mismatch[-1]) < err_ref_dmdtda:
2✔
2119
            return mismatch[-1], new_melt_f
2✔
2120

2121
        # second (arbitrary) guess is given depending on the outcome of first
2122
        # guess, melt_f is changed for percent of mismatch relative to
2123
        # err_ref_dmdtda times melt_f_max_step_length (if
2124
        # mismatch = 2 * err_ref_dmdtda this corresponds to 100%; for 100% or
2125
        # 150% the next step is (-1) * melt_f_max_step_length; if mismatch
2126
        # -40%, next step is 0.4 * melt_f_max_step_length; but always at least
2127
        # an absolute change of 0.02 is imposed to prevent too close guesses).
2128
        # (-1) as if mismatch is negative we need a larger melt_f to get closer
2129
        # to 0.
2130
        step = (-1) * np.sign(mismatch[-1]) * \
1✔
2131
            max((np.abs(mismatch[-1]) - err_ref_dmdtda) / err_ref_dmdtda *
2132
                melt_f_max_step_length, 0.02)
2133
        new_mismatch, new_melt_f = get_mismatch(melt_f_guess[0] + step)
1✔
2134
        melt_f_guess.append(new_melt_f)
1✔
2135
        mismatch.append(new_mismatch)
1✔
2136

2137
        if abs(mismatch[-1]) < err_ref_dmdtda:
1✔
2138
            return mismatch[-1], new_melt_f
×
2139

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

2167
            if abs(mismatch[-1]) < err_ref_dmdtda:
1✔
2168
                return mismatch[-1], new_melt_f
1✔
2169

2170
        # Ok when we end here the spinup could not find satisfying match after
2171
        # maxiter(ations)
2172
        raise RuntimeError(f'Could not find mismatch smaller '
1✔
2173
                           f'{err_ref_dmdtda} kg m-2 yr-1 (only '
2174
                           f'{np.min(np.abs(mismatch))} kg m-2 yr-1) in '
2175
                           f'{maxiter} Iterations!')
2176

2177
    # wrapper to get values for intermediate (mismatch, melt_f) guesses if an
2178
    # error is raised
2179
    def init_minimiser():
2✔
2180
        melt_f_guess = []
2✔
2181
        mismatch = []
2✔
2182

2183
        def minimiser(fct_to_minimise):
2✔
2184
            return minimise_with_spline_fit(fct_to_minimise, melt_f_guess,
2✔
2185
                                            mismatch)
2186

2187
        return minimiser, melt_f_guess, mismatch
2✔
2188

2189
    # define function for the actual minimisation
2190
    c_fun, models_dynamic_spinup_end = init_cost_fun()
2✔
2191

2192
    # define minimiser
2193
    minimise_given_fct, melt_f_guesses, mismatch_dmdtda = init_minimiser()
2✔
2194

2195
    try:
2✔
2196
        final_mismatch, final_melt_f = minimise_given_fct(c_fun)
2✔
2197
    except RuntimeError as e:
1✔
2198
        # something happened during minimisation, if there where some
2199
        # successful runs we return the one with the best mismatch, otherwise
2200
        # we conduct just a run with no dynamic spinup
2201
        if len(mismatch_dmdtda) == 0:
1✔
2202
            # we conducted no successful run, so run without dynamic spinup
2203
            if ignore_errors:
1✔
2204
                log.info('Dynamic melt_f calibration not successful. '
1✔
2205
                         f'Error message: {e}')
2206
                model_return = fallback_run(melt_f=melt_f_initial,
1✔
2207
                                            reset=True)
2208
                return model_return
1✔
2209
            else:
2210
                raise RuntimeError('Dynamic melt_f calibration was not '
×
2211
                                   f'successful! Error Message: {e}')
2212
        else:
2213
            if ignore_errors:
1✔
2214
                log.info('Dynamic melt_f calibration not successful. Error '
1✔
2215
                         f'message: {e}')
2216

2217
                # there where some successful runs so we return the one with the
2218
                # smallest mismatch of dmdtda
2219
                min_mismatch_index = np.argmin(np.abs(mismatch_dmdtda))
1✔
2220
                melt_f_best = np.array(melt_f_guesses)[min_mismatch_index]
1✔
2221

2222
                # check if the first guess was the best guess
2223
                only_first_guess = False
1✔
2224
                if min_mismatch_index == 1:
1✔
2225
                    only_first_guess = True
×
2226

2227
                model_return = fallback_run(
1✔
2228
                    melt_f=melt_f_best, reset=False,
2229
                    best_mismatch=np.array(mismatch_dmdtda)[min_mismatch_index],
2230
                    initial_mismatch=mismatch_dmdtda[0],
2231
                    only_first_guess=only_first_guess)
2232

2233
                return model_return
1✔
2234
            else:
2235
                raise RuntimeError('Dynamic melt_f calibration not successful. '
1✔
2236
                                   f'Error message: {e}')
2237

2238
    # check that new melt_f is correctly saved in gdir
2239
    assert final_melt_f == gdir.read_json('mb_calib')['melt_f']
2✔
2240

2241
    # hurray, dynamic melt_f calibration successful
2242
    gdir.add_to_diagnostics('used_spinup_option',
2✔
2243
                            'dynamic melt_f calibration (full success)')
2244
    gdir.add_to_diagnostics('dmdtda_mismatch_dynamic_calibration_reference',
2✔
2245
                            float(ref_dmdtda))
2246
    gdir.add_to_diagnostics('dmdtda_dynamic_calibration_given_error',
2✔
2247
                            float(err_ref_dmdtda))
2248
    gdir.add_to_diagnostics('dmdtda_dynamic_calibration_error_scaling_factor',
2✔
2249
                            float(err_dmdtda_scaling_factor))
2250
    gdir.add_to_diagnostics('dmdtda_mismatch_dynamic_calibration',
2✔
2251
                            float(final_mismatch))
2252
    gdir.add_to_diagnostics('dmdtda_mismatch_with_initial_melt_f',
2✔
2253
                            float(mismatch_dmdtda[0]))
2254
    gdir.add_to_diagnostics('melt_f_dynamic_calibration', float(final_melt_f))
2✔
2255
    gdir.add_to_diagnostics('melt_f_before_dynamic_calibration',
2✔
2256
                            float(melt_f_initial))
2257
    gdir.add_to_diagnostics('run_dynamic_melt_f_calibration_iterations',
2✔
2258
                            int(dynamic_melt_f_calibration_runs[-1]))
2259

2260
    log.info(f'Dynamic melt_f calibration worked for {gdir.rgi_id}!')
2✔
2261

2262
    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