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

OGGM / oggm / 5975837719

25 Aug 2023 12:24PM UTC coverage: 85.547% (+0.05%) from 85.502%
5975837719

push

github

web-flow
Tiny edit on index page regarding youtube (#1628)

* Update index.rst

* Update index.rst

11441 of 13374 relevant lines covered (85.55%)

3.76 hits per line

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

80.62
/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
                       target_yr=None, target_value=None,
38
                       minimise_for='area', precision_percent=1,
39
                       precision_absolute=1, min_ice_thickness=None,
40
                       first_guess_t_bias=-2, t_bias_max_step_length=2,
41
                       maxiter=30, output_filesuffix='_dynamic_spinup',
42
                       store_model_geometry=True, store_fl_diagnostics=None,
43
                       store_model_evolution=True, ignore_errors=False,
44
                       return_t_bias_best=False, ye=None,
45
                       model_flowline_filesuffix='',
46
                       add_fixed_geometry_spinup=False, **kwargs):
47
    """Dynamically spinup the glacier to match area or volume at the RGI date.
48

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

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

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

218
    if ye < target_yr:
2✔
219
        raise RuntimeError(f'The provided end year (ye = {ye}) must be larger'
1✔
220
                           f'than the target year (target_yr = {target_yr}!')
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 (target_yr - spinup_start_yr_max) > min_spinup_period:
2✔
241
            min_spinup_period = (target_yr - 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 target_yr
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',
1✔
278
                                         filesuffix=output_filesuffix,
279
                                         delete=True)
280
    else:
281
        fl_diag_path = False
1✔
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 target_yr < 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(target_yr, 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

331
        return model_dynamic_spinup_end
1✔
332

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

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

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

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

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

393
    # the actual spinup run
394
    def run_model_with_spinup_to_target_year(t_bias):
2✔
395
        forward_model_runs.append(forward_model_runs[-1] + 1)
2✔
396

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

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

411
        # Now conduct the actual model run to the rgi date
412
        model_historical = evolution_model(model_spinup.fls,
2✔
413
                                           mb_model_historical,
414
                                           y0=yr_spinup,
415
                                           **kwargs)
416
        if store_model_evolution:
2✔
417
            # check if we need to add the min_h variable (done inplace)
418
            delete_area_min_h = False
2✔
419
            ovars = cfg.PARAMS['store_diagnostic_variables']
2✔
420
            if 'area_min_h' not in ovars:
2✔
421
                ovars += ['area_min_h']
2✔
422
                delete_area_min_h = True
2✔
423

424
            ds = model_historical.run_until_and_store(
2✔
425
                ye,
426
                geom_path=geom_path,
427
                diag_path=diag_path,
428
                fl_diag_path=fl_diag_path,
429
                dynamic_spinup_min_ice_thick=min_ice_thickness,
430
                fixed_geometry_spinup_yr=fixed_geometry_spinup_yr)
431

432
            # now we delete the min_h variable again if it was not
433
            # included before (inplace)
434
            if delete_area_min_h:
2✔
435
                ovars.remove('area_min_h')
2✔
436

437
            if type(ds) == tuple:
2✔
438
                ds = ds[0]
2✔
439
            model_area_km2 = ds.area_m2_min_h.loc[target_yr].values * 1e-6
2✔
440
            model_volume_km3 = ds.volume_m3.loc[target_yr].values * 1e-9
2✔
441
        else:
442
            # only run to rgi date and extract values
443
            model_historical.run_until(target_yr)
1✔
444
            fls = model_historical.fls
1✔
445
            model_area_km2 = np.sum(
1✔
446
                [np.sum(fl.bin_area_m2[fl.thick > min_ice_thickness])
447
                 for fl in fls]) * 1e-6
448
            model_volume_km3 = model_historical.volume_km3
1✔
449
            # afterwards finish the complete run
450
            model_historical.run_until(ye)
1✔
451

452
        if cost_var == 'area_km2':
2✔
453
            return model_area_km2, model_volume_km3, model_historical, ice_free
2✔
454
        elif cost_var == 'volume_km3':
1✔
455
            return model_volume_km3, model_area_km2, model_historical, ice_free
1✔
456
        else:
457
            raise NotImplementedError(f'{cost_var}')
458

459
    def cost_fct(t_bias, model_dynamic_spinup_end_loc, other_variable_mismatch_loc):
2✔
460
        # actual model run
461
        model_value, other_value, model_dynamic_spinup, ice_free = \
2✔
462
            run_model_with_spinup_to_target_year(t_bias)
463

464
        # save the final model for later
465
        model_dynamic_spinup_end_loc.append(copy.deepcopy(model_dynamic_spinup))
2✔
466

467
        # calculate the mismatch in percent
468
        cost = (model_value - reference_value) / reference_value * 100
2✔
469
        other_variable_mismatch_loc.append(
2✔
470
            (other_value - other_reference_value) / other_reference_value * 100)
471

472
        return cost, ice_free
2✔
473

474
    def init_cost_fct():
2✔
475
        model_dynamic_spinup_end_loc = []
2✔
476
        other_variable_mismatch_loc = []
2✔
477

478
        def c_fct(t_bias):
2✔
479
            return cost_fct(t_bias, model_dynamic_spinup_end_loc,
2✔
480
                            other_variable_mismatch_loc)
481

482
        return c_fct, model_dynamic_spinup_end_loc, other_variable_mismatch_loc
2✔
483

484
    def minimise_with_spline_fit(fct_to_minimise):
2✔
485
        # defines limits of t_bias in accordance to maximal allowed change
486
        # between iterations
487
        t_bias_limits = [first_guess_t_bias - t_bias_max_step_length,
2✔
488
                         first_guess_t_bias + t_bias_max_step_length]
489
        t_bias_guess = []
2✔
490
        mismatch = []
2✔
491
        # this two variables indicate that the limits were already adapted to
492
        # avoid an ice_free or out_of_domain error
493
        was_ice_free = False
2✔
494
        was_out_of_domain = False
2✔
495
        was_errors = [was_out_of_domain, was_ice_free]
2✔
496

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

533
            # now clip t_bias with limits
534
            t_bias = np.clip(t_bias, t_bias_limits[0], t_bias_limits[1])
2✔
535

536
            # now start with mismatch calculation
537

538
            # if error during spinup (ice_free or out_of_domain) this defines
539
            # how much t_bias is changed to look for an error free glacier spinup
540
            t_bias_search_change = t_bias_max_step_length / 10
2✔
541
            # maximum number of changes to look for an error free glacier
542
            max_iterations = int(t_bias_max_step_length / t_bias_search_change)
2✔
543
            is_out_of_domain = True
2✔
544
            is_ice_free_spinup = True
2✔
545
            is_ice_free_end = True
2✔
546
            is_first_guess_ice_free = False
2✔
547
            is_first_guess_out_of_domain = False
2✔
548
            doing_first_guess = (len(mismatch) == 0)
2✔
549
            define_new_lower_limit = False
2✔
550
            define_new_upper_limit = False
2✔
551
            iteration = 0
2✔
552

553
            while ((is_out_of_domain | is_ice_free_spinup | is_ice_free_end) &
2✔
554
                   (iteration < max_iterations)):
555
                try:
2✔
556
                    tmp_mismatch, is_ice_free_spinup = fct_to_minimise(t_bias)
2✔
557

558
                    # no error occurred, so we are not outside the domain
559
                    is_out_of_domain = False
2✔
560

561
                    # check if we are ice_free after spinup, if so we search
562
                    # for a new upper limit for t_bias
563
                    if is_ice_free_spinup:
2✔
564
                        was_errors[1] = True
1✔
565
                        define_new_upper_limit = True
1✔
566
                        # special treatment if it is the first guess
567
                        if np.isclose(t_bias, first_guess_t_bias) & \
1✔
568
                                doing_first_guess:
569
                            is_first_guess_ice_free = True
1✔
570
                            # here directly jump to the smaller limit
571
                            t_bias = copy.deepcopy(t_bias_limits[0])
1✔
572
                        elif is_first_guess_ice_free & doing_first_guess:
1✔
573
                            # make large steps if it is first guess
574
                            t_bias = t_bias - t_bias_max_step_length
1✔
575
                        else:
576
                            t_bias = np.round(t_bias - t_bias_search_change,
×
577
                                              decimals=1)
578
                        if np.isclose(t_bias, t_bias_guess).any():
1✔
579
                            iteration = copy.deepcopy(max_iterations)
×
580

581
                    # check if we are ice_free at the end of the model run, if
582
                    # so we use the lower t_bias limit and change the limit if
583
                    # needed
584
                    elif np.isclose(tmp_mismatch, -100.):
2✔
585
                        is_ice_free_end = True
×
586
                        was_errors[1] = True
×
587
                        define_new_upper_limit = True
×
588
                        # special treatment if it is the first guess
589
                        if np.isclose(t_bias, first_guess_t_bias) & \
×
590
                                doing_first_guess:
591
                            is_first_guess_ice_free = True
×
592
                            # here directly jump to the smaller limit
593
                            t_bias = copy.deepcopy(t_bias_limits[0])
×
594
                        elif is_first_guess_ice_free & doing_first_guess:
×
595
                            # make large steps if it is first guess
596
                            t_bias = t_bias - t_bias_max_step_length
×
597
                        else:
598
                            # if lower limit was already used change it and use
599
                            if t_bias == t_bias_limits[0]:
×
600
                                t_bias_limits[0] = t_bias_limits[0] - \
×
601
                                    t_bias_max_step_length
602
                                t_bias = copy.deepcopy(t_bias_limits[0])
×
603
                            else:
604
                                # otherwise just try with a colder t_bias
605
                                t_bias = np.round(t_bias - t_bias_search_change,
×
606
                                                  decimals=1)
607

608
                    else:
609
                        is_ice_free_end = False
2✔
610

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

634
                    else:
635
                        # otherwise this error can not be handled here
636
                        raise RuntimeError(e)
×
637

638
                iteration += 1
2✔
639

640
            if iteration >= max_iterations:
2✔
641
                # ok we were not able to find an mismatch without error
642
                # (ice_free or out of domain), so we try to raise an descriptive
643
                # RuntimeError
644
                if len(mismatch) == 0:
1✔
645
                    # unfortunately we were not able conduct one single error
646
                    # free run
647
                    msg = 'Not able to conduct one error free run. Error is '
1✔
648
                    if is_first_guess_ice_free:
1✔
649
                        msg += f'"ice_free" with last t_bias of {t_bias}.'
1✔
650
                    elif is_first_guess_out_of_domain:
1✔
651
                        msg += f'"out_of_domain" with last t_bias of {t_bias}.'
1✔
652
                    else:
653
                        raise RuntimeError('Something unexpected happened!')
×
654

655
                    raise RuntimeError(msg)
1✔
656

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

711
            return float(tmp_mismatch), float(t_bias)
2✔
712

713
        # first guess
714
        new_mismatch, new_t_bias = get_mismatch(first_guess_t_bias)
2✔
715
        t_bias_guess.append(new_t_bias)
2✔
716
        mismatch.append(new_mismatch)
2✔
717

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

721
        # second (arbitrary) guess is given depending on the outcome of first
722
        # guess, when mismatch is 100% t_bias is changed for
723
        # t_bias_max_step_length, but at least the second guess is 0.2 °C away
724
        # from the first guess
725
        step = np.sign(mismatch[-1]) * max(np.abs(mismatch[-1]) *
2✔
726
                                           t_bias_max_step_length / 100,
727
                                           0.2)
728
        new_mismatch, new_t_bias = get_mismatch(t_bias_guess[0] + step)
2✔
729
        t_bias_guess.append(new_t_bias)
2✔
730
        mismatch.append(new_mismatch)
2✔
731

732
        if abs(mismatch[-1]) < precision_percent:
2✔
733
            return t_bias_guess, mismatch
2✔
734

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

768
            if abs(mismatch[-1]) < precision_percent:
2✔
769
                return t_bias_guess, mismatch
2✔
770

771
        # Ok when we end here the spinup could not find satisfying match after
772
        # maxiter(ations)
773
        raise RuntimeError(f'Could not find mismatch smaller '
1✔
774
                           f'{precision_percent}% (only '
775
                           f'{np.min(np.abs(mismatch))}%) in {maxiter}'
776
                           f'Iterations!')
777

778
    # define function for the actual minimisation
779
    c_fun, model_dynamic_spinup_end, other_variable_mismatch = init_cost_fct()
2✔
780

781
    # define the MassBalanceModels for different spinup periods and try to
782
    # minimise, if minimisation fails a shorter spinup period is used
783
    # (first a spinup period between initial period and 'min_spinup_period'
784
    # years and the second try is to use a period of 'min_spinup_period' years,
785
    # if it still fails the actual error is raised)
786
    if spinup_start_yr is not None:
2✔
787
        spinup_period_initial = min(target_yr - spinup_start_yr,
2✔
788
                                    target_yr - yr_min)
789
    else:
790
        spinup_period_initial = min(spinup_period, target_yr - yr_min)
1✔
791
    if spinup_period_initial <= min_spinup_period:
2✔
792
        spinup_periods_to_try = [min_spinup_period]
1✔
793
    else:
794
        # try out a maximum of three different spinup_periods
795
        spinup_periods_to_try = [spinup_period_initial,
2✔
796
                                 int((spinup_period_initial +
797
                                      min_spinup_period) / 2),
798
                                 min_spinup_period]
799
    # after defining the initial spinup period we can define the year for the
800
    # fixed_geometry_spinup
801
    if add_fixed_geometry_spinup:
2✔
802
        fixed_geometry_spinup_yr = target_yr - spinup_period_initial
2✔
803
    else:
804
        fixed_geometry_spinup_yr = None
1✔
805

806
    # check if the user provided an mb_model_spinup, otherwise we must define a
807
    # new one each iteration
808
    provided_mb_model_spinup = False
2✔
809
    if mb_model_spinup is not None:
2✔
810
        provided_mb_model_spinup = True
×
811

812
    for spinup_period in spinup_periods_to_try:
2✔
813
        yr_spinup = target_yr - spinup_period
2✔
814

815
        if not provided_mb_model_spinup:
2✔
816
            # define spinup MassBalance
817
            # spinup is running for 'target_yr - yr_spinup' years, using a
818
            # ConstantMassBalance
819
            y0_spinup = (yr_spinup + target_yr) / 2
2✔
820
            halfsize_spinup = target_yr - y0_spinup
2✔
821
            mb_model_spinup = MultipleFlowlineMassBalance(
2✔
822
                gdir, mb_model_class=ConstantMassBalance,
823
                filename='climate_historical',
824
                input_filesuffix=climate_input_filesuffix, y0=y0_spinup,
825
                halfsize=halfsize_spinup)
826

827
        # try to conduct minimisation, if an error occurred try shorter spinup
828
        # period
829
        try:
2✔
830
            final_t_bias_guess, final_mismatch = minimise_with_spline_fit(c_fun)
2✔
831
            # ok no error occurred so we succeeded
832
            break
2✔
833
        except RuntimeError as e:
1✔
834
            # if the last spinup period was min_spinup_period the dynamic
835
            # spinup failed
836
            if spinup_period == min_spinup_period:
1✔
837
                log.warning('No dynamic spinup could be conducted and the '
1✔
838
                            'original model with no spinup is saved using the '
839
                            f'provided output_filesuffix "{output_filesuffix}". '
840
                            f'The error message of the dynamic spinup is: {e}')
841
                if ignore_errors:
1✔
842
                    model_dynamic_spinup_end = save_model_without_dynamic_spinup()
1✔
843
                    if return_t_bias_best:
1✔
844
                        return model_dynamic_spinup_end, np.nan
×
845
                    else:
846
                        return model_dynamic_spinup_end
1✔
847
                else:
848
                    # delete all files which could be saved during the previous
849
                    # iterations
850
                    if geom_path and os.path.exists(geom_path):
1✔
851
                        os.remove(geom_path)
1✔
852

853
                    if fl_diag_path and os.path.exists(fl_diag_path):
1✔
854
                        os.remove(fl_diag_path)
×
855

856
                    if diag_path and os.path.exists(diag_path):
1✔
857
                        os.remove(diag_path)
1✔
858

859
                    raise RuntimeError(e)
1✔
860

861
    # hurray, dynamic spinup successfully
862
    gdir.add_to_diagnostics('run_dynamic_spinup_success', True)
2✔
863

864
    # also save some other stuff
865
    gdir.add_to_diagnostics('temp_bias_dynamic_spinup',
2✔
866
                            float(final_t_bias_guess[-1]))
867
    gdir.add_to_diagnostics('dynamic_spinup_target_year',
2✔
868
                            int(target_yr))
869
    gdir.add_to_diagnostics('dynamic_spinup_period',
2✔
870
                            int(spinup_period))
871
    gdir.add_to_diagnostics('dynamic_spinup_forward_model_iterations',
2✔
872
                            int(forward_model_runs[-1]))
873
    gdir.add_to_diagnostics(f'{minimise_for}_mismatch_dynamic_spinup_{unit}_'
2✔
874
                            f'percent',
875
                            float(final_mismatch[-1]))
876
    gdir.add_to_diagnostics(f'reference_{minimise_for}_dynamic_spinup_{unit}',
2✔
877
                            float(reference_value))
878
    gdir.add_to_diagnostics('dynamic_spinup_other_variable_reference',
2✔
879
                            float(other_reference_value))
880
    gdir.add_to_diagnostics('dynamic_spinup_mismatch_other_variable_percent',
2✔
881
                            float(other_variable_mismatch[-1]))
882

883
    # here only save the final model state if store_model_evolution = False
884
    if not store_model_evolution:
2✔
885
        with warnings.catch_warnings():
1✔
886
            # For operational runs we ignore the warnings
887
            warnings.filterwarnings('ignore', category=RuntimeWarning)
1✔
888
            model_dynamic_spinup_end[-1].run_until_and_store(
1✔
889
                target_yr,
890
                geom_path=geom_path,
891
                diag_path=diag_path,
892
                fl_diag_path=fl_diag_path, )
893

894
    if return_t_bias_best:
2✔
895
        return model_dynamic_spinup_end[-1], final_t_bias_guess[-1]
2✔
896
    else:
897
        return model_dynamic_spinup_end[-1]
1✔
898

899

900
def define_new_melt_f_in_gdir(gdir, new_melt_f):
9✔
901
    """
902
    Helper function to define a new melt_f in a gdir. Is used inside the run
903
    functions of the dynamic melt_f calibration.
904

905
    Parameters
906
    ----------
907
    gdir : :py:class:`oggm.GlacierDirectory`
908
        the glacier directory to change the melt_f
909
    new_melt_f : float
910
        the new melt_f to save in the gdir
911

912
    Returns
913
    -------
914

915
    """
916
    try:
2✔
917
        df = gdir.read_json('mb_calib')
2✔
918
    except FileNotFoundError:
×
919
        raise InvalidWorkflowError('`mb_calib.json` does not exist in gdir. '
×
920
                                   'You first need to calibrate the whole '
921
                                   'MassBalanceModel before changing melt_f '
922
                                   'alone!')
923
    df['melt_f'] = new_melt_f
2✔
924
    gdir.write_json(df, 'mb_calib')
2✔
925

926

927
def dynamic_melt_f_run_with_dynamic_spinup(
9✔
928
        gdir, melt_f, yr0_ref_mb, yr1_ref_mb, fls_init, ys, ye,
929
        output_filesuffix='', evolution_model=None,
930
        mb_model_historical=None, mb_model_spinup=None,
931
        minimise_for='area', climate_input_filesuffix='', spinup_period=20,
932
        min_spinup_period=10, target_yr=None, precision_percent=1,
933
        precision_absolute=1, min_ice_thickness=None,
934
        first_guess_t_bias=-2, t_bias_max_step_length=2, maxiter=30,
935
        store_model_geometry=True, store_fl_diagnostics=None,
936
        local_variables=None, set_local_variables=False, do_inversion=True,
937
        spinup_start_yr_max=None, add_fixed_geometry_spinup=True,
938
        **kwargs):
939
    """
940
    This function is one option for a 'run_function' for the
941
    'run_dynamic_melt_f_calibration' function (the corresponding
942
    'fallback_function' is
943
    'dynamic_melt_f_run_with_dynamic_spinup_fallback'). This
944
    function defines a new melt_f in the glacier directory and conducts an
945
    inversion calibrating A to match '_vol_m3_ref' with this new melt_f
946
    ('calibrate_inversion_from_consensus'). Afterwards a dynamic spinup is
947
    conducted to match 'minimise_for' (for more info look at docstring of
948
    'run_dynamic_spinup'). And in the end the geodetic mass balance of the
949
    current run is calculated (between the period [yr0_ref_mb, yr1_ref_mb]) and
950
    returned.
951

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

1080
    Returns
1081
    -------
1082
    :py:class:`oggm.core.flowline.evolution_model`, float
1083
        The final model after the run and the calculated geodetic mass balance
1084
    """
1085

1086
    evolution_model = decide_evolution_model(evolution_model)
2✔
1087

1088
    if not isinstance(local_variables, dict):
2✔
1089
        raise ValueError('You must provide a dict for local_variables!')
1✔
1090

1091
    from oggm.workflow import calibrate_inversion_from_consensus
2✔
1092

1093
    if set_local_variables:
2✔
1094
        # clear the provided dictionary and set the first elements
1095
        local_variables.clear()
2✔
1096
        local_variables['t_bias'] = [first_guess_t_bias]
2✔
1097
        # ATTENTION: it is assumed that the flowlines in gdir have the volume
1098
        # we want to match during calibrate_inversion_from_consensus when we
1099
        # set_local_variables
1100
        fls_ref = gdir.read_pickle('model_flowlines')
2✔
1101
        local_variables['vol_m3_ref'] = np.sum([f.volume_m3 for f in fls_ref])
2✔
1102

1103
        # we are done with preparing the local_variables for the upcoming iterations
1104
        return None
2✔
1105

1106
    if target_yr is None:
2✔
1107
        target_yr = gdir.rgi_date + 1  # + 1 converted to hydro years
×
1108
    if min_spinup_period > target_yr - ys:
2✔
1109
        log.info('The target year is closer to ys as the minimum spinup '
×
1110
                 'period -> therefore the minimum spinup period is '
1111
                 'adapted and it is the only period which is tried by the '
1112
                 'dynamic spinup function!')
1113
        min_spinup_period = target_yr - ys
×
1114
        spinup_period = target_yr - ys
×
1115

1116
    if spinup_start_yr_max is None:
2✔
1117
        spinup_start_yr_max = yr0_ref_mb
2✔
1118

1119
    if spinup_start_yr_max > yr0_ref_mb:
2✔
1120
        log.info('The provided maximum start year is larger then the '
×
1121
                 'start year of the geodetic period, therefore it will be '
1122
                 'set to the start year of the geodetic period!')
1123
        spinup_start_yr_max = yr0_ref_mb
×
1124

1125
    # check that inversion is only possible without providing own fls
1126
    if do_inversion:
2✔
1127
        if not np.all([np.all(getattr(fl_prov, 'surface_h') ==
2✔
1128
                              getattr(fl_orig, 'surface_h')) and
1129
                       np.all(getattr(fl_prov, 'bed_h') ==
1130
                              getattr(fl_orig, 'bed_h'))
1131
                       for fl_prov, fl_orig in
1132
                       zip(fls_init, gdir.read_pickle('model_flowlines'))]):
1133
            raise InvalidWorkflowError('If you want to perform a dynamic '
×
1134
                                       'melt_f calibration including an '
1135
                                       'inversion, it is not possible to '
1136
                                       'provide your own flowlines! (fls_init '
1137
                                       'should be None or '
1138
                                       'the original model_flowlines)')
1139

1140
    # Here we start with the actual model run
1141
    if melt_f == gdir.read_json('mb_calib')['melt_f']:
2✔
1142
        # we do not need to define a new melt_f or do an inversion
1143
        do_inversion = False
2✔
1144
    else:
1145
        define_new_melt_f_in_gdir(gdir, melt_f)
2✔
1146

1147
    if do_inversion:
2✔
1148
        with utils.DisableLogger():
2✔
1149
            apparent_mb_from_any_mb(gdir,
2✔
1150
                                    add_to_log_file=False,  # dont write to log
1151
                                    )
1152
            # do inversion with A calibration to current volume
1153
            calibrate_inversion_from_consensus(
2✔
1154
                [gdir], apply_fs_on_mismatch=True, error_on_mismatch=False,
1155
                filter_inversion_output=True,
1156
                volume_m3_reference=local_variables['vol_m3_ref'],
1157
                add_to_log_file=False)
1158

1159
    # this is used to keep the original model_flowline unchanged (-> to be able
1160
    # to conduct different dynamic calibration runs in the same gdir)
1161
    model_flowline_filesuffix = '_dyn_melt_f_calib'
2✔
1162
    init_present_time_glacier(gdir, filesuffix=model_flowline_filesuffix,
2✔
1163
                              add_to_log_file=False)
1164

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

1205
    # calculate dmdtda from previous simulation here
1206
    with utils.DisableLogger():
2✔
1207
        ds = utils.compile_run_output(gdir, input_filesuffix=output_filesuffix,
2✔
1208
                                      path=False)
1209
    dmdtda_mdl = ((ds.volume.loc[yr1_ref_mb].values -
2✔
1210
                   ds.volume.loc[yr0_ref_mb].values) /
1211
                  gdir.rgi_area_m2 /
1212
                  (yr1_ref_mb - yr0_ref_mb) *
1213
                  cfg.PARAMS['ice_density'])
1214

1215
    return model, dmdtda_mdl
2✔
1216

1217

1218
def dynamic_melt_f_run_with_dynamic_spinup_fallback(
9✔
1219
        gdir, melt_f, fls_init, ys, ye, local_variables, output_filesuffix='',
1220
        evolution_model=None, minimise_for='area',
1221
        mb_model_historical=None, mb_model_spinup=None,
1222
        climate_input_filesuffix='', spinup_period=20, min_spinup_period=10,
1223
        target_yr=None, precision_percent=1,
1224
        precision_absolute=1, min_ice_thickness=10,
1225
        first_guess_t_bias=-2, t_bias_max_step_length=2, maxiter=30,
1226
        store_model_geometry=True, store_fl_diagnostics=None,
1227
        do_inversion=True, spinup_start_yr_max=None,
1228
        add_fixed_geometry_spinup=True, **kwargs):
1229
    """
1230
    This is the fallback function corresponding to the function
1231
    'dynamic_melt_f_run_with_dynamic_spinup', which are provided
1232
    to 'run_dynamic_melt_f_calibration'. It is used if the run_function fails and
1233
    if 'ignore_error == True' in 'run_dynamic_melt_f_calibration'. First it resets
1234
    melt_f of gdir. Afterwards it tries to conduct a dynamic spinup. If this
1235
    also fails the last thing is to just do a run without a dynamic spinup
1236
    (only a fixed geometry spinup).
1237

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

1352
    Returns
1353
    -------
1354
    :py:class:`oggm.core.flowline.evolution_model`
1355
        The final model after the run.
1356
    """
1357
    from oggm.workflow import calibrate_inversion_from_consensus
1✔
1358

1359
    evolution_model = decide_evolution_model(evolution_model)
1✔
1360

1361
    if local_variables is None:
1✔
1362
        raise RuntimeError('Need the volume to do'
1✔
1363
                           'calibrate_inversion_from_consensus provided in '
1364
                           'local_variables!')
1365

1366
    # revert gdir to original state if necessary
1367
    if melt_f != gdir.read_json('mb_calib')['melt_f']:
×
1368
        define_new_melt_f_in_gdir(gdir, melt_f)
×
1369
        if do_inversion:
×
1370
            with utils.DisableLogger():
×
1371
                apparent_mb_from_any_mb(gdir,
×
1372
                                        add_to_log_file=False)
1373
                calibrate_inversion_from_consensus(
×
1374
                    [gdir], apply_fs_on_mismatch=True, error_on_mismatch=False,
1375
                    filter_inversion_output=True,
1376
                    volume_m3_reference=local_variables['vol_m3_ref'],
1377
                    add_to_log_file=False)
1378
    if os.path.isfile(os.path.join(gdir.dir,
×
1379
                                   'model_flowlines_dyn_melt_f_calib.pkl')):
1380
        os.remove(os.path.join(gdir.dir,
×
1381
                               'model_flowlines_dyn_melt_f_calib.pkl'))
1382

1383
    if target_yr is None:
×
1384
        target_yr = gdir.rgi_date + 1  # + 1 converted to hydro years
×
1385
    if min_spinup_period > target_yr - ys:
×
1386
        log.info('The RGI year is closer to ys as the minimum spinup '
×
1387
                 'period -> therefore the minimum spinup period is '
1388
                 'adapted and it is the only period which is tried by the '
1389
                 'dynamic spinup function!')
1390
        min_spinup_period = target_yr - ys
×
1391
        spinup_period = target_yr - ys
×
1392

1393
    yr_clim_min = gdir.get_climate_info()['baseline_yr_0']
×
1394
    try:
×
1395
        model_end = run_dynamic_spinup(
×
1396
            gdir,
1397
            continue_on_error=False,  # force to raise an error in @entity_task
1398
            add_to_log_file=False,
1399
            init_model_fls=fls_init,
1400
            climate_input_filesuffix=climate_input_filesuffix,
1401
            evolution_model=evolution_model,
1402
            mb_model_historical=mb_model_historical,
1403
            mb_model_spinup=mb_model_spinup,
1404
            spinup_period=spinup_period,
1405
            spinup_start_yr=ys,
1406
            min_spinup_period=min_spinup_period,
1407
            spinup_start_yr_max=spinup_start_yr_max,
1408
            target_yr=target_yr,
1409
            minimise_for=minimise_for,
1410
            precision_percent=precision_percent,
1411
            precision_absolute=precision_absolute,
1412
            min_ice_thickness=min_ice_thickness,
1413
            first_guess_t_bias=first_guess_t_bias,
1414
            t_bias_max_step_length=t_bias_max_step_length,
1415
            maxiter=maxiter,
1416
            output_filesuffix=output_filesuffix,
1417
            store_model_geometry=store_model_geometry,
1418
            store_fl_diagnostics=store_fl_diagnostics,
1419
            ignore_errors=False,
1420
            ye=ye,
1421
            add_fixed_geometry_spinup=add_fixed_geometry_spinup,
1422
            **kwargs)
1423

1424
        gdir.add_to_diagnostics('used_spinup_option', 'dynamic spinup only')
×
1425

1426
    except RuntimeError:
×
1427
        log.warning('No dynamic spinup could be conducted by using the '
×
1428
                    f'original melt factor ({melt_f}). Therefore the last '
1429
                    'try is to conduct a run until ye without a dynamic '
1430
                    'spinup.')
1431
        model_end = run_from_climate_data(
×
1432
            gdir,
1433
            add_to_log_file=False,
1434
            min_ys=yr_clim_min, ye=ye,
1435
            output_filesuffix=output_filesuffix,
1436
            climate_input_filesuffix=climate_input_filesuffix,
1437
            store_model_geometry=store_model_geometry,
1438
            store_fl_diagnostics=store_fl_diagnostics,
1439
            init_model_fls=fls_init, evolution_model=evolution_model,
1440
            fixed_geometry_spinup_yr=ys)
1441

1442
        gdir.add_to_diagnostics('used_spinup_option', 'fixed geometry spinup')
×
1443

1444
        # set all dynamic diagnostics to None if there where some successful
1445
        # runs
1446
        diag = gdir.get_diagnostics()
×
1447
        if minimise_for == 'area':
×
1448
            unit = 'km2'
×
1449
        elif minimise_for == 'volume':
×
1450
            unit = 'km3'
×
1451
        else:
1452
            raise NotImplementedError
1453
        for key in ['temp_bias_dynamic_spinup', 'dynamic_spinup_period',
×
1454
                    'dynamic_spinup_forward_model_iterations',
1455
                    f'{minimise_for}_mismatch_dynamic_spinup_{unit}_percent',
1456
                    f'reference_{minimise_for}_dynamic_spinup_{unit}',
1457
                    'dynamic_spinup_other_variable_reference',
1458
                    'dynamic_spinup_mismatch_other_variable_percent']:
1459
            if key in diag:
×
1460
                gdir.add_to_diagnostics(key, None)
×
1461

1462
        gdir.add_to_diagnostics('run_dynamic_spinup_success', False)
×
1463
    return model_end
×
1464

1465

1466
def dynamic_melt_f_run(
9✔
1467
        gdir, melt_f, yr0_ref_mb, yr1_ref_mb, fls_init, ys, ye,
1468
        output_filesuffix='', evolution_model=None,
1469
        local_variables=None, set_local_variables=False, target_yr=None,
1470
        **kwargs):
1471
    """
1472
    This function is one option for a 'run_function' for the
1473
    'run_dynamic_melt_f_calibration' function (the corresponding
1474
    'fallback_function' is 'dynamic_melt_f_run_fallback'). It is meant to
1475
    define a new melt_f in the given gdir and conduct a
1476
    'run_from_climate_data' run between ys and ye and return the geodetic mass
1477
    balance (units: kg m-2 yr-1) of the period yr0_ref_mb and yr1_ref_mb.
1478

1479
    Parameters
1480
    ----------
1481
    gdir : :py:class:`oggm.GlacierDirectory`
1482
        the glacier directory to process
1483
    melt_f : float
1484
        the melt_f used for this run
1485
    yr0_ref_mb : int
1486
        the start year of the geodetic mass balance
1487
    yr1_ref_mb : int
1488
        the end year of the geodetic mass balance
1489
    fls_init : []
1490
        List of flowlines to use to initialise the model
1491
    ys : int
1492
        start year of the complete run, must by smaller or equal y0_ref_mb
1493
    ye : int
1494
        end year of the complete run, must be smaller or equal y1_ref_mb
1495
    output_filesuffix : str
1496
        For the output file.
1497
        Default is ''
1498
    evolution_model : :class:oggm.core.FlowlineModel
1499
        which evolution model to use. Default: cfg.PARAMS['evolution_model']
1500
        Not all models work in all circumstances!
1501
    local_variables : None
1502
        Not needed in this function, just here to match with the function
1503
        call in run_dynamic_melt_f_calibration.
1504
    set_local_variables : bool
1505
        Not needed in this function. Only here to be confirm with the use of
1506
        this function in 'run_dynamic_melt_f_calibration'.
1507
    target_yr : int or None
1508
        The target year for a potential dynamic spinup (not needed here).
1509
        Default is None
1510
    kwargs : dict
1511
        kwargs to pass to the evolution_model instance
1512

1513
    Returns
1514
    -------
1515
    :py:class:`oggm.core.flowline.evolution_model`, float
1516
        The final model after the run and the calculated geodetic mass balance
1517
    """
1518

1519
    evolution_model = decide_evolution_model(evolution_model)
1✔
1520

1521
    if set_local_variables:
1✔
1522
        # No local variables needed in this function
1523
        return None
1✔
1524

1525
    # Here we start with the actual model run
1526
    define_new_melt_f_in_gdir(gdir, melt_f)
1✔
1527

1528
    # conduct model run
1529
    try:
1✔
1530
        model = run_from_climate_data(gdir,
1✔
1531
                                      # force to raise an error in @entity_task
1532
                                      continue_on_error=False,
1533
                                      add_to_log_file=False,
1534
                                      ys=ys, ye=ye,
1535
                                      output_filesuffix=output_filesuffix,
1536
                                      init_model_fls=fls_init,
1537
                                      evolution_model=evolution_model,
1538
                                      **kwargs)
1539
    except RuntimeError as e:
1✔
1540
        raise RuntimeError(f'run_from_climate_data raised error! '
1✔
1541
                           f'(Message: {e})')
1542

1543
    # calculate dmdtda from previous simulation here
1544
    with utils.DisableLogger():
1✔
1545
        ds = utils.compile_run_output(gdir, input_filesuffix=output_filesuffix,
1✔
1546
                                      path=False)
1547
    dmdtda_mdl = ((ds.volume.loc[yr1_ref_mb].values -
1✔
1548
                   ds.volume.loc[yr0_ref_mb].values) /
1549
                  gdir.rgi_area_m2 /
1550
                  (yr1_ref_mb - yr0_ref_mb) *
1551
                  cfg.PARAMS['ice_density'])
1552

1553
    return model, dmdtda_mdl
1✔
1554

1555

1556
def dynamic_melt_f_run_fallback(
9✔
1557
        gdir, melt_f, fls_init, ys, ye, local_variables, output_filesuffix='',
1558
        evolution_model=None, target_yr=None, **kwargs):
1559
    """
1560
    This is the fallback function corresponding to the function
1561
    'dynamic_melt_f_run', which are provided to
1562
    'run_dynamic_melt_f_calibration'. It is used if the run_function fails and
1563
    if 'ignore_error=True' in 'run_dynamic_melt_f_calibration'. It sets
1564
    melt_f and conduct a run_from_climate_data run from ys to ye.
1565

1566
    Parameters
1567
    ----------
1568
    gdir : :py:class:`oggm.GlacierDirectory`
1569
        the glacier directory to process
1570
    melt_f : float
1571
        the melt_f used for this run
1572
    fls_init : []
1573
        List of flowlines to use to initialise the model
1574
    ys : int
1575
        start year of the run
1576
    ye : int
1577
        end year of the run
1578
    local_variables : dict
1579
        Not needed in this function, just here to match with the function
1580
        call in run_dynamic_melt_f_calibration.
1581
    output_filesuffix : str
1582
        For the output file.
1583
        Default is ''
1584
    evolution_model : :class:oggm.core.FlowlineModel
1585
        which evolution model to use. Default: cfg.PARAMS['evolution_model']
1586
        Not all models work in all circumstances!
1587
    target_yr : int or None
1588
        The target year for a potential dynamic spinup (not needed here).
1589
        Default is None
1590
    kwargs : dict
1591
        kwargs to pass to the evolution_model instance
1592

1593
     Returns
1594
    -------
1595
    :py:class:`oggm.core.flowline.evolution_model`
1596
        The final model after the run.
1597
    """
1598

1599
    evolution_model = decide_evolution_model(evolution_model)
1✔
1600

1601
    define_new_melt_f_in_gdir(gdir, melt_f)
1✔
1602

1603
    # conduct model run
1604
    try:
1✔
1605
        model = run_from_climate_data(gdir,
1✔
1606
                                      # force to raise an error in @entity_task
1607
                                      continue_on_error=False,
1608
                                      add_to_log_file=False,
1609
                                      ys=ys, ye=ye,
1610
                                      output_filesuffix=output_filesuffix,
1611
                                      init_model_fls=fls_init,
1612
                                      evolution_model=evolution_model,
1613
                                      **kwargs)
1614
        gdir.add_to_diagnostics('used_spinup_option', 'no spinup')
1✔
1615
    except RuntimeError as e:
×
1616
        raise RuntimeError(f'In fallback function run_from_climate_data '
×
1617
                           f'raised error! (Message: {e})')
1618

1619
    return model
1✔
1620

1621

1622
@entity_task(log, writes=['inversion_flowlines'])
9✔
1623
def run_dynamic_melt_f_calibration(
9✔
1624
        gdir, ref_dmdtda=None, err_ref_dmdtda=None, err_dmdtda_scaling_factor=1,
1625
        ref_period='', melt_f_min=None,
1626
        melt_f_max=None, melt_f_max_step_length_minimum=0.1, maxiter=20,
1627
        ignore_errors=False, output_filesuffix='_dynamic_melt_f',
1628
        ys=None, ye=None, target_yr=None,
1629
        run_function=dynamic_melt_f_run_with_dynamic_spinup,
1630
        kwargs_run_function=None,
1631
        fallback_function=dynamic_melt_f_run_with_dynamic_spinup_fallback,
1632
        kwargs_fallback_function=None, init_model_filesuffix=None,
1633
        init_model_yr=None, init_model_fls=None,
1634
        first_guess_diagnostic_msg='dynamic spinup only'):
1635
    """Calibrate melt_f to match a geodetic mass balance incorporating a
1636
    dynamic model run.
1637

1638
    This task iteratively search for a melt_f to match a provided geodetic
1639
    mass balance. How one model run looks like is defined in the 'run_function'.
1640
    This function should take a new melt_f guess, conducts a dynamic run and
1641
    calculate the geodetic mass balance. The goal is to match the geodetic mass
1642
    blanance 'ref_dmdtda' inside the provided error 'err_ref_dmdtda'. If the
1643
    minimisation of the mismatch between the provided and modeled geodetic mass
1644
    balance is not working the 'fallback_function' is called. In there it is
1645
    decided what run should be conducted in such a failing case. Further if
1646
    'ignore_error' is set to True and we could not find a satisfying mismatch
1647
    the best run so far is saved (if not one successful run with 'run_function'
1648
    the 'fallback_function' is called).
1649

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

1758
    Returns
1759
    -------
1760
    :py:class:`oggm.core.flowline.evolution_model`
1761
        The final dynamically spined-up model. Type depends on the selected
1762
        evolution_model.
1763
    """
1764
    # melt_f constraints
1765
    if melt_f_min is None:
2✔
1766
        melt_f_min = cfg.PARAMS['melt_f_min']
2✔
1767
    if melt_f_max is None:
2✔
1768
        melt_f_max = cfg.PARAMS['melt_f_max']
×
1769

1770
    if kwargs_run_function is None:
2✔
1771
        kwargs_run_function = {}
1✔
1772
    if kwargs_fallback_function is None:
2✔
1773
        kwargs_fallback_function = {}
1✔
1774

1775
    # geodetic mb stuff
1776
    if not ref_period:
2✔
1777
        ref_period = cfg.PARAMS['geodetic_mb_period']
2✔
1778
    # if a reference geodetic mb is specified also the error of it must be
1779
    # specified, and vice versa
1780
    if ((ref_dmdtda is None and err_ref_dmdtda is not None) or
2✔
1781
            (ref_dmdtda is not None and err_ref_dmdtda is None)):
1782
        raise RuntimeError('If you provide a reference geodetic mass balance '
1✔
1783
                           '(ref_dmdtda) you must also provide an error for it '
1784
                           '(err_ref_dmdtda), and vice versa!')
1785
    # Get the reference geodetic mb and error if not given
1786
    if ref_dmdtda is None:
2✔
1787
        df_ref_dmdtda = utils.get_geodetic_mb_dataframe().loc[gdir.rgi_id]
2✔
1788
        # reference geodetic mass balance from Hugonnet 2021
1789
        ref_dmdtda = float(
2✔
1790
            df_ref_dmdtda.loc[df_ref_dmdtda['period'] == ref_period]['dmdtda'])
1791
        # dmdtda: in meters water-equivalent per year -> we convert
1792
        ref_dmdtda *= 1000  # kg m-2 yr-1
2✔
1793
        # error of reference geodetic mass balance from Hugonnet 2021
1794
        err_ref_dmdtda = float(df_ref_dmdtda.loc[df_ref_dmdtda['period'] ==
2✔
1795
                                                 ref_period]['err_dmdtda'])
1796
        err_ref_dmdtda *= 1000  # kg m-2 yr-1
2✔
1797
        err_ref_dmdtda *= err_dmdtda_scaling_factor
2✔
1798

1799
    if err_ref_dmdtda <= 0:
2✔
1800
        raise RuntimeError('The provided error for the geodetic mass-balance '
1✔
1801
                           '(err_ref_dmdtda) must be positive and non zero! But'
1802
                           f'given was {err_ref_dmdtda}!')
1803
    # get start and end year of geodetic mb
1804
    yr0_ref_mb, yr1_ref_mb = ref_period.split('_')
2✔
1805
    yr0_ref_mb = int(yr0_ref_mb.split('-')[0])
2✔
1806
    yr1_ref_mb = int(yr1_ref_mb.split('-')[0])
2✔
1807

1808
    clim_info = gdir.get_climate_info()
2✔
1809

1810
    if ye is None:
2✔
1811
        # One adds 1 because the run ends at the end of the year
1812
        ye = clim_info['baseline_yr_1'] + 1
1✔
1813

1814
    if ye < yr1_ref_mb:
2✔
1815
        raise RuntimeError('The provided ye is smaller than the end year of '
1✔
1816
                           'the given geodetic_mb_period!')
1817

1818
    if ys is None:
2✔
1819
        ys = clim_info['baseline_yr_0']
1✔
1820

1821
    if ys > yr0_ref_mb:
2✔
1822
        raise RuntimeError('The provided ys is larger than the start year of '
1✔
1823
                           'the given geodetic_mb_period!')
1824

1825
    if target_yr is None:
2✔
1826
        target_yr = gdir.rgi_date + 1  # + 1 converted to hydro years
2✔
1827
    if target_yr < ys:
2✔
1828
        if ignore_errors:
×
1829
            log.info('The rgi year is smaller than the provided start year '
×
1830
                     'ys -> setting the rgi year to ys to continue!')
1831
            target_yr = ys
×
1832
        else:
1833
            raise RuntimeError('The rgi year is smaller than the provided '
×
1834
                               'start year ys!')
1835
    kwargs_run_function['target_yr'] = target_yr
2✔
1836
    kwargs_fallback_function['target_yr'] = target_yr
2✔
1837

1838
    # get initial flowlines from which we want to start from
1839
    if init_model_filesuffix is not None:
2✔
1840
        fp = gdir.get_filepath('model_geometry',
1✔
1841
                               filesuffix=init_model_filesuffix)
1842
        fmod = FileModel(fp)
1✔
1843
        if init_model_yr is None:
1✔
1844
            init_model_yr = fmod.last_yr
1✔
1845
        fmod.run_until(init_model_yr)
1✔
1846
        init_model_fls = fmod.fls
1✔
1847

1848
    if init_model_fls is None:
2✔
1849
        fls_init = gdir.read_pickle('model_flowlines')
2✔
1850
    else:
1851
        fls_init = copy.deepcopy(init_model_fls)
1✔
1852

1853
    # save original melt_f for later to be able to recreate original gdir
1854
    # (using the fallback function) if an error occurs
1855
    melt_f_initial = gdir.read_json('mb_calib')['melt_f']
2✔
1856

1857
    # define maximum allowed change of melt_f between two iterations. Is needed
1858
    # to avoid to large changes (=likely lead to an error). It is defined in a
1859
    # way that in maxiter steps the further away limit can be reached
1860
    melt_f_max_step_length = np.max(
2✔
1861
        [np.max(np.abs(np.array([melt_f_min, melt_f_min]) - melt_f_initial)) /
1862
         maxiter,
1863
         melt_f_max_step_length_minimum])
1864

1865
    # only used to check performance of minimisation
1866
    dynamic_melt_f_calibration_runs = [0]
2✔
1867

1868
    # this function is called if the actual dynamic melt_f calibration fails
1869
    def fallback_run(melt_f, reset, best_mismatch=None, initial_mismatch=None,
2✔
1870
                     only_first_guess=None):
1871
        if reset:
1✔
1872
            # unfortunately we could not conduct an error free run using the
1873
            # provided run_function, so we us the fallback_function
1874

1875
            # this diagnostics should be overwritten inside the fallback_function
1876
            gdir.add_to_diagnostics('used_spinup_option', 'fallback_function')
1✔
1877

1878
            model = fallback_function(gdir=gdir, melt_f=melt_f,
1✔
1879
                                      fls_init=fls_init, ys=ys, ye=ye,
1880
                                      local_variables=local_variables_run_function,
1881
                                      output_filesuffix=output_filesuffix,
1882
                                      **kwargs_fallback_function)
1883
        else:
1884
            # we were not able to reach the desired precision during the
1885
            # minimisation, but at least we conducted a few error free runs
1886
            # using the run_function, and therefore we save the best guess we
1887
            # found so far
1888
            if only_first_guess:
1✔
1889
                gdir.add_to_diagnostics('used_spinup_option',
×
1890
                                        first_guess_diagnostic_msg)
1891
            else:
1892
                gdir.add_to_diagnostics('used_spinup_option',
1✔
1893
                                        'dynamic melt_f calibration (part '
1894
                                        'success)')
1895
            model, dmdtda_mdl = run_function(gdir=gdir, melt_f=melt_f,
1✔
1896
                                             yr0_ref_mb=yr0_ref_mb,
1897
                                             yr1_ref_mb=yr1_ref_mb,
1898
                                             fls_init=fls_init, ys=ys, ye=ye,
1899
                                             output_filesuffix=output_filesuffix,
1900
                                             local_variables=local_variables_run_function,
1901
                                             **kwargs_run_function)
1902

1903
            gdir.add_to_diagnostics(
1✔
1904
                'dmdtda_mismatch_dynamic_calibration_reference',
1905
                float(ref_dmdtda))
1906
            gdir.add_to_diagnostics(
1✔
1907
                'dmdtda_dynamic_calibration_given_error',
1908
                float(err_ref_dmdtda))
1909
            gdir.add_to_diagnostics('dmdtda_dynamic_calibration_error_scaling_factor',
1✔
1910
                                    float(err_dmdtda_scaling_factor))
1911
            gdir.add_to_diagnostics(
1✔
1912
                'dmdtda_mismatch_dynamic_calibration',
1913
                float(best_mismatch))
1914
            gdir.add_to_diagnostics(
1✔
1915
                'dmdtda_mismatch_with_initial_melt_f',
1916
                float(initial_mismatch))
1917
            gdir.add_to_diagnostics('melt_f_dynamic_calibration',
1✔
1918
                                    float(melt_f))
1919
            gdir.add_to_diagnostics('melt_f_before_dynamic_calibration',
1✔
1920
                                    float(melt_f_initial))
1921
            gdir.add_to_diagnostics('run_dynamic_melt_f_calibration_iterations',
1✔
1922
                                    int(dynamic_melt_f_calibration_runs[-1]))
1923

1924
        return model
1✔
1925

1926
    # here we define the local variables which are used in the run_function,
1927
    # for some run_functions this is useful to save parameters from a previous
1928
    # run to be faster in the upcoming runs
1929
    local_variables_run_function = {}
2✔
1930
    run_function(gdir=gdir, melt_f=None, yr0_ref_mb=None, yr1_ref_mb=None,
2✔
1931
                 fls_init=None, ys=None, ye=None,
1932
                 local_variables=local_variables_run_function,
1933
                 set_local_variables=True, **kwargs_run_function)
1934

1935
    # this is the actual model run which is executed each iteration in order to
1936
    # minimise the mismatch of dmdtda of model and observation
1937
    def model_run(melt_f):
2✔
1938
        # to check performance of minimisation
1939
        dynamic_melt_f_calibration_runs.append(
2✔
1940
            dynamic_melt_f_calibration_runs[-1] + 1)
1941

1942
        model, dmdtda_mdl = run_function(gdir=gdir, melt_f=melt_f,
2✔
1943
                                         yr0_ref_mb=yr0_ref_mb,
1944
                                         yr1_ref_mb=yr1_ref_mb,
1945
                                         fls_init=fls_init, ys=ys, ye=ye,
1946
                                         output_filesuffix=output_filesuffix,
1947
                                         local_variables=local_variables_run_function,
1948
                                         **kwargs_run_function)
1949
        return model, dmdtda_mdl
2✔
1950

1951
    def cost_fct(melt_f, model_dynamic_spinup_end):
2✔
1952

1953
        # actual model run
1954
        model_dynamic_spinup, dmdtda_mdl = model_run(melt_f)
2✔
1955

1956
        # save final model for later
1957
        model_dynamic_spinup_end.append(copy.deepcopy(model_dynamic_spinup))
2✔
1958

1959
        # calculate the mismatch of dmdtda
1960
        cost = float(dmdtda_mdl - ref_dmdtda)
2✔
1961

1962
        return cost
2✔
1963

1964
    def init_cost_fun():
2✔
1965
        model_dynamic_spinup_end = []
2✔
1966

1967
        def c_fun(melt_f):
2✔
1968
            return cost_fct(melt_f, model_dynamic_spinup_end)
2✔
1969

1970
        return c_fun, model_dynamic_spinup_end
2✔
1971

1972
    # Here start with own spline minimisation algorithm
1973
    def minimise_with_spline_fit(fct_to_minimise, melt_f_guess, mismatch):
2✔
1974
        # defines limits of melt_f in accordance to maximal allowed change
1975
        # between iterations
1976
        melt_f_limits = [melt_f_initial - melt_f_max_step_length,
2✔
1977
                         melt_f_initial + melt_f_max_step_length]
1978

1979
        # this two variables indicate that the limits were already adapted to
1980
        # avoid an error
1981
        was_min_error = False
2✔
1982
        was_max_error = False
2✔
1983
        was_errors = [was_min_error, was_max_error]
2✔
1984

1985
        def get_mismatch(melt_f):
2✔
1986
            melt_f = copy.deepcopy(melt_f)
2✔
1987
            # first check if the melt_f is inside limits
1988
            if melt_f < melt_f_limits[0]:
2✔
1989
                # was the smaller limit already executed, if not first do this
1990
                if melt_f_limits[0] not in melt_f_guess:
2✔
1991
                    melt_f = copy.deepcopy(melt_f_limits[0])
2✔
1992
                else:
1993
                    # smaller limit was already used, check if it was
1994
                    # already newly defined with error
1995
                    if was_errors[0]:
×
1996
                        raise RuntimeError('Not able to minimise without '
×
1997
                                           'raising an error at lower limit of '
1998
                                           'melt_f!')
1999
                    else:
2000
                        # ok we set a new lower limit, consider also minimum
2001
                        # limit
2002
                        melt_f_limits[0] = max(melt_f_min,
×
2003
                                               melt_f_limits[0] -
2004
                                               melt_f_max_step_length)
2005
            elif melt_f > melt_f_limits[1]:
2✔
2006
                # was the larger limit already executed, if not first do this
2007
                if melt_f_limits[1] not in melt_f_guess:
1✔
2008
                    melt_f = copy.deepcopy(melt_f_limits[1])
1✔
2009
                else:
2010
                    # larger limit was already used, check if it was
2011
                    # already newly defined with ice free glacier
2012
                    if was_errors[1]:
×
2013
                        raise RuntimeError('Not able to minimise without '
×
2014
                                           'raising an error at upper limit of '
2015
                                           'melt_f!')
2016
                    else:
2017
                        # ok we set a new upper limit, consider also maximum
2018
                        # limit
2019
                        melt_f_limits[1] = min(melt_f_max,
×
2020
                                               melt_f_limits[1] +
2021
                                               melt_f_max_step_length)
2022

2023
            # now clip melt_f with limits (to be sure)
2024
            melt_f = np.clip(melt_f, melt_f_limits[0], melt_f_limits[1])
2✔
2025
            if melt_f in melt_f_guess:
2✔
2026
                raise RuntimeError('This melt_f was already tried. Probably '
×
2027
                                   'we are at one of the max or min limit and '
2028
                                   'still have no satisfactory mismatch '
2029
                                   'found!')
2030

2031
            # if error during dynamic calibration this defines how much
2032
            # melt_f is changed in the upcoming iterations to look for an
2033
            # error free run
2034
            melt_f_search_change = melt_f_max_step_length / 10
2✔
2035
            # maximum number of changes to look for an error free run
2036
            max_iterations = int(melt_f_max_step_length /
2✔
2037
                                 melt_f_search_change)
2038

2039
            current_min_error = False
2✔
2040
            current_max_error = False
2✔
2041
            doing_first_guess = (len(mismatch) == 0)
2✔
2042
            iteration = 0
2✔
2043
            current_melt_f = copy.deepcopy(melt_f)
2✔
2044

2045
            # in this loop if an error at the limits is raised we go step by
2046
            # step away from the limits until we are at the initial guess or we
2047
            # found an error free run
2048
            tmp_mismatch = None
2✔
2049
            while ((current_min_error | current_max_error | iteration == 0) &
2✔
2050
                   (iteration < max_iterations)):
2051
                try:
2✔
2052
                    tmp_mismatch = fct_to_minimise(melt_f)
2✔
2053
                except RuntimeError as e:
1✔
2054
                    # check if we are at the lower limit
2055
                    if melt_f == melt_f_limits[0]:
1✔
2056
                        # check if there was already an error at the lower limit
2057
                        if was_errors[0]:
×
2058
                            raise RuntimeError('Second time with error at '
×
2059
                                               'lower limit of melt_f! '
2060
                                               'Error message of model run: '
2061
                                               f'{e}')
2062
                        else:
2063
                            was_errors[0] = True
×
2064
                            current_min_error = True
×
2065

2066
                    # check if we are at the upperlimit
2067
                    elif melt_f == melt_f_limits[1]:
1✔
2068
                        # check if there was already an error at the lower limit
2069
                        if was_errors[1]:
1✔
2070
                            raise RuntimeError('Second time with error at '
×
2071
                                               'upper limit of melt_f! '
2072
                                               'Error message of model run: '
2073
                                               f'{e}')
2074
                        else:
2075
                            was_errors[1] = True
1✔
2076
                            current_max_error = True
1✔
2077

2078
                    if current_min_error:
1✔
2079
                        # currently we searching for a new lower limit with no
2080
                        # error
2081
                        melt_f = np.round(melt_f + melt_f_search_change,
×
2082
                                          decimals=1)
2083
                    elif current_max_error:
1✔
2084
                        # currently we searching for a new upper limit with no
2085
                        # error
2086
                        melt_f = np.round(melt_f - melt_f_search_change,
1✔
2087
                                          decimals=1)
2088

2089
                    # if we end close to an already executed guess while
2090
                    # searching for a new limit we quite
2091
                    if np.isclose(melt_f, melt_f_guess).any():
1✔
2092
                        raise RuntimeError('Not able to further minimise, '
×
2093
                                           'return the best we have so far!'
2094
                                           f'Error message: {e}')
2095

2096
                    if doing_first_guess:
1✔
2097
                        # unfortunately first guess is not working
2098
                        raise RuntimeError('Dynamic calibration is not working '
1✔
2099
                                           'with first guess! Error '
2100
                                           f'message: {e}')
2101

2102
                    if np.isclose(melt_f, current_melt_f):
1✔
2103
                        # something unexpected happen so we end here
2104
                        raise RuntimeError('Unexpected error not at the limits'
×
2105
                                           f' of melt_f. Error Message: {e}')
2106

2107
                iteration += 1
2✔
2108

2109
            if iteration >= max_iterations:
2✔
2110
                # ok we were not able to find an mismatch without error
2111
                if current_min_error:
×
2112
                    raise RuntimeError('Not able to find new lower limit for '
×
2113
                                       'melt_f!')
2114
                elif current_max_error:
×
2115
                    raise RuntimeError('Not able to find new upper limit for '
×
2116
                                       'melt_f!')
2117
                else:
2118
                    raise RuntimeError('Something unexpected happened during '
×
2119
                                       'definition of new melt_f limits!')
2120
            else:
2121
                # if we found a new limit set it
2122
                if current_min_error:
2✔
2123
                    melt_f_limits[0] = copy.deepcopy(melt_f)
×
2124
                elif current_max_error:
2✔
2125
                    melt_f_limits[1] = copy.deepcopy(melt_f)
1✔
2126

2127
            if tmp_mismatch is None:
2✔
2128
                raise RuntimeError('Not able to find a new mismatch for '
1✔
2129
                                   'dmdtda!')
2130

2131
            return float(tmp_mismatch), float(melt_f)
2✔
2132

2133
        # first guess
2134
        new_mismatch, new_melt_f = get_mismatch(melt_f_initial)
2✔
2135
        melt_f_guess.append(new_melt_f)
2✔
2136
        mismatch.append(new_mismatch)
2✔
2137

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

2141
        # second (arbitrary) guess is given depending on the outcome of first
2142
        # guess, melt_f is changed for percent of mismatch relative to
2143
        # err_ref_dmdtda times melt_f_max_step_length (if
2144
        # mismatch = 2 * err_ref_dmdtda this corresponds to 100%; for 100% or
2145
        # 150% the next step is (-1) * melt_f_max_step_length; if mismatch
2146
        # -40%, next step is 0.4 * melt_f_max_step_length; but always at least
2147
        # an absolute change of 0.02 is imposed to prevent too close guesses).
2148
        # (-1) as if mismatch is negative we need a larger melt_f to get closer
2149
        # to 0.
2150
        step = (-1) * np.sign(mismatch[-1]) * \
2✔
2151
            max((np.abs(mismatch[-1]) - err_ref_dmdtda) / err_ref_dmdtda *
2152
                melt_f_max_step_length, 0.02)
2153
        new_mismatch, new_melt_f = get_mismatch(melt_f_guess[0] + step)
2✔
2154
        melt_f_guess.append(new_melt_f)
2✔
2155
        mismatch.append(new_mismatch)
2✔
2156

2157
        if abs(mismatch[-1]) < err_ref_dmdtda:
2✔
2158
            return mismatch[-1], new_melt_f
×
2159

2160
        # Now start with splin fit for guessing
2161
        while len(melt_f_guess) < maxiter:
2✔
2162
            # get next guess from splin (fit partial linear function to
2163
            # previously calculated (mismatch, melt_f) pairs and get melt_f
2164
            # value where mismatch=0 from this fitted curve)
2165
            sort_index = np.argsort(np.array(mismatch))
2✔
2166
            tck = interpolate.splrep(np.array(mismatch)[sort_index],
2✔
2167
                                     np.array(melt_f_guess)[sort_index],
2168
                                     k=1)
2169
            # here we catch interpolation errors (two different melt_f with
2170
            # same mismatch), could happen if one melt_f was close to a newly
2171
            # defined limit
2172
            if np.isnan(tck[1]).any():
2✔
2173
                if was_errors[0]:
×
2174
                    raise RuntimeError('Second time with error at lower '
×
2175
                                       'limit of melt_f! (nan in splin fit)')
2176
                elif was_errors[1]:
×
2177
                    raise RuntimeError('Second time with error at upper '
×
2178
                                       'limit of melt_f! (nan in splin fit)')
2179
                else:
2180
                    raise RuntimeError('Not able to minimise! Problem is '
×
2181
                                       'unknown. (nan in splin fit)')
2182
            new_mismatch, new_melt_f = get_mismatch(
2✔
2183
                float(interpolate.splev(0, tck)))
2184
            melt_f_guess.append(new_melt_f)
2✔
2185
            mismatch.append(new_mismatch)
2✔
2186

2187
            if abs(mismatch[-1]) < err_ref_dmdtda:
2✔
2188
                return mismatch[-1], new_melt_f
2✔
2189

2190
        # Ok when we end here the spinup could not find satisfying match after
2191
        # maxiter(ations)
2192
        raise RuntimeError(f'Could not find mismatch smaller '
1✔
2193
                           f'{err_ref_dmdtda} kg m-2 yr-1 (only '
2194
                           f'{np.min(np.abs(mismatch))} kg m-2 yr-1) in '
2195
                           f'{maxiter} Iterations!')
2196

2197
    # wrapper to get values for intermediate (mismatch, melt_f) guesses if an
2198
    # error is raised
2199
    def init_minimiser():
2✔
2200
        melt_f_guess = []
2✔
2201
        mismatch = []
2✔
2202

2203
        def minimiser(fct_to_minimise):
2✔
2204
            return minimise_with_spline_fit(fct_to_minimise, melt_f_guess,
2✔
2205
                                            mismatch)
2206

2207
        return minimiser, melt_f_guess, mismatch
2✔
2208

2209
    # define function for the actual minimisation
2210
    c_fun, models_dynamic_spinup_end = init_cost_fun()
2✔
2211

2212
    # define minimiser
2213
    minimise_given_fct, melt_f_guesses, mismatch_dmdtda = init_minimiser()
2✔
2214

2215
    try:
2✔
2216
        final_mismatch, final_melt_f = minimise_given_fct(c_fun)
2✔
2217
    except RuntimeError as e:
1✔
2218
        # something happened during minimisation, if there where some
2219
        # successful runs we return the one with the best mismatch, otherwise
2220
        # we conduct just a run with no dynamic spinup
2221
        if len(mismatch_dmdtda) == 0:
1✔
2222
            # we conducted no successful run, so run without dynamic spinup
2223
            if ignore_errors:
1✔
2224
                log.info('Dynamic melt_f calibration not successful. '
1✔
2225
                         f'Error message: {e}')
2226
                model_return = fallback_run(melt_f=melt_f_initial,
1✔
2227
                                            reset=True)
2228
                return model_return
1✔
2229
            else:
2230
                raise RuntimeError('Dynamic melt_f calibration was not '
×
2231
                                   f'successful! Error Message: {e}')
2232
        else:
2233
            if ignore_errors:
1✔
2234
                log.info('Dynamic melt_f calibration not successful. Error '
1✔
2235
                         f'message: {e}')
2236

2237
                # there where some successful runs so we return the one with the
2238
                # smallest mismatch of dmdtda
2239
                min_mismatch_index = np.argmin(np.abs(mismatch_dmdtda))
1✔
2240
                melt_f_best = np.array(melt_f_guesses)[min_mismatch_index]
1✔
2241

2242
                # check if the first guess was the best guess
2243
                only_first_guess = False
1✔
2244
                if min_mismatch_index == 1:
1✔
2245
                    only_first_guess = True
×
2246

2247
                model_return = fallback_run(
1✔
2248
                    melt_f=melt_f_best, reset=False,
2249
                    best_mismatch=np.array(mismatch_dmdtda)[min_mismatch_index],
2250
                    initial_mismatch=mismatch_dmdtda[0],
2251
                    only_first_guess=only_first_guess)
2252

2253
                return model_return
1✔
2254
            else:
2255
                raise RuntimeError('Dynamic melt_f calibration not successful. '
1✔
2256
                                   f'Error message: {e}')
2257

2258
    # check that new melt_f is correctly saved in gdir
2259
    assert final_melt_f == gdir.read_json('mb_calib')['melt_f']
2✔
2260

2261
    # hurray, dynamic melt_f calibration successful
2262
    gdir.add_to_diagnostics('used_spinup_option',
2✔
2263
                            'dynamic melt_f calibration (full success)')
2264
    gdir.add_to_diagnostics('dmdtda_mismatch_dynamic_calibration_reference',
2✔
2265
                            float(ref_dmdtda))
2266
    gdir.add_to_diagnostics('dmdtda_dynamic_calibration_given_error',
2✔
2267
                            float(err_ref_dmdtda))
2268
    gdir.add_to_diagnostics('dmdtda_dynamic_calibration_error_scaling_factor',
2✔
2269
                            float(err_dmdtda_scaling_factor))
2270
    gdir.add_to_diagnostics('dmdtda_mismatch_dynamic_calibration',
2✔
2271
                            float(final_mismatch))
2272
    gdir.add_to_diagnostics('dmdtda_mismatch_with_initial_melt_f',
2✔
2273
                            float(mismatch_dmdtda[0]))
2274
    gdir.add_to_diagnostics('melt_f_dynamic_calibration', float(final_melt_f))
2✔
2275
    gdir.add_to_diagnostics('melt_f_before_dynamic_calibration',
2✔
2276
                            float(melt_f_initial))
2277
    gdir.add_to_diagnostics('run_dynamic_melt_f_calibration_iterations',
2✔
2278
                            int(dynamic_melt_f_calibration_runs[-1]))
2279

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

2282
    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