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

OGGM / oggm / 17036427554

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

Pull #1795

github

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

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

28 existing lines in 2 files now uncovered.

12404 of 14699 relevant lines covered (84.39%)

3.78 hits per line

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

79.5
/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, settings_filesuffix='',
9✔
31
                       init_model_filesuffix=None, init_model_yr=None,
32
                       init_model_fls=None,
33
                       climate_input_filesuffix='',
34
                       evolution_model=None,
35
                       mb_model_historical=None, mb_model_spinup=None,
36
                       spinup_period=20, spinup_start_yr=None,
37
                       min_spinup_period=10, spinup_start_yr_max=None,
38
                       target_yr=None, target_value=None,
39
                       minimise_for='area', precision_percent=1,
40
                       precision_absolute=1, min_ice_thickness=None,
41
                       first_guess_t_spinup=-2, t_spinup_max_step_length=2,
42
                       maxiter=30, output_filesuffix=None,
43
                       store_model_geometry=True, store_fl_diagnostics=None,
44
                       store_model_evolution=True, ignore_errors=False,
45
                       return_t_spinup_best=False, ye=None,
46
                       model_flowline_filesuffix='',
47
                       add_fixed_geometry_spinup=False, **kwargs):
48
    """Dynamically spinup the glacier to match area or volume at the RGI date.
49

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

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

206
    Returns
207
    -------
208
    :py:class:`oggm.core.flowline.evolution_model`
209
        The final dynamically spined-up model. Type depends on the selected
210
        evolution_model.
211
    """
212

213
    if output_filesuffix is None:
2✔
214
        output_filesuffix = settings_filesuffix
1✔
215

216
    evolution_model = decide_evolution_model(gdir=gdir,
2✔
217
                                             evolution_model=evolution_model)
218

219
    if target_yr is None:
2✔
220
        # Even in calendar dates, we prefer to set rgi_year in the next year
221
        # as the rgi is often from snow free images the year before (e.g. Aug)
222
        target_yr = gdir.rgi_date + 1
1✔
223

224
    if ye is None:
2✔
225
        ye = target_yr
1✔
226

227
    if ye < target_yr:
2✔
228
        raise RuntimeError(f'The provided end year (ye = {ye}) must be larger'
1✔
229
                           f'than the target year (target_yr = {target_yr}!')
230

231
    yr_min = gdir.get_climate_info()['baseline_yr_0']
2✔
232

233
    if min_ice_thickness is None:
2✔
234
        min_ice_thickness = gdir.settings['dynamic_spinup_min_ice_thick']
2✔
235

236
    # check provided maximum start year here, and change min_spinup_period
237
    if spinup_start_yr_max is not None:
2✔
238
        if spinup_start_yr_max < yr_min:
2✔
239
            raise RuntimeError(f'The provided maximum start year (= '
1✔
240
                               f'{spinup_start_yr_max}) must be larger than '
241
                               f'the start year of the provided climate data '
242
                               f'(= {yr_min})!')
243
        if spinup_start_yr is not None:
2✔
244
            if spinup_start_yr_max < spinup_start_yr:
2✔
245
                raise RuntimeError(f'The provided start year (= '
1✔
246
                                   f'{spinup_start_yr}) must be smaller than '
247
                                   f'the maximum start year '
248
                                   f'{spinup_start_yr_max}!')
249
        if (target_yr - spinup_start_yr_max) > min_spinup_period:
2✔
250
            min_spinup_period = (target_yr - spinup_start_yr_max)
×
251

252
    if init_model_filesuffix is not None:
2✔
253
        fp = gdir.get_filepath('model_geometry',
1✔
254
                               filesuffix=init_model_filesuffix)
255
        fmod = FileModel(fp)
1✔
256
        if init_model_yr is None:
1✔
257
            init_model_yr = fmod.last_yr
×
258
        fmod.run_until(init_model_yr)
1✔
259
        init_model_fls = fmod.fls
1✔
260

261
    if init_model_fls is None:
2✔
262
        fls_spinup = gdir.read_pickle('model_flowlines',
1✔
263
                                      filesuffix=model_flowline_filesuffix)
264
    else:
265
        fls_spinup = copy.deepcopy(init_model_fls)
2✔
266

267
    # MassBalance for actual run from yr_spinup to target_yr
268
    if mb_model_historical is None:
2✔
269
        mb_model_historical = MultipleFlowlineMassBalance(
2✔
270
            gdir, settings_filesuffix=settings_filesuffix,
271
            mb_model_class=MonthlyTIModel,
272
            filename='climate_historical',
273
            input_filesuffix=climate_input_filesuffix)
274

275
    # here we define the file-paths for the output
276
    if store_model_geometry:
2✔
277
        geom_path = gdir.get_filepath('model_geometry',
2✔
278
                                      filesuffix=output_filesuffix,
279
                                      delete=True)
280
    else:
281
        geom_path = False
1✔
282

283
    if store_fl_diagnostics is None:
2✔
284
        store_fl_diagnostics = gdir.settings['store_fl_diagnostics']
2✔
285

286
    if store_fl_diagnostics:
2✔
287
        fl_diag_path = gdir.get_filepath('fl_diagnostics',
1✔
288
                                         filesuffix=output_filesuffix,
289
                                         delete=True)
290
    else:
291
        fl_diag_path = False
1✔
292

293
    diag_path = gdir.get_filepath('model_diagnostics',
2✔
294
                                  filesuffix=output_filesuffix,
295
                                  delete=True)
296

297
    fs = gdir.settings['fs']
2✔
298
    glen_a = gdir.settings['glen_a']
2✔
299
    if gdir.settings['use_inversion_params_for_run']:
2✔
300
        # try-except statements for backwards-compatibility with gdir.get_diagnostics
301
        try:
2✔
302
            fs = gdir.settings['inversion_fs']
2✔
303
        except KeyError:
×
304
            pass  # if not available we stick with the default fs
×
305

306
        try:
2✔
307
            glen_a = gdir.settings['inversion_glen_a']
2✔
308
        except KeyError:
×
309
            pass  # if not available we stick with the default glen_a
×
310

311
    kwargs.setdefault('fs', fs)
2✔
312
    kwargs.setdefault('glen_a', glen_a)
2✔
313

314
    mb_elev_feedback = kwargs.get('mb_elev_feedback', 'annual')
2✔
315
    if mb_elev_feedback != 'annual':
2✔
316
        raise InvalidParamsError('Only use annual mb_elev_feedback with the '
1✔
317
                                 'dynamic spinup function!')
318

319
    if gdir.settings['use_kcalving_for_run']:
2✔
320
        raise InvalidParamsError('Dynamic spinup not tested with '
1✔
321
                                 "gdir.settings['use_kcalving_for_run'] is `True`!")
322

323
    # this function saves a model without conducting a dynamic spinup, but with
324
    # the provided output_filesuffix, so following tasks can find it.
325
    # This is necessary if target_yr < yr_min + 10 or if the dynamic spinup failed.
326
    def save_model_without_dynamic_spinup():
2✔
327
        gdir.settings['run_dynamic_spinup_success'] = False
1✔
328
        # this is only for backwards compatibility
329
        if settings_filesuffix == '':
1✔
330
            gdir.add_to_diagnostics('run_dynamic_spinup_success', False)
1✔
331
        yr_use = np.clip(target_yr, yr_min, None)
1✔
332
        model_dynamic_spinup_end = evolution_model(flowlines=fls_spinup,
1✔
333
                                                   mb_model=mb_model_historical,
334
                                                   y0=yr_use,
335
                                                   gdir=gdir,
336
                                                   settings_filesuffix=settings_filesuffix,
337
                                                   **kwargs)
338

339
        with warnings.catch_warnings():
1✔
340
            if ye < yr_use:
1✔
341
                yr_run = yr_use
×
342
            else:
343
                yr_run = ye
1✔
344
            # For operational runs we ignore the warnings
345
            warnings.filterwarnings('ignore', category=RuntimeWarning)
1✔
346
            model_dynamic_spinup_end.run_until_and_store(
1✔
347
                yr_run,
348
                geom_path=geom_path,
349
                diag_path=diag_path,
350
                fl_diag_path=fl_diag_path)
351

352
        return model_dynamic_spinup_end
1✔
353

354
    if target_yr < yr_min + min_spinup_period:
2✔
355
        log.warning('The provided rgi_date is smaller than yr_climate_start + '
1✔
356
                    'min_spinup_period, therefore no dynamic spinup is '
357
                    'conducted and the original flowlines are saved at the '
358
                    'provided target year or the start year of the provided '
359
                    'climate data (if yr_climate_start > target_yr)')
360
        if ignore_errors:
1✔
361
            model_dynamic_spinup_end = save_model_without_dynamic_spinup()
1✔
362
            if return_t_spinup_best:
1✔
363
                return model_dynamic_spinup_end, np.nan
×
364
            else:
365
                return model_dynamic_spinup_end
1✔
366
        else:
367
            raise RuntimeError('The difference between the rgi_date and the '
1✔
368
                               'start year of the climate data is too small to '
369
                               'run a dynamic spinup!')
370

371
    # here we define the flowline we want to match, it is assumed that during
372
    # the inversion the volume was calibrated towards the consensus estimate
373
    # (as it is by default), but this means the volume is matched on a
374
    # regional scale, maybe we could use here the individual glacier volume
375
    fls_ref = copy.deepcopy(fls_spinup)
2✔
376
    if minimise_for == 'area':
2✔
377
        unit = 'km2'
2✔
378
        other_variable = 'volume'
2✔
379
        other_unit = 'km3'
2✔
380
    elif minimise_for == 'volume':
1✔
381
        unit = 'km3'
1✔
382
        other_variable = 'area'
1✔
383
        other_unit = 'km2'
1✔
384
    else:
385
        raise NotImplementedError
386
    cost_var = f'{minimise_for}_{unit}'
2✔
387
    if target_value is None:
2✔
388
        reference_value = np.sum([getattr(f, cost_var) for f in fls_ref])
2✔
389
    else:
390
        reference_value = target_value
1✔
391
    other_reference_value = np.sum([getattr(f, f'{other_variable}_{other_unit}')
2✔
392
                                    for f in fls_ref])
393

394
    # if reference value is zero no dynamic spinup is possible
395
    if reference_value == 0.:
2✔
396
        if ignore_errors:
1✔
397
            model_dynamic_spinup_end = save_model_without_dynamic_spinup()
1✔
398
            if return_t_spinup_best:
1✔
399
                return model_dynamic_spinup_end, np.nan
×
400
            else:
401
                return model_dynamic_spinup_end
1✔
402
        else:
403
            raise RuntimeError('The given reference value is Zero, no dynamic '
1✔
404
                               'spinup possible!')
405

406
    # here we adjust the used precision_percent to make sure the resulting
407
    # absolute mismatch is smaller than precision_absolute
408
    precision_percent = min(precision_percent,
2✔
409
                            precision_absolute / reference_value * 100)
410

411
    # only used to check performance of function
412
    forward_model_runs = [0]
2✔
413

414
    # the actual spinup run
415
    def run_model_with_spinup_to_target_year(t_spinup):
2✔
416
        forward_model_runs.append(forward_model_runs[-1] + 1)
2✔
417

418
        # with t_spinup the glacier state after spinup is changed between iterations
419
        mb_model_spinup.temp_bias = t_spinup
2✔
420
        # run the spinup
421
        model_spinup = evolution_model(flowlines=copy.deepcopy(fls_spinup),
2✔
422
                                       mb_model=mb_model_spinup,
423
                                       y0=0,
424
                                       gdir=gdir,
425
                                       settings_filesuffix=settings_filesuffix,
426
                                       **kwargs)
427
        model_spinup.run_until(2 * halfsize_spinup)
2✔
428

429
        # if glacier is completely gone return information in ice-free
430
        ice_free = False
2✔
431
        if np.isclose(model_spinup.volume_km3, 0.):
2✔
432
            ice_free = True
1✔
433

434
        # Now conduct the actual model run to the rgi date
435
        model_historical = evolution_model(flowlines=model_spinup.fls,
2✔
436
                                           mb_model=mb_model_historical,
437
                                           y0=yr_spinup,
438
                                           gdir=gdir,
439
                                           settings_filesuffix=settings_filesuffix,
440
                                           **kwargs)
441
        if store_model_evolution:
2✔
442
            # check if we need to add the min_h variable (done inplace)
443
            delete_area_min_h = False
2✔
444
            ovars = gdir.settings['store_diagnostic_variables']
2✔
445
            if 'area_min_h' not in ovars:
2✔
446
                ovars += ['area_min_h']
2✔
447
                gdir.settings['store_diagnostic_variables'] = ovars
2✔
448
                delete_area_min_h = True
2✔
449

450
            ds = model_historical.run_until_and_store(
2✔
451
                ye,
452
                geom_path=geom_path,
453
                diag_path=diag_path,
454
                fl_diag_path=fl_diag_path,
455
                dynamic_spinup_min_ice_thick=min_ice_thickness,
456
                fixed_geometry_spinup_yr=fixed_geometry_spinup_yr)
457

458
            # now we delete the min_h variable again if it was not
459
            # included before (inplace)
460
            if delete_area_min_h:
2✔
461
                ovars.remove('area_min_h')
2✔
462
                gdir.settings['store_diagnostic_variables'] = ovars
2✔
463

464
            if type(ds) == tuple:
2✔
465
                ds = ds[0]
2✔
466
            model_area_km2 = ds.area_m2_min_h.loc[target_yr].values * 1e-6
2✔
467
            model_volume_km3 = ds.volume_m3.loc[target_yr].values * 1e-9
2✔
468
        else:
469
            # only run to rgi date and extract values
470
            model_historical.run_until(target_yr)
1✔
471
            fls = model_historical.fls
1✔
472
            model_area_km2 = np.sum(
1✔
473
                [np.sum(fl.bin_area_m2[fl.thick > min_ice_thickness])
474
                 for fl in fls]) * 1e-6
475
            model_volume_km3 = model_historical.volume_km3
1✔
476
            # afterwards finish the complete run
477
            model_historical.run_until(ye)
1✔
478

479
        if cost_var == 'area_km2':
2✔
480
            return model_area_km2, model_volume_km3, model_historical, ice_free
2✔
481
        elif cost_var == 'volume_km3':
1✔
482
            return model_volume_km3, model_area_km2, model_historical, ice_free
1✔
483
        else:
484
            raise NotImplementedError(f'{cost_var}')
485

486
    def cost_fct(t_spinup, model_dynamic_spinup_end_loc, other_variable_mismatch_loc):
2✔
487
        # actual model run
488
        model_value, other_value, model_dynamic_spinup, ice_free = \
2✔
489
            run_model_with_spinup_to_target_year(t_spinup)
490

491
        # save the final model for later
492
        model_dynamic_spinup_end_loc.append(copy.deepcopy(model_dynamic_spinup))
2✔
493

494
        # calculate the mismatch in percent
495
        cost = (model_value - reference_value) / reference_value * 100
2✔
496
        other_variable_mismatch_loc.append(
2✔
497
            (other_value - other_reference_value) / other_reference_value * 100)
498

499
        return cost, ice_free
2✔
500

501
    def init_cost_fct():
2✔
502
        model_dynamic_spinup_end_loc = []
2✔
503
        other_variable_mismatch_loc = []
2✔
504

505
        def c_fct(t_spinup):
2✔
506
            return cost_fct(t_spinup, model_dynamic_spinup_end_loc,
2✔
507
                            other_variable_mismatch_loc)
508

509
        return c_fct, model_dynamic_spinup_end_loc, other_variable_mismatch_loc
2✔
510

511
    def minimise_with_spline_fit(fct_to_minimise):
2✔
512
        # defines limits of t_spinup in accordance to maximal allowed change
513
        # between iterations
514
        t_spinup_limits = [first_guess_t_spinup - t_spinup_max_step_length,
2✔
515
                           first_guess_t_spinup + t_spinup_max_step_length]
516
        t_spinup_guess = []
2✔
517
        mismatch = []
2✔
518
        # this two variables indicate that the limits were already adapted to
519
        # avoid an ice_free or out_of_domain error
520
        was_ice_free = False
2✔
521
        was_out_of_domain = False
2✔
522
        was_errors = [was_out_of_domain, was_ice_free]
2✔
523

524
        def get_mismatch(t_spinup):
2✔
525
            t_spinup = copy.deepcopy(t_spinup)
2✔
526
            # first check if the new t_spinup is in limits
527
            if t_spinup < t_spinup_limits[0]:
2✔
528
                # was the smaller limit already executed, if not first do this
529
                if t_spinup_limits[0] not in t_spinup_guess:
1✔
530
                    t_spinup = copy.deepcopy(t_spinup_limits[0])
×
531
                else:
532
                    # smaller limit was already used, check if it was
533
                    # already newly defined with glacier exceeding domain
534
                    if was_errors[0]:
1✔
535
                        raise RuntimeError('Not able to minimise without '
×
536
                                           'exceeding the domain! Best '
537
                                           f'mismatch '
538
                                           f'{np.min(np.abs(mismatch))}%')
539
                    else:
540
                        # ok we set a new lower limit
541
                        t_spinup_limits[0] = (t_spinup_limits[0] -
1✔
542
                                              t_spinup_max_step_length)
543
            elif t_spinup > t_spinup_limits[1]:
2✔
544
                # was the larger limit already executed, if not first do this
545
                if t_spinup_limits[1] not in t_spinup_guess:
1✔
546
                    t_spinup = copy.deepcopy(t_spinup_limits[1])
1✔
547
                else:
548
                    # larger limit was already used, check if it was
549
                    # already newly defined with ice free glacier
550
                    if was_errors[1]:
1✔
551
                        raise RuntimeError('Not able to minimise without ice '
×
552
                                           'free glacier after spinup! Best '
553
                                           'mismatch '
554
                                           f'{np.min(np.abs(mismatch))}%')
555
                    else:
556
                        # ok we set a new upper limit
557
                        t_spinup_limits[1] = (t_spinup_limits[1] +
1✔
558
                                              t_spinup_max_step_length)
559

560
            # now clip t_spinup with limits
561
            t_spinup = np.clip(t_spinup, t_spinup_limits[0], t_spinup_limits[1])
2✔
562

563
            # now start with mismatch calculation
564

565
            # if error during spinup (ice_free or out_of_domain) this defines
566
            # how much t_spinup is changed to look for an error free glacier spinup
567
            t_spinup_search_change = t_spinup_max_step_length / 10
2✔
568
            # maximum number of changes to look for an error free glacier
569
            max_iterations = int(t_spinup_max_step_length / t_spinup_search_change)
2✔
570
            is_out_of_domain = True
2✔
571
            is_ice_free_spinup = True
2✔
572
            is_ice_free_end = True
2✔
573
            is_first_guess_ice_free = False
2✔
574
            is_first_guess_out_of_domain = False
2✔
575
            doing_first_guess = (len(mismatch) == 0)
2✔
576
            define_new_lower_limit = False
2✔
577
            define_new_upper_limit = False
2✔
578
            iteration = 0
2✔
579

580
            while ((is_out_of_domain | is_ice_free_spinup | is_ice_free_end) &
2✔
581
                   (iteration < max_iterations)):
582
                try:
2✔
583
                    tmp_mismatch, is_ice_free_spinup = fct_to_minimise(t_spinup)
2✔
584

585
                    # no error occurred, so we are not outside the domain
586
                    is_out_of_domain = False
2✔
587

588
                    # check if we are ice_free after spinup, if so we search
589
                    # for a new upper limit for t_spinup
590
                    if is_ice_free_spinup:
2✔
591
                        was_errors[1] = True
1✔
592
                        define_new_upper_limit = True
1✔
593
                        # special treatment if it is the first guess
594
                        if np.isclose(t_spinup, first_guess_t_spinup) & \
1✔
595
                                doing_first_guess:
596
                            is_first_guess_ice_free = True
1✔
597
                            # here directly jump to the smaller limit
598
                            t_spinup = copy.deepcopy(t_spinup_limits[0])
1✔
599
                        elif is_first_guess_ice_free & doing_first_guess:
1✔
600
                            # make large steps if it is first guess
601
                            t_spinup = t_spinup - t_spinup_max_step_length
1✔
602
                        else:
603
                            t_spinup = np.round(t_spinup - t_spinup_search_change,
×
604
                                                decimals=1)
605
                        if np.isclose(t_spinup, t_spinup_guess).any():
1✔
606
                            iteration = copy.deepcopy(max_iterations)
×
607

608
                    # check if we are ice_free at the end of the model run, if
609
                    # so we use the lower t_spinup limit and change the limit if
610
                    # needed
611
                    elif np.isclose(tmp_mismatch, -100.):
2✔
612
                        is_ice_free_end = True
×
613
                        was_errors[1] = True
×
614
                        define_new_upper_limit = True
×
615
                        # special treatment if it is the first guess
616
                        if np.isclose(t_spinup, first_guess_t_spinup) & \
×
617
                                doing_first_guess:
618
                            is_first_guess_ice_free = True
×
619
                            # here directly jump to the smaller limit
620
                            t_spinup = copy.deepcopy(t_spinup_limits[0])
×
621
                        elif is_first_guess_ice_free & doing_first_guess:
×
622
                            # make large steps if it is first guess
623
                            t_spinup = t_spinup - t_spinup_max_step_length
×
624
                        else:
625
                            # if lower limit was already used change it and use
626
                            if t_spinup == t_spinup_limits[0]:
×
627
                                t_spinup_limits[0] = (t_spinup_limits[0] -
×
628
                                                      t_spinup_max_step_length)
629
                                t_spinup = copy.deepcopy(t_spinup_limits[0])
×
630
                            else:
631
                                # otherwise just try with a colder t_spinup
632
                                t_spinup = np.round(t_spinup - t_spinup_search_change,
×
633
                                                    decimals=1)
634

635
                    else:
636
                        is_ice_free_end = False
2✔
637

638
                except RuntimeError as e:
1✔
639
                    # check if glacier grow to large
640
                    if 'Glacier exceeds domain boundaries, at year:' in f'{e}':
1✔
641
                        # ok we where outside the domain, therefore we search
642
                        # for a new lower limit for t_spinup in 0.1 °C steps
643
                        is_out_of_domain = True
1✔
644
                        define_new_lower_limit = True
1✔
645
                        was_errors[0] = True
1✔
646
                        # special treatment if it is the first guess
647
                        if np.isclose(t_spinup, first_guess_t_spinup) & \
1✔
648
                                doing_first_guess:
649
                            is_first_guess_out_of_domain = True
1✔
650
                            # here directly jump to the larger limit
651
                            t_spinup = t_spinup_limits[1]
1✔
652
                        elif is_first_guess_out_of_domain & doing_first_guess:
1✔
653
                            # make large steps if it is first guess
654
                            t_spinup = t_spinup + t_spinup_max_step_length
1✔
655
                        else:
656
                            t_spinup = np.round(t_spinup + t_spinup_search_change,
1✔
657
                                                decimals=1)
658
                        if np.isclose(t_spinup, t_spinup_guess).any():
1✔
659
                            iteration = copy.deepcopy(max_iterations)
×
660

661
                    else:
662
                        # otherwise this error can not be handled here
663
                        raise RuntimeError(e)
×
664

665
                iteration += 1
2✔
666

667
            if iteration >= max_iterations:
2✔
668
                # ok we were not able to find an mismatch without error
669
                # (ice_free or out of domain), so we try to raise an descriptive
670
                # RuntimeError
671
                if len(mismatch) == 0:
1✔
672
                    # unfortunately we were not able conduct one single error
673
                    # free run
674
                    msg = 'Not able to conduct one error free run. Error is '
1✔
675
                    if is_first_guess_ice_free:
1✔
676
                        msg += f'"ice_free" with last t_spinup of {t_spinup}.'
1✔
677
                    elif is_first_guess_out_of_domain:
1✔
678
                        msg += f'"out_of_domain" with last t_spinup of {t_spinup}.'
1✔
679
                    else:
680
                        raise RuntimeError('Something unexpected happened!')
×
681

682
                    raise RuntimeError(msg)
1✔
683

684
                elif define_new_lower_limit:
×
685
                    raise RuntimeError('Not able to minimise without '
×
686
                                       'exceeding the domain! Best '
687
                                       f'mismatch '
688
                                       f'{np.min(np.abs(mismatch))}%')
689
                elif define_new_upper_limit:
×
690
                    raise RuntimeError('Not able to minimise without ice '
×
691
                                       'free glacier after spinup! Best mismatch '
692
                                       f'{np.min(np.abs(mismatch))}%')
693
                elif is_ice_free_end:
×
694
                    raise RuntimeError('Not able to find a t_spinup so that '
×
695
                                       'glacier is not ice free at the end! '
696
                                       '(Last t_spinup '
697
                                       f'{t_spinup + t_spinup_max_step_length} °C)')
698
                else:
699
                    raise RuntimeError('Something unexpected happened during '
×
700
                                       'definition of new t_spinup limits!')
701
            else:
702
                # if we found a new limit set it
703
                if define_new_upper_limit & define_new_lower_limit:
2✔
704
                    # we can end here if we are at the first guess and took
705
                    # a to large step
706
                    was_errors[0] = False
×
707
                    was_errors[1] = False
×
708
                    if t_spinup <= t_spinup_limits[0]:
×
709
                        t_spinup_limits[0] = t_spinup
×
710
                        t_spinup_limits[1] = (t_spinup_limits[0] +
×
711
                                              t_spinup_max_step_length)
712
                    elif t_spinup >= t_spinup_limits[1]:
×
713
                        t_spinup_limits[1] = t_spinup
×
714
                        t_spinup_limits[0] = (t_spinup_limits[1] -
×
715
                                              t_spinup_max_step_length)
716
                    else:
717
                        if is_first_guess_ice_free:
×
718
                            t_spinup_limits[1] = t_spinup
×
719
                        elif is_out_of_domain:
×
720
                            t_spinup_limits[0] = t_spinup
×
721
                        else:
722
                            raise RuntimeError('I have not expected to get here!')
×
723
                elif define_new_lower_limit:
2✔
724
                    t_spinup_limits[0] = copy.deepcopy(t_spinup)
1✔
725
                    if t_spinup >= t_spinup_limits[1]:
1✔
726
                        # this happens when the first guess was out of domain
727
                        was_errors[0] = False
1✔
728
                        t_spinup_limits[1] = (t_spinup_limits[0] +
1✔
729
                                              t_spinup_max_step_length)
730
                elif define_new_upper_limit:
2✔
731
                    t_spinup_limits[1] = copy.deepcopy(t_spinup)
×
732
                    if t_spinup <= t_spinup_limits[0]:
×
733
                        # this happens when the first guess was ice free
734
                        was_errors[1] = False
×
735
                        t_spinup_limits[0] = (t_spinup_limits[1] -
×
736
                                              t_spinup_max_step_length)
737

738
            return float(tmp_mismatch), float(t_spinup)
2✔
739

740
        # first guess
741
        new_mismatch, new_t_spinup = get_mismatch(first_guess_t_spinup)
2✔
742
        t_spinup_guess.append(new_t_spinup)
2✔
743
        mismatch.append(new_mismatch)
2✔
744

745
        if abs(mismatch[-1]) < precision_percent:
2✔
746
            return t_spinup_guess, mismatch
2✔
747

748
        # second (arbitrary) guess is given depending on the outcome of first
749
        # guess, when mismatch is 100% t_spinup is changed for
750
        # t_spinup_max_step_length, but at least the second guess is 0.2 °C away
751
        # from the first guess
752
        step = np.sign(mismatch[-1]) * max(np.abs(mismatch[-1]) *
2✔
753
                                           t_spinup_max_step_length / 100,
754
                                           0.2)
755
        new_mismatch, new_t_spinup = get_mismatch(t_spinup_guess[0] + step)
2✔
756
        t_spinup_guess.append(new_t_spinup)
2✔
757
        mismatch.append(new_mismatch)
2✔
758

759
        if abs(mismatch[-1]) < precision_percent:
2✔
760
            return t_spinup_guess, mismatch
2✔
761

762
        # Now start with splin fit for guessing
763
        while len(t_spinup_guess) < maxiter:
2✔
764
            # get next guess from splin (fit partial linear function to previously
765
            # calculated (mismatch, t_spinup) pairs and get t_spinup value where
766
            # mismatch=0 from this fitted curve)
767
            sort_index = np.argsort(np.array(mismatch))
2✔
768
            tck = interpolate.splrep(np.array(mismatch)[sort_index],
2✔
769
                                     np.array(t_spinup_guess)[sort_index],
770
                                     k=1)
771
            # here we catch interpolation errors (two different t_spinup with
772
            # same mismatch), could happen if one t_spinup was close to a newly
773
            # defined limit
774
            if np.isnan(tck[1]).any():
2✔
775
                if was_errors[0]:
×
776
                    raise RuntimeError('Not able to minimise without '
×
777
                                       'exceeding the domain! Best '
778
                                       f'mismatch '
779
                                       f'{np.min(np.abs(mismatch))}%')
780
                elif was_errors[1]:
×
781
                    raise RuntimeError('Not able to minimise without ice '
×
782
                                       'free glacier! Best mismatch '
783
                                       f'{np.min(np.abs(mismatch))}%')
784
                else:
785
                    raise RuntimeError('Not able to minimise! Problem is '
×
786
                                       'unknown, need to check by hand! Best '
787
                                       'mismatch '
788
                                       f'{np.min(np.abs(mismatch))}%')
789
            new_mismatch, new_t_spinup = get_mismatch(float(interpolate.splev(0,
2✔
790
                                                                            tck)
791
                                                          ))
792
            t_spinup_guess.append(new_t_spinup)
2✔
793
            mismatch.append(new_mismatch)
2✔
794

795
            if abs(mismatch[-1]) < precision_percent:
2✔
796
                return t_spinup_guess, mismatch
2✔
797

798
        # Ok when we end here the spinup could not find satisfying match after
799
        # maxiter(ations)
800
        raise RuntimeError(f'Could not find mismatch smaller '
1✔
801
                           f'{precision_percent}% (only '
802
                           f'{np.min(np.abs(mismatch))}%) in {maxiter}'
803
                           f'Iterations!')
804

805
    # define function for the actual minimisation
806
    c_fun, model_dynamic_spinup_end, other_variable_mismatch = init_cost_fct()
2✔
807

808
    # define the MassBalanceModels for different spinup periods and try to
809
    # minimise, if minimisation fails a shorter spinup period is used
810
    # (first a spinup period between initial period and 'min_spinup_period'
811
    # years and the second try is to use a period of 'min_spinup_period' years,
812
    # if it still fails the actual error is raised)
813
    if spinup_start_yr is not None:
2✔
814
        spinup_period_initial = min(target_yr - spinup_start_yr,
2✔
815
                                    target_yr - yr_min)
816
    else:
817
        spinup_period_initial = min(spinup_period, target_yr - yr_min)
1✔
818
    if spinup_period_initial <= min_spinup_period:
2✔
819
        spinup_periods_to_try = [min_spinup_period]
1✔
820
    else:
821
        # try out a maximum of three different spinup_periods
822
        spinup_periods_to_try = [spinup_period_initial,
2✔
823
                                 int((spinup_period_initial +
824
                                      min_spinup_period) / 2),
825
                                 min_spinup_period]
826
    # after defining the initial spinup period we can define the year for the
827
    # fixed_geometry_spinup
828
    if add_fixed_geometry_spinup:
2✔
829
        fixed_geometry_spinup_yr = target_yr - spinup_period_initial
2✔
830
    else:
831
        fixed_geometry_spinup_yr = None
1✔
832

833
    # check if the user provided an mb_model_spinup, otherwise we must define a
834
    # new one each iteration
835
    provided_mb_model_spinup = False
2✔
836
    if mb_model_spinup is not None:
2✔
837
        provided_mb_model_spinup = True
×
838

839
    for spinup_period in spinup_periods_to_try:
2✔
840
        yr_spinup = target_yr - spinup_period
2✔
841

842
        if not provided_mb_model_spinup:
2✔
843
            # define spinup MassBalance
844
            # spinup is running for 'target_yr - yr_spinup' years, using a
845
            # ConstantMassBalance
846
            y0_spinup = (yr_spinup + target_yr) / 2
2✔
847
            halfsize_spinup = target_yr - y0_spinup
2✔
848
            mb_model_spinup = MultipleFlowlineMassBalance(
2✔
849
                gdir, settings_filesuffix=settings_filesuffix,
850
                mb_model_class=ConstantMassBalance,
851
                filename='climate_historical',
852
                input_filesuffix=climate_input_filesuffix, y0=y0_spinup,
853
                halfsize=halfsize_spinup)
854

855
        # try to conduct minimisation, if an error occurred try shorter spinup
856
        # period
857
        try:
2✔
858
            final_t_spinup_guess, final_mismatch = minimise_with_spline_fit(c_fun)
2✔
859
            # ok no error occurred so we succeeded
860
            break
2✔
861
        except RuntimeError as e:
1✔
862
            # if the last spinup period was min_spinup_period the dynamic
863
            # spinup failed
864
            if spinup_period == min_spinup_period:
1✔
865
                log.warning('No dynamic spinup could be conducted and the '
1✔
866
                            'original model with no spinup is saved using the '
867
                            f'provided output_filesuffix "{output_filesuffix}". '
868
                            f'The error message of the dynamic spinup is: {e}')
869
                if ignore_errors:
1✔
870
                    model_dynamic_spinup_end = save_model_without_dynamic_spinup()
1✔
871
                    if return_t_spinup_best:
1✔
872
                        return model_dynamic_spinup_end, np.nan
×
873
                    else:
874
                        return model_dynamic_spinup_end
1✔
875
                else:
876
                    # delete all files which could be saved during the previous
877
                    # iterations
878
                    if geom_path and os.path.exists(geom_path):
1✔
879
                        os.remove(geom_path)
1✔
880

881
                    if fl_diag_path and os.path.exists(fl_diag_path):
1✔
882
                        os.remove(fl_diag_path)
×
883

884
                    if diag_path and os.path.exists(diag_path):
1✔
885
                        os.remove(diag_path)
1✔
886

887
                    raise RuntimeError(e)
1✔
888

889
    # hurray, dynamic spinup successfully
890
    # safe some stuff for diagnostics
891
    diag_dyn = {
2✔
892
        'run_dynamic_spinup_success': True,
893
        'temp_bias_dynamic_spinup': float(final_t_spinup_guess[-1]),
894
        'dynamic_spinup_target_year': int(target_yr),
895
        'dynamic_spinup_period': int(spinup_period),
896
        'dynamic_spinup_forward_model_iterations': int(forward_model_runs[-1]),
897
        f'{minimise_for}_mismatch_dynamic_spinup_{unit}_percent':
898
            float(final_mismatch[-1]),
899
        f'reference_{minimise_for}_dynamic_spinup_{unit}':
900
            float(reference_value),
901
        'dynamic_spinup_other_variable_reference': float(other_reference_value),
902
        'dynamic_spinup_mismatch_other_variable_percent':
903
            float(other_variable_mismatch[-1]),
904
    }
905

906
    for k, v in diag_dyn.items():
2✔
907
        gdir.settings[k] = v
2✔
908
        # this is only for backwards compatibility
909
        if settings_filesuffix == '':
2✔
910
            gdir.add_to_diagnostics(k, v)
2✔
911

912
    # here only save the final model state if store_model_evolution = False
913
    if not store_model_evolution:
2✔
914
        with warnings.catch_warnings():
1✔
915
            # For operational runs we ignore the warnings
916
            warnings.filterwarnings('ignore', category=RuntimeWarning)
1✔
917
            model_dynamic_spinup_end[-1].run_until_and_store(
1✔
918
                target_yr,
919
                geom_path=geom_path,
920
                diag_path=diag_path,
921
                fl_diag_path=fl_diag_path, )
922

923
    if return_t_spinup_best:
2✔
924
        return model_dynamic_spinup_end[-1], final_t_spinup_guess[-1]
2✔
925
    else:
926
        return model_dynamic_spinup_end[-1]
1✔
927

928

929
def define_new_melt_f_in_gdir(gdir, new_melt_f):
9✔
930
    """
931
    Helper function to define a new melt_f in a gdir. Is used inside the run
932
    functions of the dynamic melt_f calibration.
933

934
    Parameters
935
    ----------
936
    gdir : :py:class:`oggm.GlacierDirectory`
937
        the glacier directory to change the melt_f
938
    new_melt_f : float
939
        the new melt_f to save in the gdir
940

941
    Returns
942
    -------
943

944
    """
945
    if 'melt_f' not in gdir.settings:
2✔
946
        raise InvalidWorkflowError('No `melt_f` in gdir.settings. '
×
947
                                   'You first need to calibrate the whole '
948
                                   'MassBalanceModel before changing melt_f '
949
                                   'alone!')
950
    gdir.settings['melt_f'] = new_melt_f
2✔
951

952

953
def dynamic_melt_f_run_with_dynamic_spinup(
9✔
954
        gdir, melt_f, yr0_ref_mb, yr1_ref_mb, fls_init, ys, ye,
955
        settings_filesuffix='', output_filesuffix=None, evolution_model=None,
956
        mb_model_historical=None, mb_model_spinup=None,
957
        model_flowline_filesuffix='',
958
        minimise_for='area', climate_input_filesuffix='', spinup_period=20,
959
        min_spinup_period=10, target_yr=None, precision_percent=1,
960
        precision_absolute=1, min_ice_thickness=None,
961
        first_guess_t_spinup=-2, t_spinup_max_step_length=2, maxiter=30,
962
        store_model_geometry=True, store_fl_diagnostics=None,
963
        local_variables=None, set_local_variables=False, do_inversion=True,
964
        spinup_start_yr_max=None, add_fixed_geometry_spinup=True,
965
        **kwargs):
966
    """
967
    This function is one option for a 'run_function' for the
968
    'run_dynamic_melt_f_calibration' function (the corresponding
969
    'fallback_function' is
970
    'dynamic_melt_f_run_with_dynamic_spinup_fallback'). This
971
    function defines a new melt_f in the glacier directory and conducts an
972
    inversion calibrating A to match 'ref_volume_m3' with this new melt_f
973
    ('calibrate_inversion_from_volume'). Afterwards a dynamic spinup is
974
    conducted to match 'minimise_for' (for more info look at docstring of
975
    'run_dynamic_spinup'). And in the end the geodetic mass balance of the
976
    current run is calculated (between the period [yr0_ref_mb, yr1_ref_mb]) and
977
    returned.
978

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

1110
    Returns
1111
    -------
1112
    :py:class:`oggm.core.flowline.evolution_model`, float
1113
        The final model after the run and the calculated geodetic mass balance
1114
    """
1115

1116
    if output_filesuffix is None:
2✔
1117
        output_filesuffix = settings_filesuffix
2✔
1118

1119
    evolution_model = decide_evolution_model(gdir=gdir,
2✔
1120
                                             evolution_model=evolution_model)
1121

1122
    if not isinstance(local_variables, dict):
2✔
1123
        raise ValueError('You must provide a dict for local_variables!')
1✔
1124

1125
    from oggm.workflow import calibrate_inversion_from_volume
2✔
1126

1127
    if set_local_variables:
2✔
1128
        # clear the provided dictionary and set the first elements
1129
        local_variables.clear()
2✔
1130
        local_variables['t_spinup'] = [first_guess_t_spinup]
2✔
1131

1132
        # for backward compatiblitiy, we check if volume is part of the
1133
        # observations file. If not we through a warning and add it
1134
        if 'ref_volume_m3' not in gdir.observations:
2✔
1135
            log.warning("You seem to be using 'older' preprocessed directories "
1✔
1136
                        "with a more recent version of OGGM. While this is "
1137
                        "possible be aware that the handling of observations "
1138
                        "has changed. TODO: add link once new OGGM is released")
1139
            ref_volume_m3 = {}
1✔
1140
            fls_ref = gdir.read_pickle('model_flowlines',
1✔
1141
                                       filesuffix=model_flowline_filesuffix)
1142
            ref_volume_m3['value'] = np.sum([f.volume_m3 for f in fls_ref])
1✔
1143
            ref_volume_m3['year'] = gdir.rgi_date
1✔
1144
            gdir.observations['ref_volume_m3'] = ref_volume_m3
1✔
1145

1146
        # we are done with preparing the local_variables for the upcoming iterations
1147
        return None
2✔
1148

1149
    if target_yr is None:
2✔
1150
        target_yr = gdir.rgi_date + 1  # + 1 converted to hydro years
×
1151
    if min_spinup_period > target_yr - ys:
2✔
1152
        log.info('The target year is closer to ys as the minimum spinup '
×
1153
                 'period -> therefore the minimum spinup period is '
1154
                 'adapted and it is the only period which is tried by the '
1155
                 'dynamic spinup function!')
1156
        min_spinup_period = target_yr - ys
×
1157
        spinup_period = target_yr - ys
×
1158

1159
    if spinup_start_yr_max is None:
2✔
1160
        spinup_start_yr_max = yr0_ref_mb
2✔
1161

1162
    if spinup_start_yr_max > yr0_ref_mb:
2✔
1163
        log.info('The provided maximum start year is larger then the '
×
1164
                 'start year of the geodetic period, therefore it will be '
1165
                 'set to the start year of the geodetic period!')
1166
        spinup_start_yr_max = yr0_ref_mb
×
1167

1168
    # check that inversion is only possible without providing own fls
1169
    if do_inversion:
2✔
1170
        if not np.all([np.all(getattr(fl_prov, 'surface_h') ==
2✔
1171
                              getattr(fl_orig, 'surface_h')) and
1172
                       np.all(getattr(fl_prov, 'bed_h') ==
1173
                              getattr(fl_orig, 'bed_h'))
1174
                       for fl_prov, fl_orig in
1175
                       zip(fls_init,
1176
                           gdir.read_pickle(
1177
                               'model_flowlines',
1178
                               filesuffix=model_flowline_filesuffix))]):
1179
            raise InvalidWorkflowError('If you want to perform a dynamic '
×
1180
                                       'melt_f calibration including an '
1181
                                       'inversion, it is not possible to '
1182
                                       'provide your own flowlines! (fls_init '
1183
                                       'should be None or '
1184
                                       'the original model_flowlines)')
1185

1186
    # Here we start with the actual model run
1187
    if melt_f == gdir.settings['melt_f']:
2✔
1188
        # we do not need to define a new melt_f or do an inversion
1189
        do_inversion = False
2✔
1190
    else:
1191
        define_new_melt_f_in_gdir(gdir, melt_f)
2✔
1192

1193
    if do_inversion:
2✔
1194
        with utils.DisableLogger():
2✔
1195
            apparent_mb_from_any_mb(gdir,
2✔
1196
                                    settings_filesuffix=settings_filesuffix,
1197
                                    add_to_log_file=False,  # dont write to log
1198
                                    )
1199
            # do inversion with A calibration to current volume
1200
            calibrate_inversion_from_volume(
2✔
1201
                [gdir], settings_filesuffix=settings_filesuffix,
1202
                apply_fs_on_mismatch=True, error_on_mismatch=False,
1203
                filter_inversion_output=True,
1204
                ref_volume_m3=gdir.observations['ref_volume_m3']['value'],
1205
                ref_volume_year=gdir.observations['ref_volume_m3']['year'],
1206
                add_to_log_file=False)
1207

1208
    model_flowline_filesuffix = f'{model_flowline_filesuffix}_dyn_melt_f_calib'
2✔
1209
    init_present_time_glacier(gdir, settings_filesuffix=settings_filesuffix,
2✔
1210
                              output_filesuffix=model_flowline_filesuffix,
1211
                              add_to_log_file=False)
1212

1213
    # Now do a dynamic spinup to match area
1214
    # do not ignore errors in dynamic spinup, so all 'bad'/intermediate files
1215
    # are deleted in run_dynamic_spinup function
1216
    try:
2✔
1217
        model, last_best_t_spinup = run_dynamic_spinup(
2✔
1218
            gdir,
1219
            settings_filesuffix=settings_filesuffix,
1220
            continue_on_error=False,  # force to raise an error in @entity_task
1221
            add_to_log_file=False,  # dont write to log file in @entity_task
1222
            init_model_fls=fls_init,
1223
            climate_input_filesuffix=climate_input_filesuffix,
1224
            evolution_model=evolution_model,
1225
            mb_model_historical=mb_model_historical,
1226
            mb_model_spinup=mb_model_spinup,
1227
            spinup_period=spinup_period,
1228
            spinup_start_yr=ys,
1229
            spinup_start_yr_max=spinup_start_yr_max,
1230
            min_spinup_period=min_spinup_period, target_yr=target_yr,
1231
            precision_percent=precision_percent,
1232
            precision_absolute=precision_absolute,
1233
            min_ice_thickness=min_ice_thickness,
1234
            t_spinup_max_step_length=t_spinup_max_step_length,
1235
            maxiter=maxiter,
1236
            minimise_for=minimise_for,
1237
            first_guess_t_spinup=local_variables['t_spinup'][-1],
1238
            output_filesuffix=output_filesuffix,
1239
            store_model_evolution=True, ignore_errors=False,
1240
            return_t_spinup_best=True, ye=ye,
1241
            store_model_geometry=store_model_geometry,
1242
            store_fl_diagnostics=store_fl_diagnostics,
1243
            model_flowline_filesuffix=model_flowline_filesuffix,
1244
            add_fixed_geometry_spinup=add_fixed_geometry_spinup,
1245
            **kwargs)
1246
        # save the temperature bias which was successful in the last iteration
1247
        # as we expect we are not so far away in the next iteration (only
1248
        # needed for optimisation, potentially need less iterations in
1249
        # run_dynamic_spinup)
1250
        local_variables['t_spinup'].append(last_best_t_spinup)
2✔
1251
    except RuntimeError as e:
1✔
1252
        raise RuntimeError(f'Dynamic spinup raised error! (Message: {e})')
1✔
1253

1254
    # calculate dmdtda from previous simulation here
1255
    with utils.DisableLogger():
2✔
1256
        ds = utils.compile_run_output(gdir, input_filesuffix=output_filesuffix,
2✔
1257
                                      path=False)
1258
    dmdtda_mdl = ((ds.volume.loc[yr1_ref_mb].values[0] -
2✔
1259
                   ds.volume.loc[yr0_ref_mb].values[0]) /
1260
                  gdir.rgi_area_m2 /
1261
                  (yr1_ref_mb - yr0_ref_mb) *
1262
                  gdir.settings['ice_density'])
1263

1264
    return model, dmdtda_mdl
2✔
1265

1266

1267
def dynamic_melt_f_run_with_dynamic_spinup_fallback(
9✔
1268
        gdir, melt_f, fls_init, ys, ye,
1269
        settings_filesuffix='', output_filesuffix=None,
1270
        evolution_model=None, minimise_for='area',
1271
        mb_model_historical=None, mb_model_spinup=None,
1272
        climate_input_filesuffix='', spinup_period=20, min_spinup_period=10,
1273
        target_yr=None, precision_percent=1,
1274
        precision_absolute=1, min_ice_thickness=None,
1275
        first_guess_t_spinup=-2, t_spinup_max_step_length=2, maxiter=30,
1276
        store_model_geometry=True, store_fl_diagnostics=None,
1277
        do_inversion=True, spinup_start_yr_max=None,
1278
        add_fixed_geometry_spinup=True, **kwargs):
1279
    """
1280
    This is the fallback function corresponding to the function
1281
    'dynamic_melt_f_run_with_dynamic_spinup', which are provided
1282
    to 'run_dynamic_melt_f_calibration'. It is used if the run_function fails and
1283
    if 'ignore_error == True' in 'run_dynamic_melt_f_calibration'. First it resets
1284
    melt_f of gdir. Afterwards it tries to conduct a dynamic spinup. If this
1285
    also fails the last thing is to just do a run without a dynamic spinup
1286
    (only a fixed geometry spinup).
1287

1288
    Parameters
1289
    ----------
1290
    gdir : :py:class:`oggm.GlacierDirectory`
1291
        the glacier directory to process
1292
    melt_f : float
1293
        the melt_f used for this run
1294
    fls_init : []
1295
        List of flowlines to use to initialise the model
1296
    ys : int
1297
        start year of the run
1298
    ye : int
1299
        end year of the run
1300
    settings_filesuffix: str
1301
        You can use a different set of settings by providing a filesuffix. This
1302
        is useful for sensitivity experiments.
1303
    output_filesuffix : str
1304
        For the output file. If None the settings_filesuffix will be used.
1305
        Default is None
1306
    evolution_model : :class:oggm.core.FlowlineModel
1307
        which evolution model to use. Default: gdir.settings['evolution_model']
1308
        Not all models work in all circumstances!
1309
    mb_model_historical : :py:class:`core.MassBalanceModel`
1310
        User-povided MassBalanceModel instance for the historical run. Default
1311
        is to use a MonthlyTIModel model  together with the provided
1312
        parameter climate_input_filesuffix.
1313
    mb_model_spinup : :py:class:`core.MassBalanceModel`
1314
        User-povided MassBalanceModel instance for the spinup before the
1315
        historical run. Default is to use a ConstantMassBalance model together
1316
        with the provided parameter climate_input_filesuffix and during the
1317
        period of spinup_start_yr until rgi_year (e.g. 1979 - 2000).
1318
    minimise_for : str
1319
        The variable we want to match at target_yr. Options are 'area' or
1320
        'volume'.
1321
        Default is 'area'.
1322
    climate_input_filesuffix : str
1323
        filesuffix for the input climate file
1324
        Default is ''
1325
    spinup_period : int
1326
        The period how long the spinup should run. Start date of historical run
1327
        is defined "target_yr - spinup_period". Minimum allowed value is
1328
        defined with 'min_spinup_period'. If the provided climate data starts
1329
        at year later than (target_yr - spinup_period) the spinup_period is set
1330
        to (target_yr - yr_climate_start). Caution if spinup_start_yr is set
1331
        the spinup_period is ignored.
1332
        Default is 20
1333
    min_spinup_period : int
1334
        If the dynamic spinup function fails with the initial 'spinup_period'
1335
        a shorter period is tried. Here you can define the minimum period to
1336
        try.
1337
        Default is 10
1338
    target_yr : int or None
1339
        The rgi date, at which we want to match area or volume.
1340
        If None, gdir.rgi_date + 1 is used (the default).
1341
        Default is None
1342
    precision_percent : float
1343
        Gives the precision we want to match for the selected variable
1344
        ('minimise_for') at rgi_date in percent. The algorithm makes sure that
1345
        the resulting relative mismatch is smaller than precision_percent, but
1346
        also that the absolute value is smaller than precision_absolute.
1347
        Default is 1, meaning the difference must be within 1% of the given
1348
        value (area or volume).
1349
    precision_absolute : float
1350
        Gives an minimum absolute value to match. The algorithm makes sure that
1351
        the resulting relative mismatch is smaller than
1352
        precision_percent, but also that the absolute value is
1353
        smaller than precision_absolute.
1354
        The unit of precision_absolute depends on minimise_for (if 'area' in
1355
        km2, if 'volume' in km3)
1356
        Default is 1.
1357
    min_ice_thickness : float
1358
        Gives an minimum ice thickness for model grid points which are counted
1359
        to the total model value. This could be useful to filter out seasonal
1360
        'glacier growth', as OGGM do not differentiate between snow and ice in
1361
        the forward model run. Therefore you could see quite fast changes
1362
        (spikes) in the time-evolution (especially visible in length and area).
1363
        If you set this value to 0 the filtering can be switched off.
1364
        Default is gdir.settings['dynamic_spinup_min_ice_thick'].
1365
    first_guess_t_spinup : float
1366
        The initial guess for the temperature bias for the spinup
1367
        MassBalanceModel in °C.
1368
        Default is -2.
1369
    t_spinup_max_step_length : float
1370
        Defines the maximums allowed change of t_spinup between two iterations. Is
1371
        needed to avoid to large changes.
1372
        Default is 2
1373
    maxiter : int
1374
        Maximum number of minimisation iterations per dynamic spinup where area
1375
        or volume is tried to be matched. If reached and 'ignore_errors=False'
1376
        an error is raised.
1377
        Default is 30
1378
    store_model_geometry : bool
1379
        whether to store the full model geometry run file to disk or not.
1380
        Default is True
1381
    store_fl_diagnostics : bool or None
1382
        Whether to store the model flowline diagnostics to disk or not.
1383
        Default is None (-> gdir.settings['store_fl_diagnostics'])
1384
    do_inversion : bool
1385
        If True a complete inversion is conducted using the provided melt_f
1386
        before the actual fallback run.
1387
        Default is False
1388
    spinup_start_yr_max : int or None
1389
        Possibility to provide a maximum year where the dynamic spinup must
1390
        start from at least. If set, this overrides the min_spinup_period if
1391
        target_yr - spinup_start_yr_max > min_spinup_period.
1392
        Default is None
1393
    add_fixed_geometry_spinup : bool
1394
        If True and the original spinup_period of the dynamical spinup must be
1395
        shortened (due to ice-free or out-of-boundary error) a
1396
        fixed-geometry-spinup is added at the beginning so that the resulting
1397
        model run always starts from ys.
1398
        Default is True
1399
    kwargs : dict
1400
        kwargs to pass to the evolution_model instance
1401

1402
    Returns
1403
    -------
1404
    :py:class:`oggm.core.flowline.evolution_model`
1405
        The final model after the run.
1406
    """
NEW
1407
    from oggm.workflow import calibrate_inversion_from_volume
×
1408

UNCOV
1409
    if output_filesuffix is None:
×
UNCOV
1410
        output_filesuffix = settings_filesuffix
×
1411

UNCOV
1412
    evolution_model = decide_evolution_model(gdir=gdir,
×
1413
                                             evolution_model=evolution_model)
1414

1415
    # revert gdir to original state if necessary
UNCOV
1416
    if melt_f != gdir.settings['melt_f']:
×
1417
        define_new_melt_f_in_gdir(gdir, melt_f)
×
1418
        if do_inversion:
×
1419
            with utils.DisableLogger():
×
1420
                apparent_mb_from_any_mb(gdir,
×
1421
                                        settings_filesuffix=settings_filesuffix,
1422
                                        add_to_log_file=False)
NEW
1423
                calibrate_inversion_from_volume(
×
1424
                    [gdir], settings_filesuffix=settings_filesuffix,
1425
                    apply_fs_on_mismatch=True, error_on_mismatch=False,
1426
                    filter_inversion_output=True,
1427
                    ref_volume_m3=gdir.observations['ref_volume_m3']['value'],
1428
                    ref_volume_year=gdir.observations['ref_volume_m3']['year'],
1429
                    add_to_log_file=False)
1430
    if os.path.isfile(os.path.join(gdir.dir,
×
1431
                                   'model_flowlines_dyn_melt_f_calib.pkl')):
1432
        os.remove(os.path.join(gdir.dir,
×
1433
                               'model_flowlines_dyn_melt_f_calib.pkl'))
1434

1435
    if target_yr is None:
×
1436
        target_yr = gdir.rgi_date + 1  # + 1 converted to hydro years
×
1437
    if min_spinup_period > target_yr - ys:
×
1438
        log.info('The RGI year is closer to ys as the minimum spinup '
×
1439
                 'period -> therefore the minimum spinup period is '
1440
                 'adapted and it is the only period which is tried by the '
1441
                 'dynamic spinup function!')
1442
        min_spinup_period = target_yr - ys
×
1443
        spinup_period = target_yr - ys
×
1444

1445
    yr_clim_min = gdir.get_climate_info()['baseline_yr_0']
×
1446
    try:
×
1447
        model_end = run_dynamic_spinup(
×
1448
            gdir,
1449
            settings_filesuffix=settings_filesuffix,
1450
            continue_on_error=False,  # force to raise an error in @entity_task
1451
            add_to_log_file=False,
1452
            init_model_fls=fls_init,
1453
            climate_input_filesuffix=climate_input_filesuffix,
1454
            evolution_model=evolution_model,
1455
            mb_model_historical=mb_model_historical,
1456
            mb_model_spinup=mb_model_spinup,
1457
            spinup_period=spinup_period,
1458
            spinup_start_yr=ys,
1459
            min_spinup_period=min_spinup_period,
1460
            spinup_start_yr_max=spinup_start_yr_max,
1461
            target_yr=target_yr,
1462
            minimise_for=minimise_for,
1463
            precision_percent=precision_percent,
1464
            precision_absolute=precision_absolute,
1465
            min_ice_thickness=min_ice_thickness,
1466
            first_guess_t_spinup=first_guess_t_spinup,
1467
            t_spinup_max_step_length=t_spinup_max_step_length,
1468
            maxiter=maxiter,
1469
            output_filesuffix=output_filesuffix,
1470
            store_model_geometry=store_model_geometry,
1471
            store_fl_diagnostics=store_fl_diagnostics,
1472
            ignore_errors=False,
1473
            ye=ye,
1474
            add_fixed_geometry_spinup=add_fixed_geometry_spinup,
1475
            **kwargs)
1476

1477
        gdir.settings['used_spinup_option'] = 'dynamic spinup only'
×
1478
        # this is only for backwards compatibility
1479
        gdir.add_to_diagnostics('used_spinup_option', 'dynamic spinup only')
×
1480

1481
    except RuntimeError:
×
1482
        log.warning('No dynamic spinup could be conducted by using the '
×
1483
                    f'original melt factor ({melt_f}). Therefore the last '
1484
                    'try is to conduct a run until ye without a dynamic '
1485
                    'spinup.')
1486

1487
        # set all dynamic diagnostics to None if there where some successful
1488
        # runs
1489
        diag = gdir.get_diagnostics()
×
1490
        if minimise_for == 'area':
×
1491
            unit = 'km2'
×
1492
        elif minimise_for == 'volume':
×
1493
            unit = 'km3'
×
1494
        else:
1495
            raise NotImplementedError
1496
        for key in ['temp_bias_dynamic_spinup', 'dynamic_spinup_period',
×
1497
                    'dynamic_spinup_forward_model_iterations',
1498
                    f'{minimise_for}_mismatch_dynamic_spinup_{unit}_percent',
1499
                    f'reference_{minimise_for}_dynamic_spinup_{unit}',
1500
                    'dynamic_spinup_other_variable_reference',
1501
                    'dynamic_spinup_mismatch_other_variable_percent']:
1502
            if key in diag:
×
1503
                gdir.settings[key] = None
×
1504
                # this is only for backwards compatibility
1505
                gdir.add_to_diagnostics(key, None)
×
1506

1507
        gdir.settings['run_dynamic_spinup_success'] = False
×
1508
        # this is only for backwards compatibility
1509
        gdir.add_to_diagnostics('run_dynamic_spinup_success', False)
×
1510

1511
        # try to make a fixed geometry spinup
1512
        model_end = run_from_climate_data(
×
1513
            gdir,
1514
            # force to raise an error in @entity_task
1515
            continue_on_error=False,
1516
            add_to_log_file=False,
1517
            settings_filesuffix=settings_filesuffix,
1518
            min_ys=yr_clim_min, ye=ye,
1519
            output_filesuffix=output_filesuffix,
1520
            climate_input_filesuffix=climate_input_filesuffix,
1521
            store_model_geometry=store_model_geometry,
1522
            store_fl_diagnostics=store_fl_diagnostics,
1523
            init_model_fls=fls_init, evolution_model=evolution_model,
1524
            fixed_geometry_spinup_yr=ys)
1525

1526
        gdir.settings['used_spinup_option'] = 'fixed geometry spinup'
×
1527
        # this is only for backwards compatibility
1528
        gdir.add_to_diagnostics('used_spinup_option', 'fixed geometry spinup')
×
1529

1530
    return model_end
×
1531

1532

1533
def dynamic_melt_f_run(
9✔
1534
        gdir, melt_f, yr0_ref_mb, yr1_ref_mb, fls_init, ys, ye,
1535
        settings_filesuffix='',  output_filesuffix=None, evolution_model=None,
1536
        local_variables=None, set_local_variables=False, target_yr=None,
1537
        model_flowline_filesuffix='',
1538
        **kwargs):
1539
    """
1540
    This function is one option for a 'run_function' for the
1541
    'run_dynamic_melt_f_calibration' function (the corresponding
1542
    'fallback_function' is 'dynamic_melt_f_run_fallback'). It is meant to
1543
    define a new melt_f in the given gdir and conduct a
1544
    'run_from_climate_data' run between ys and ye and return the geodetic mass
1545
    balance (units: kg m-2 yr-1) of the period yr0_ref_mb and yr1_ref_mb.
1546

1547
    Parameters
1548
    ----------
1549
    gdir : :py:class:`oggm.GlacierDirectory`
1550
        the glacier directory to process
1551
    settings_filesuffix: str
1552
        You can use a different set of settings by providing a filesuffix. This
1553
        is useful for sensitivity experiments.
1554
    melt_f : float
1555
        the melt_f used for this run
1556
    yr0_ref_mb : int
1557
        the start year of the geodetic mass balance
1558
    yr1_ref_mb : int
1559
        the end year of the geodetic mass balance
1560
    fls_init : []
1561
        List of flowlines to use to initialise the model
1562
    ys : int
1563
        start year of the complete run, must by smaller or equal y0_ref_mb
1564
    ye : int
1565
        end year of the complete run, must be smaller or equal y1_ref_mb
1566
    output_filesuffix : str
1567
        For the output file. If None the settings_filesuffix will be used.
1568
        Default is None
1569
    evolution_model : :class:oggm.core.FlowlineModel
1570
        which evolution model to use. Default: gdir.settings['evolution_model']
1571
        Not all models work in all circumstances!
1572
    local_variables : None
1573
        Not needed in this function, just here to match with the function
1574
        call in run_dynamic_melt_f_calibration.
1575
    set_local_variables : bool
1576
        Not needed in this function. Only here to be confirm with the use of
1577
        this function in 'run_dynamic_melt_f_calibration'.
1578
    target_yr : int or None
1579
        The target year for a potential dynamic spinup (not needed here).
1580
        Default is None
1581
    kwargs : dict
1582
        kwargs to pass to the evolution_model instance
1583

1584
    Returns
1585
    -------
1586
    :py:class:`oggm.core.flowline.evolution_model`, float
1587
        The final model after the run and the calculated geodetic mass balance
1588
    """
1589

1590
    if output_filesuffix is None:
1✔
1591
        output_filesuffix = settings_filesuffix
1✔
1592

1593
    evolution_model = decide_evolution_model(gdir=gdir,
1✔
1594
                                             evolution_model=evolution_model)
1595

1596
    if set_local_variables:
1✔
1597
        # No local variables needed in this function
1598
        return None
1✔
1599

1600
    # Here we start with the actual model run
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
                                      settings_filesuffix=settings_filesuffix,
1610
                                      ys=ys, ye=ye,
1611
                                      output_filesuffix=output_filesuffix,
1612
                                      init_model_fls=fls_init,
1613
                                      evolution_model=evolution_model,
1614
                                      **kwargs)
1615
    except RuntimeError as e:
1✔
1616
        raise RuntimeError(f'run_from_climate_data raised error! '
1✔
1617
                           f'(Message: {e})')
1618

1619
    # calculate dmdtda from previous simulation here
1620
    with utils.DisableLogger():
1✔
1621
        ds = utils.compile_run_output(gdir, input_filesuffix=output_filesuffix,
1✔
1622
                                      path=False)
1623
    dmdtda_mdl = ((ds.volume.loc[yr1_ref_mb].values[0] -
1✔
1624
                   ds.volume.loc[yr0_ref_mb].values[0]) /
1625
                  gdir.rgi_area_m2 /
1626
                  (yr1_ref_mb - yr0_ref_mb) *
1627
                  gdir.settings['ice_density'])
1628

1629
    return model, dmdtda_mdl
1✔
1630

1631

1632
def dynamic_melt_f_run_fallback(
9✔
1633
        gdir, melt_f, fls_init, ys, ye,
1634
        settings_filesuffix='', output_filesuffix=None,
1635
        evolution_model=None, target_yr=None, **kwargs):
1636
    """
1637
    This is the fallback function corresponding to the function
1638
    'dynamic_melt_f_run', which are provided to
1639
    'run_dynamic_melt_f_calibration'. It is used if the run_function fails and
1640
    if 'ignore_error=True' in 'run_dynamic_melt_f_calibration'. It sets
1641
    melt_f and conduct a run_from_climate_data run from ys to ye.
1642

1643
    Parameters
1644
    ----------
1645
    gdir : :py:class:`oggm.GlacierDirectory`
1646
        the glacier directory to process
1647
    melt_f : float
1648
        the melt_f used for this run
1649
    fls_init : []
1650
        List of flowlines to use to initialise the model
1651
    ys : int
1652
        start year of the run
1653
    ye : int
1654
        end year of the run
1655
    settings_filesuffix: str
1656
        You can use a different set of settings by providing a filesuffix. This
1657
        is useful for sensitivity experiments.
1658
    output_filesuffix : str
1659
        For the output file. If None the settings_filesuffix will be used.
1660
        Default is None
1661
    evolution_model : :class:oggm.core.FlowlineModel
1662
        which evolution model to use. Default: gdir.settings['evolution_model']
1663
        Not all models work in all circumstances!
1664
    target_yr : int or None
1665
        The target year for a potential dynamic spinup (not needed here).
1666
        Default is None
1667
    kwargs : dict
1668
        kwargs to pass to the evolution_model instance
1669

1670
     Returns
1671
    -------
1672
    :py:class:`oggm.core.flowline.evolution_model`
1673
        The final model after the run.
1674
    """
1675

1676
    if output_filesuffix is None:
1✔
1677
        output_filesuffix = settings_filesuffix
×
1678

1679
    evolution_model = decide_evolution_model(gdir=gdir,
1✔
1680
                                             evolution_model=evolution_model)
1681

1682
    define_new_melt_f_in_gdir(gdir, melt_f)
1✔
1683

1684
    # conduct model run
1685
    try:
1✔
1686
        model = run_from_climate_data(gdir,
1✔
1687
                                      # force to raise an error in @entity_task
1688
                                      continue_on_error=False,
1689
                                      add_to_log_file=False,
1690
                                      settings_filesuffix=settings_filesuffix,
1691
                                      ys=ys, ye=ye,
1692
                                      output_filesuffix=output_filesuffix,
1693
                                      init_model_fls=fls_init,
1694
                                      evolution_model=evolution_model,
1695
                                      **kwargs)
1696
        gdir.settings['used_spinup_option'] = 'no spinup'
1✔
1697
        # this is only for backwards compatibility
1698
        gdir.add_to_diagnostics('used_spinup_option', 'no spinup')
1✔
1699
    except RuntimeError as e:
×
1700
        raise RuntimeError(f'In fallback function run_from_climate_data '
×
1701
                           f'raised error! (Message: {e})')
1702

1703
    return model
1✔
1704

1705

1706
@entity_task(log, writes=['inversion_flowlines'])
9✔
1707
def run_dynamic_melt_f_calibration(
9✔
1708
        gdir, settings_filesuffix='', observations_filesuffix='',
1709
        overwrite_observations=False,
1710
        ref_mb=None, ref_mb_err=None, ref_mb_err_scaling_factor=1,
1711
        ref_mb_period=None, melt_f_min=None,
1712
        melt_f_max=None, melt_f_max_step_length_minimum=0.1, maxiter=20,
1713
        ignore_errors=False, output_filesuffix=None,
1714
        model_flowline_filesuffix=None,
1715
        ys=None, ye=None, target_yr=None,
1716
        run_function=dynamic_melt_f_run_with_dynamic_spinup,
1717
        kwargs_run_function=None,
1718
        fallback_function=dynamic_melt_f_run_with_dynamic_spinup_fallback,
1719
        kwargs_fallback_function=None, init_model_filesuffix=None,
1720
        init_model_yr=None, init_model_fls=None,
1721
        first_guess_diagnostic_msg='dynamic spinup only'):
1722
    """Calibrate melt_f to match a geodetic mass balance incorporating a
1723
    dynamic model run.
1724

1725
    This task iteratively search for a melt_f to match a provided geodetic
1726
    mass balance. How one model run looks like is defined in the 'run_function'.
1727
    This function should take a new melt_f guess, conducts a dynamic run and
1728
    calculate the geodetic mass balance. The goal is to match the geodetic mass
1729
    blanance 'ref_mb' inside the provided error 'ref_mb_err' (by default the
1730
    values from Hugonnet are used). If the minimisation of the mismatch between
1731
    the provided and modeled geodetic mass balance is not working the
1732
    'fallback_function' is called. In there it is decided what run should be
1733
    conducted in such a failing case. Further if 'ignore_error' is set to True
1734
    and we could not find a satisfying mismatch the best run so far is saved
1735
    (if not one successful run with 'run_function' the 'fallback_function' is
1736
    called).
1737

1738
    Parameters
1739
    ----------
1740
    gdir : :py:class:`oggm.GlacierDirectory`
1741
        the glacier directory to process
1742
    settings_filesuffix: str
1743
        You can use a different set of settings by providing a filesuffix. This
1744
        is useful for sensitivity experiments. Code-wise the settings_filesuffix
1745
        is set in the @entity-task decorater.
1746
    observations_filesuffix: str
1747
        The observations filesuffix, from where the reference calibration data
1748
        'ref_mb' should be used. Code-wise the observations_filesuffix is set in
1749
        the @entity-task decorater.
1750
    overwrite_observations : bool
1751
        If you want to overwrite already existing observation values in the
1752
        provided observations file set this to True. Default is False.
1753
    ref_mb : float or None
1754
        The reference geodetic mass balance to match (units: kg m-2 yr-1). If
1755
        None the data from Hugonnet 2021 is used.
1756
        Default is None
1757
    ref_mb_err : float or None
1758
        The error of the reference geodetic mass balance to match (unit: kg m-2
1759
        yr-1). Must always be a positive number. If None the data from Hugonett
1760
        2021 is used.
1761
        Default is None
1762
    ref_mb_err_scaling_factor : float
1763
        The error of the geodetic mass balance is multiplied by this factor.
1764
        When looking at more glaciers you should set this factor smaller than
1765
        1 (Default), but the smaller this factor the more glaciers will fail
1766
        during calibration.
1767
        The idea is that we reduce the uncertainty of individual observations
1768
        to count for correlated uncertainties when looking at regional or
1769
        global scales. If err_scaling_factor is 1 (Default) and you look at the
1770
        results of more than one glacier this equals that all errors are
1771
        uncorrelated. Therefore the result will be outside the uncertainty
1772
        boundaries given in Hugonett 2021 e.g. for the global estimate, because
1773
        some correlation of the individual errors is assumed during aggregation
1774
        of glaciers to regions (for more details see paper Hugonett 2021).
1775
    ref_mb_period : str
1776
        If ref_mb is None one of '2000-01-01_2010-01-01',
1777
        '2010-01-01_2020-01-01', '2000-01-01_2020-01-01'. If ref_mb is
1778
        set, this should still match the same format but can be any date.
1779
        Default is '' (-> PARAMS['geodetic_mb_period'])
1780
    melt_f_min : float or None
1781
        Lower absolute limit for melt_f.
1782
        Default is None (-> gdir.settings['melt_f_min'])
1783
    melt_f_max : float or None
1784
        Upper absolute limit for melt_f.
1785
        Default is None (-> gdir.settings['melt_f_max'])
1786
    melt_f_max_step_length_minimum : float
1787
        Defines a minimum maximal change of melt_f between two iterations. The
1788
        maximum step length is needed to avoid too large steps, which likely
1789
        lead to an error.
1790
        Default is 0.1
1791
    maxiter : int
1792
        Maximum number of minimisation iterations of minimising mismatch to
1793
        dmdtda by changing melt_f. Each of this iterations conduct a complete
1794
        run defined in the 'run_function'. If maxiter reached and
1795
        'ignore_errors=False' an error is raised.
1796
        Default is 20
1797
    ignore_errors : bool
1798
        If True and the 'run_function' with melt_f calibration is not working
1799
        to match dmdtda inside the provided uncertainty fully, but their where
1800
        some successful runs which improved the first guess, they are saved as
1801
        part success, and if not a single run was successful the
1802
        'fallback_function' is called.
1803
        If False and the 'run_function' with melt_f calibration is not working
1804
        fully an error is raised.
1805
        Default is True
1806
    output_filesuffix : str
1807
        For the output file. If None the settings_filesuffix will be used.
1808
        Default is None
1809
    model_flowline_filesuffix : str
1810
        for the model flowline to use if no other flowlines for initialisation
1811
        are provided. If None the settings_filesuffix will be used.
1812
        Default is None
1813
    ys : int or None
1814
        The start year of the conducted run. If None the first year of the
1815
        provided climate file.
1816
        Default is None
1817
    ye : int or None
1818
        The end year of the conducted run. If None the last year of the
1819
        provided climate file.
1820
        Default is None
1821
    target_yr : int or None
1822
        The target year for a potential dynamic spinup (see run_dynamic_spinup
1823
        function for more info).
1824
        If None, gdir.rgi_date + 1 is used (the default).
1825
        Default is None
1826
    run_function : function
1827
        This function defines how a new defined melt_f is used to conduct the
1828
        next model run. This function must contain the arguments 'gdir',
1829
        'settings_filesuffix', 'melt_f', 'yr0_ref_mb', 'yr1_ref_mb', 'fls_init',
1830
        'ys', 'ye' and 'output_filesuffix'. Further this function must return
1831
        the final model and the calculated geodetic mass balance dmdtda in
1832
        kg m-2 yr-1.
1833
    kwargs_run_function : None or dict
1834
        Can provide additional keyword arguments to the run_function as a
1835
        dictionary.
1836
    fallback_function : function
1837
        This is a fallback function if the calibration is not working using
1838
        'run_function' it is called. This function must contain the arguments
1839
        'gdir', 'settings_filesuffix', 'melt_f', 'fls_init', 'ys', 'ye',
1840
        and 'output_filesuffix'. Further this function should
1841
        return the final model.
1842
    kwargs_fallback_function : None or dict
1843
        Can provide additional keyword arguments to the fallback_function as a
1844
        dictionary.
1845
    init_model_filesuffix : str or None
1846
        If you want to start from a previous model run state. This state
1847
        should be at time yr_rgi_date.
1848
        Default is None
1849
    init_model_yr : int or None
1850
        the year of the initial run you want to start from. The default
1851
        is to take the last year of the simulation.
1852
    init_model_fls : []
1853
        List of flowlines to use to initialise the model (the default is the
1854
        present_time_glacier file from the glacier directory).
1855
        Ignored if `init_model_filesuffix` is set.
1856
    first_guess_diagnostic_msg : str
1857
        This message will be added to the glacier diagnostics if only the
1858
        default melt_f resulted in a successful 'run_function' run.
1859
        Default is 'dynamic spinup only'
1860

1861
    Returns
1862
    -------
1863
    :py:class:`oggm.core.flowline.evolution_model`
1864
        The final dynamically spined-up model. Type depends on the selected
1865
        evolution_model.
1866
    """
1867

1868
    if output_filesuffix is None:
2✔
1869
        output_filesuffix = settings_filesuffix
1✔
1870

1871
    if model_flowline_filesuffix is None:
2✔
1872
        model_flowline_filesuffix = settings_filesuffix
2✔
1873

1874
    # melt_f constraints
1875
    if melt_f_min is None:
2✔
1876
        melt_f_min = gdir.settings['melt_f_min']
2✔
1877
    if melt_f_max is None:
2✔
1878
        melt_f_max = gdir.settings['melt_f_max']
×
1879

1880
    if kwargs_run_function is None:
2✔
1881
        kwargs_run_function = {}
1✔
1882
    if kwargs_fallback_function is None:
2✔
1883
        kwargs_fallback_function = {}
1✔
1884

1885
    # geodetic mb stuff
1886
    # handle which ref mb to use (default or provided via kwargs or
1887
    # in observations file)
1888
    ref_mb_provided = {}
2✔
1889
    if ref_mb is not None:
2✔
1890
        ref_mb_provided['value'] = ref_mb
1✔
1891
    if ref_mb_err is not None:
2✔
1892
        ref_mb_provided['err'] = ref_mb_err
1✔
1893
    if ref_mb_period is not None:
2✔
1894
        ref_mb_provided['period'] = ref_mb_period
1✔
1895

1896
    if 'ref_mb' in gdir.observations:
2✔
1897
        ref_mb_in_file = gdir.observations['ref_mb']
2✔
1898
    else:
1899
        ref_mb_in_file = None
1✔
1900

1901
    # if nothing is provided use hugonnet
1902
    if (ref_mb_provided == {}) and (ref_mb_in_file is None):
2✔
1903
        if not ref_mb_period:
1✔
1904
            ref_mb_period = gdir.settings['geodetic_mb_period']
1✔
1905
        df_ref_mb = utils.get_geodetic_mb_dataframe().loc[gdir.rgi_id]
1✔
1906
        sel = df_ref_mb.loc[df_ref_mb['period'] == ref_mb_period].iloc[0]
1✔
1907
        # reference geodetic mass balance from Hugonnet 2021
1908
        ref_mb = float(sel['dmdtda'])
1✔
1909
        # dmdtda: in meters water-equivalent per year -> we convert
1910
        ref_mb *= 1000  # kg m-2 yr-1
1✔
1911
        # error of reference geodetic mass balance from Hugonnet 2021
1912
        ref_mb_err = float(sel['err_dmdtda'])
1✔
1913
        ref_mb_err *= 1000  # kg m-2 yr-1
1✔
1914
        ref_mb_use = {
1✔
1915
            'value': ref_mb,
1916
            'period': ref_mb_period,
1917
            'err': ref_mb_err,
1918
        }
1919

1920
    # if nothing in file, or if we should overwrite the file
1921
    elif (ref_mb_in_file is None) or (ref_mb_in_file is not None and
2✔
1922
                                      overwrite_observations):
1923
        ref_mb_use = ref_mb_provided
1✔
1924

1925
    # only in file
1926
    elif ref_mb_in_file is not None and ref_mb_provided == {}:
2✔
1927
        ref_mb_use = ref_mb_in_file
2✔
1928

1929
    # provided in file and as kwarg
1930
    else:
1931
        # if the provided is the same as the one stored in the file it is fine
NEW
1932
        if ref_mb_in_file != ref_mb_provided:
×
NEW
1933
            raise InvalidWorkflowError(
×
1934
                'You provided a reference mass balance, but their is already '
1935
                'one stored in the current observation file '
1936
                f'({os.path.basename(gdir.observations.path)}). If you want to '
1937
                'overwrite set overwrite_observations = True.')
1938
        else:
NEW
1939
            ref_mb_use = ref_mb_in_file
×
1940

1941
    gdir.observations['ref_mb'] = ref_mb_use
2✔
1942

1943
    # now we can extract the actual values we want to use
1944
    if 'value' in ref_mb_use:
2✔
1945
        ref_mb = ref_mb_use['value']
2✔
1946
    if 'err' in ref_mb_use:
2✔
1947
        ref_mb_err = ref_mb_use['err']
2✔
1948
        # apply the scaling factor
1949
        ref_mb_err *= ref_mb_err_scaling_factor
2✔
1950
    if 'period' in ref_mb_use:
2✔
1951
        ref_mb_period = ref_mb_use['period']
2✔
1952

1953
    # if a reference geodetic mb is specified also the error of it must be
1954
    # specified, and vice versa
1955
    if ((ref_mb is None and ref_mb_err is not None) or
2✔
1956
            (ref_mb is not None and ref_mb_err is None)):
1957
        raise RuntimeError('If you provide a reference geodetic mass balance '
1✔
1958
                           '(ref_mb) you must also provide an error for it '
1959
                           '(ref_mb_err), and vice versa!')
1960

1961
    if ref_mb_err <= 0:
2✔
1962
        raise RuntimeError('The provided error for the geodetic mass-balance '
1✔
1963
                           '(ref_mb_err) must be positive and non zero! But '
1964
                           f'given was {ref_mb_err}!')
1965
    # get start and end year of geodetic mb
1966
    yr0_ref_mb, yr1_ref_mb = ref_mb_period.split('_')
2✔
1967
    yr0_ref_mb = int(yr0_ref_mb.split('-')[0])
2✔
1968
    yr1_ref_mb = int(yr1_ref_mb.split('-')[0])
2✔
1969

1970
    clim_info = gdir.get_climate_info()
2✔
1971

1972
    if ye is None:
2✔
1973
        # One adds 1 because the run ends at the end of the year
1974
        ye = clim_info['baseline_yr_1'] + 1
1✔
1975

1976
    if ye < yr1_ref_mb:
2✔
1977
        raise RuntimeError('The provided ye is smaller than the end year of '
1✔
1978
                           'the given geodetic_mb_period!')
1979

1980
    if ys is None:
2✔
1981
        ys = clim_info['baseline_yr_0']
1✔
1982

1983
    if ys > yr0_ref_mb:
2✔
1984
        raise RuntimeError('The provided ys is larger than the start year of '
1✔
1985
                           'the given geodetic_mb_period!')
1986

1987
    if target_yr is None:
2✔
1988
        target_yr = gdir.rgi_date + 1  # + 1 converted to hydro years
2✔
1989
    if target_yr < ys:
2✔
1990
        if ignore_errors:
×
1991
            log.info('The rgi year is smaller than the provided start year '
×
1992
                     'ys -> setting the rgi year to ys to continue!')
1993
            target_yr = ys
×
1994
        else:
1995
            raise RuntimeError('The rgi year is smaller than the provided '
×
1996
                               'start year ys!')
1997
    kwargs_run_function['target_yr'] = target_yr
2✔
1998
    kwargs_fallback_function['target_yr'] = target_yr
2✔
1999

2000
    # get initial flowlines from which we want to start from
2001
    if init_model_filesuffix is not None:
2✔
2002
        fp = gdir.get_filepath('model_geometry',
1✔
2003
                               filesuffix=init_model_filesuffix)
2004
        fmod = FileModel(fp)
1✔
2005
        if init_model_yr is None:
1✔
2006
            init_model_yr = fmod.last_yr
1✔
2007
        fmod.run_until(init_model_yr)
1✔
2008
        init_model_fls = fmod.fls
1✔
2009

2010
    if init_model_fls is None:
2✔
2011
        fls_init = gdir.read_pickle('model_flowlines',
2✔
2012
                                    filesuffix=model_flowline_filesuffix)
2013
    else:
2014
        fls_init = copy.deepcopy(init_model_fls)
1✔
2015

2016
    # save original melt_f for later to be able to recreate original gdir
2017
    # (using the fallback function) if an error occurs
2018
    melt_f_initial = gdir.settings['melt_f']
2✔
2019

2020
    # define maximum allowed change of melt_f between two iterations. Is needed
2021
    # to avoid to large changes (=likely lead to an error). It is defined in a
2022
    # way that in maxiter steps the further away limit can be reached
2023
    melt_f_max_step_length = np.max(
2✔
2024
        [np.max(np.abs(np.array([melt_f_min, melt_f_min]) - melt_f_initial)) /
2025
         maxiter,
2026
         melt_f_max_step_length_minimum])
2027

2028
    # only used to check performance of minimisation
2029
    dynamic_melt_f_calibration_runs = [0]
2✔
2030

2031
    # this function is called if the actual dynamic melt_f calibration fails
2032
    def fallback_run(melt_f, reset, best_mismatch=None, initial_mismatch=None,
2✔
2033
                     only_first_guess=None):
2034
        if reset:
1✔
2035
            # unfortunately we could not conduct an error free run using the
2036
            # provided run_function, so we us the fallback_function
2037

2038
            # this diagnostics should be overwritten inside the fallback_function
2039
            gdir.settings['used_spinup_option'] = 'fallback_function'
1✔
2040
            # this is only for backwards compatibility
2041
            gdir.add_to_diagnostics('used_spinup_option', 'fallback_function')
1✔
2042

2043
            model = fallback_function(gdir=gdir,
1✔
2044
                                      settings_filesuffix=settings_filesuffix,
2045
                                      melt_f=melt_f, fls_init=fls_init,
2046
                                      ys=ys, ye=ye,
2047
                                      output_filesuffix=output_filesuffix,
2048
                                      **kwargs_fallback_function)
2049
        else:
2050
            # we were not able to reach the desired precision during the
2051
            # minimisation, but at least we conducted a few error free runs
2052
            # using the run_function, and therefore we save the best guess we
2053
            # found so far
2054
            if only_first_guess:
1✔
2055
                gdir.settings['used_spinup_option'] = first_guess_diagnostic_msg
×
2056
                # this is only for backwards compatibility
2057
                gdir.add_to_diagnostics('used_spinup_option',
×
2058
                                        first_guess_diagnostic_msg)
2059
            else:
2060
                gdir.settings['used_spinup_option'] = ('dynamic melt_f '
1✔
2061
                                                       'calibration (part success)')
2062
                # this is only for backwards compatibility
2063
                gdir.add_to_diagnostics('used_spinup_option',
1✔
2064
                                        'dynamic melt_f calibration (part '
2065
                                        'success)')
2066
            model, dmdtda_mdl = run_function(gdir=gdir,
1✔
2067
                                             settings_filesuffix=settings_filesuffix,
2068
                                             melt_f=melt_f,
2069
                                             yr0_ref_mb=yr0_ref_mb,
2070
                                             yr1_ref_mb=yr1_ref_mb,
2071
                                             fls_init=fls_init, ys=ys, ye=ye,
2072
                                             output_filesuffix=output_filesuffix,
2073
                                             local_variables=local_variables_run_function,
2074
                                             **kwargs_run_function)
2075

2076
            # some dianostics for later
2077
            diag_dyn_melt = {
1✔
2078
                'dmdtda_mismatch_dynamic_calibration_reference':
2079
                    float(ref_mb),
2080
                'dmdtda_dynamic_calibration_given_error': float(ref_mb_err),
2081
                'dmdtda_dynamic_calibration_error_scaling_factor':
2082
                    float(ref_mb_err_scaling_factor),
2083
                'dmdtda_mismatch_dynamic_calibration': float(best_mismatch),
2084
                'dmdtda_mismatch_with_initial_melt_f': float(initial_mismatch),
2085
                'melt_f_dynamic_calibration': float(melt_f),
2086
                'melt_f_before_dynamic_calibration': float(melt_f_initial),
2087
                'run_dynamic_melt_f_calibration_iterations':
2088
                    int(dynamic_melt_f_calibration_runs[-1]),
2089
            }
2090

2091
            for k, v in diag_dyn_melt.items():
1✔
2092
                gdir.settings[k] = v
1✔
2093
                # this is only for backwards compatibility
2094
                if settings_filesuffix == '':
1✔
2095
                    gdir.add_to_diagnostics(k, v)
1✔
2096

2097
        return model
1✔
2098

2099
    # here we define the local variables which are used in the run_function,
2100
    # for some run_functions this is useful to save parameters from a previous
2101
    # run to be faster in the upcoming runs
2102
    local_variables_run_function = {}
2✔
2103
    run_function(gdir=gdir, settings_filesuffix=settings_filesuffix,
2✔
2104
                 melt_f=None, yr0_ref_mb=None, yr1_ref_mb=None,
2105
                 fls_init=None, ys=None, ye=None,
2106
                 local_variables=local_variables_run_function,
2107
                 model_flowline_filesuffix=model_flowline_filesuffix,
2108
                 set_local_variables=True, **kwargs_run_function)
2109

2110
    # this is the actual model run which is executed each iteration in order to
2111
    # minimise the mismatch of dmdtda of model and observation
2112
    def model_run(melt_f):
2✔
2113
        # to check performance of minimisation
2114
        dynamic_melt_f_calibration_runs.append(
2✔
2115
            dynamic_melt_f_calibration_runs[-1] + 1)
2116

2117
        model, dmdtda_mdl = run_function(gdir=gdir,
2✔
2118
                                         settings_filesuffix=settings_filesuffix,
2119
                                         melt_f=melt_f,
2120
                                         yr0_ref_mb=yr0_ref_mb,
2121
                                         yr1_ref_mb=yr1_ref_mb,
2122
                                         fls_init=fls_init, ys=ys, ye=ye,
2123
                                         output_filesuffix=output_filesuffix,
2124
                                         local_variables=local_variables_run_function,
2125
                                         **kwargs_run_function)
2126
        return model, dmdtda_mdl
2✔
2127

2128
    def cost_fct(melt_f, model_dynamic_spinup_end):
2✔
2129

2130
        # actual model run
2131
        model_dynamic_spinup, dmdtda_mdl = model_run(melt_f)
2✔
2132

2133
        # save final model for later
2134
        model_dynamic_spinup_end.append(copy.deepcopy(model_dynamic_spinup))
2✔
2135

2136
        # calculate the mismatch of dmdtda
2137
        cost = float(dmdtda_mdl - ref_mb)
2✔
2138

2139
        return cost
2✔
2140

2141
    def init_cost_fun():
2✔
2142
        model_dynamic_spinup_end = []
2✔
2143

2144
        def c_fun(melt_f):
2✔
2145
            return cost_fct(melt_f, model_dynamic_spinup_end)
2✔
2146

2147
        return c_fun, model_dynamic_spinup_end
2✔
2148

2149
    # Here start with own spline minimisation algorithm
2150
    def minimise_with_spline_fit(fct_to_minimise, melt_f_guess, mismatch):
2✔
2151
        # defines limits of melt_f in accordance to maximal allowed change
2152
        # between iterations
2153
        melt_f_limits = [max(melt_f_initial - melt_f_max_step_length,
2✔
2154
                             melt_f_min),
2155
                         min(melt_f_initial + melt_f_max_step_length,
2156
                             melt_f_max)]
2157

2158
        # this two variables indicate that the limits were already adapted to
2159
        # avoid an error
2160
        was_min_error = False
2✔
2161
        was_max_error = False
2✔
2162
        was_errors = [was_min_error, was_max_error]
2✔
2163

2164
        def get_mismatch(melt_f):
2✔
2165
            melt_f = copy.deepcopy(melt_f)
2✔
2166
            # first check if the melt_f is inside limits
2167
            if melt_f < melt_f_limits[0]:
2✔
2168
                # was the smaller limit already executed, if not first do this
2169
                if melt_f_limits[0] not in melt_f_guess:
2✔
2170
                    melt_f = copy.deepcopy(melt_f_limits[0])
2✔
2171
                else:
2172
                    # smaller limit was already used, check if it was
2173
                    # already newly defined with error
2174
                    if was_errors[0]:
1✔
2175
                        raise RuntimeError('Not able to minimise without '
×
2176
                                           'raising an error at lower limit of '
2177
                                           'melt_f!')
2178
                    else:
2179
                        # ok we set a new lower limit, consider also minimum
2180
                        # limit
2181
                        melt_f_limits[0] = max(melt_f_min,
1✔
2182
                                               melt_f_limits[0] -
2183
                                               melt_f_max_step_length)
2184
            elif melt_f > melt_f_limits[1]:
2✔
2185
                # was the larger limit already executed, if not first do this
2186
                if melt_f_limits[1] not in melt_f_guess:
1✔
2187
                    melt_f = copy.deepcopy(melt_f_limits[1])
1✔
2188
                else:
2189
                    # larger limit was already used, check if it was
2190
                    # already newly defined with ice free glacier
2191
                    if was_errors[1]:
×
2192
                        raise RuntimeError('Not able to minimise without '
×
2193
                                           'raising an error at upper limit of '
2194
                                           'melt_f!')
2195
                    else:
2196
                        # ok we set a new upper limit, consider also maximum
2197
                        # limit
2198
                        melt_f_limits[1] = min(melt_f_max,
×
2199
                                               melt_f_limits[1] +
2200
                                               melt_f_max_step_length)
2201

2202
            # now clip melt_f with limits (to be sure)
2203
            melt_f = np.clip(melt_f, melt_f_limits[0], melt_f_limits[1])
2✔
2204
            if melt_f in melt_f_guess:
2✔
2205
                raise RuntimeError('This melt_f was already tried. Probably '
×
2206
                                   'we are at one of the max or min limit and '
2207
                                   'still have no satisfactory mismatch '
2208
                                   'found!')
2209

2210
            # if error during dynamic calibration this defines how much
2211
            # melt_f is changed in the upcoming iterations to look for an
2212
            # error free run
2213
            melt_f_search_change = melt_f_max_step_length / 10
2✔
2214
            # maximum number of changes to look for an error free run
2215
            max_iterations = int(melt_f_max_step_length /
2✔
2216
                                 melt_f_search_change)
2217

2218
            current_min_error = False
2✔
2219
            current_max_error = False
2✔
2220
            doing_first_guess = (len(mismatch) == 0)
2✔
2221
            iteration = 0
2✔
2222
            current_melt_f = copy.deepcopy(melt_f)
2✔
2223

2224
            # in this loop if an error at the limits is raised we go step by
2225
            # step away from the limits until we are at the initial guess or we
2226
            # found an error free run
2227
            tmp_mismatch = None
2✔
2228
            while ((current_min_error | current_max_error | iteration == 0) &
2✔
2229
                   (iteration < max_iterations)):
2230
                try:
2✔
2231
                    tmp_mismatch = fct_to_minimise(melt_f)
2✔
2232
                except RuntimeError as e:
1✔
2233
                    # check if we are at the lower limit
2234
                    if melt_f == melt_f_limits[0]:
1✔
2235
                        # check if there was already an error at the lower limit
2236
                        if was_errors[0]:
×
2237
                            raise RuntimeError('Second time with error at '
×
2238
                                               'lower limit of melt_f! '
2239
                                               'Error message of model run: '
2240
                                               f'{e}')
2241
                        else:
2242
                            was_errors[0] = True
×
2243
                            current_min_error = True
×
2244

2245
                    # check if we are at the upperlimit
2246
                    elif melt_f == melt_f_limits[1]:
1✔
2247
                        # check if there was already an error at the lower limit
2248
                        if was_errors[1]:
1✔
2249
                            raise RuntimeError('Second time with error at '
×
2250
                                               'upper limit of melt_f! '
2251
                                               'Error message of model run: '
2252
                                               f'{e}')
2253
                        else:
2254
                            was_errors[1] = True
1✔
2255
                            current_max_error = True
1✔
2256

2257
                    if current_min_error:
1✔
2258
                        # currently we searching for a new lower limit with no
2259
                        # error
2260
                        melt_f = np.round(melt_f + melt_f_search_change,
×
2261
                                          decimals=1)
2262
                    elif current_max_error:
1✔
2263
                        # currently we searching for a new upper limit with no
2264
                        # error
2265
                        melt_f = np.round(melt_f - melt_f_search_change,
1✔
2266
                                          decimals=1)
2267

2268
                    # if we end close to an already executed guess while
2269
                    # searching for a new limit we quite
2270
                    if np.isclose(melt_f, melt_f_guess).any():
1✔
2271
                        raise RuntimeError('Not able to further minimise, '
×
2272
                                           'return the best we have so far!'
2273
                                           f'Error message: {e}')
2274

2275
                    if doing_first_guess:
1✔
2276
                        # unfortunately first guess is not working
2277
                        raise RuntimeError('Dynamic calibration is not working '
1✔
2278
                                           'with first guess! Error '
2279
                                           f'message: {e}')
2280

2281
                    if np.isclose(melt_f, current_melt_f):
1✔
2282
                        # something unexpected happen so we end here
2283
                        raise RuntimeError('Unexpected error not at the limits'
×
2284
                                           f' of melt_f. Error Message: {e}')
2285

2286
                iteration += 1
2✔
2287

2288
            if iteration >= max_iterations:
2✔
2289
                # ok we were not able to find an mismatch without error
2290
                if current_min_error:
×
2291
                    raise RuntimeError('Not able to find new lower limit for '
×
2292
                                       'melt_f!')
2293
                elif current_max_error:
×
2294
                    raise RuntimeError('Not able to find new upper limit for '
×
2295
                                       'melt_f!')
2296
                else:
2297
                    raise RuntimeError('Something unexpected happened during '
×
2298
                                       'definition of new melt_f limits!')
2299
            else:
2300
                # if we found a new limit set it
2301
                if current_min_error:
2✔
2302
                    melt_f_limits[0] = copy.deepcopy(melt_f)
×
2303
                elif current_max_error:
2✔
2304
                    melt_f_limits[1] = copy.deepcopy(melt_f)
1✔
2305

2306
            if tmp_mismatch is None:
2✔
2307
                raise RuntimeError('Not able to find a new mismatch for '
1✔
2308
                                   'dmdtda!')
2309

2310
            return float(tmp_mismatch), float(melt_f)
2✔
2311

2312
        # first guess
2313
        new_mismatch, new_melt_f = get_mismatch(melt_f_initial)
2✔
2314
        melt_f_guess.append(new_melt_f)
2✔
2315
        mismatch.append(new_mismatch)
2✔
2316

2317
        if abs(mismatch[-1]) < ref_mb_err:
2✔
2318
            return mismatch[-1], new_melt_f
1✔
2319

2320
        # second (arbitrary) guess is given depending on the outcome of first
2321
        # guess, melt_f is changed for percent of mismatch relative to
2322
        # ref_mb_err times melt_f_max_step_length (if
2323
        # mismatch = 2 * ref_mb_err this corresponds to 100%; for 100% or
2324
        # 150% the next step is (-1) * melt_f_max_step_length; if mismatch
2325
        # -40%, next step is 0.4 * melt_f_max_step_length; but always at least
2326
        # an absolute change of 0.02 is imposed to prevent too close guesses).
2327
        # (-1) as if mismatch is negative we need a larger melt_f to get closer
2328
        # to 0.
2329
        step = (-1) * np.sign(mismatch[-1]) * \
2✔
2330
            max((np.abs(mismatch[-1]) - ref_mb_err) / ref_mb_err *
2331
                melt_f_max_step_length, 0.02)
2332
        new_mismatch, new_melt_f = get_mismatch(melt_f_guess[0] + step)
2✔
2333
        melt_f_guess.append(new_melt_f)
2✔
2334
        mismatch.append(new_mismatch)
2✔
2335

2336
        if abs(mismatch[-1]) < ref_mb_err:
2✔
2337
            return mismatch[-1], new_melt_f
×
2338

2339
        # Now start with splin fit for guessing
2340
        while len(melt_f_guess) < maxiter:
2✔
2341
            # get next guess from splin (fit partial linear function to
2342
            # previously calculated (mismatch, melt_f) pairs and get melt_f
2343
            # value where mismatch=0 from this fitted curve)
2344
            sort_index = np.argsort(np.array(mismatch))
2✔
2345
            tck = interpolate.splrep(np.array(mismatch)[sort_index],
2✔
2346
                                     np.array(melt_f_guess)[sort_index],
2347
                                     k=1)
2348
            # here we catch interpolation errors (two different melt_f with
2349
            # same mismatch), could happen if one melt_f was close to a newly
2350
            # defined limit
2351
            if np.isnan(tck[1]).any():
2✔
2352
                if was_errors[0]:
×
2353
                    raise RuntimeError('Second time with error at lower '
×
2354
                                       'limit of melt_f! (nan in splin fit)')
2355
                elif was_errors[1]:
×
2356
                    raise RuntimeError('Second time with error at upper '
×
2357
                                       'limit of melt_f! (nan in splin fit)')
2358
                else:
2359
                    raise RuntimeError('Not able to minimise! Problem is '
×
2360
                                       'unknown. (nan in splin fit)')
2361
            new_mismatch, new_melt_f = get_mismatch(
2✔
2362
                float(interpolate.splev(0, tck)))
2363
            melt_f_guess.append(new_melt_f)
2✔
2364
            mismatch.append(new_mismatch)
2✔
2365

2366
            if abs(mismatch[-1]) < ref_mb_err:
2✔
2367
                return mismatch[-1], new_melt_f
2✔
2368

2369
        # Ok when we end here the spinup could not find satisfying match after
2370
        # maxiter(ations)
2371
        raise RuntimeError(f'Could not find mismatch smaller '
1✔
2372
                           f'{ref_mb_err} kg m-2 yr-1 (only '
2373
                           f'{np.min(np.abs(mismatch))} kg m-2 yr-1) in '
2374
                           f'{maxiter} Iterations!')
2375

2376
    # wrapper to get values for intermediate (mismatch, melt_f) guesses if an
2377
    # error is raised
2378
    def init_minimiser():
2✔
2379
        melt_f_guess = []
2✔
2380
        mismatch = []
2✔
2381

2382
        def minimiser(fct_to_minimise):
2✔
2383
            return minimise_with_spline_fit(fct_to_minimise, melt_f_guess,
2✔
2384
                                            mismatch)
2385

2386
        return minimiser, melt_f_guess, mismatch
2✔
2387

2388
    # define function for the actual minimisation
2389
    c_fun, models_dynamic_spinup_end = init_cost_fun()
2✔
2390

2391
    # define minimiser
2392
    minimise_given_fct, melt_f_guesses, mismatch_dmdtda = init_minimiser()
2✔
2393

2394
    try:
2✔
2395
        final_mismatch, final_melt_f = minimise_given_fct(c_fun)
2✔
2396
    except RuntimeError as e:
1✔
2397
        # something happened during minimisation, if there where some
2398
        # successful runs we return the one with the best mismatch, otherwise
2399
        # we conduct just a run with no dynamic spinup
2400
        if len(mismatch_dmdtda) == 0:
1✔
2401
            # we conducted no successful run, so run without dynamic spinup
2402
            if ignore_errors:
1✔
2403
                log.info('Dynamic melt_f calibration not successful. '
1✔
2404
                         f'Error message: {e}')
2405
                model_return = fallback_run(melt_f=melt_f_initial,
1✔
2406
                                            reset=True)
2407
                return model_return
1✔
2408
            else:
2409
                raise RuntimeError('Dynamic melt_f calibration was not '
×
2410
                                   f'successful! Error Message: {e}')
2411
        else:
2412
            if ignore_errors:
1✔
2413
                log.info('Dynamic melt_f calibration not successful. Error '
1✔
2414
                         f'message: {e}')
2415

2416
                # there where some successful runs so we return the one with the
2417
                # smallest mismatch of dmdtda
2418
                min_mismatch_index = np.argmin(np.abs(mismatch_dmdtda))
1✔
2419
                melt_f_best = np.array(melt_f_guesses)[min_mismatch_index]
1✔
2420

2421
                # check if the first guess was the best guess
2422
                only_first_guess = False
1✔
2423
                if min_mismatch_index == 1:
1✔
2424
                    only_first_guess = True
×
2425

2426
                model_return = fallback_run(
1✔
2427
                    melt_f=melt_f_best, reset=False,
2428
                    best_mismatch=np.array(mismatch_dmdtda)[min_mismatch_index],
2429
                    initial_mismatch=mismatch_dmdtda[0],
2430
                    only_first_guess=only_first_guess)
2431

2432
                return model_return
1✔
2433
            else:
2434
                raise RuntimeError('Dynamic melt_f calibration not successful. '
1✔
2435
                                   f'Error message: {e}')
2436

2437
    # check that new melt_f is correctly saved in gdir
2438
    assert final_melt_f == gdir.settings['melt_f']
2✔
2439

2440
    # hurray, dynamic melt_f calibration successful
2441
    diag_dyn_melt = {
2✔
2442
        'used_spinup_option': 'dynamic melt_f calibration (full success)',
2443
        'dmdtda_mismatch_dynamic_calibration_reference': float(ref_mb),
2444
        'dmdtda_dynamic_calibration_given_error': float(ref_mb_err),
2445
        'dmdtda_dynamic_calibration_error_scaling_factor':
2446
            float(ref_mb_err_scaling_factor),
2447
        'dmdtda_mismatch_dynamic_calibration': float(final_mismatch),
2448
        'dmdtda_mismatch_with_initial_melt_f': float(mismatch_dmdtda[0]),
2449
        'melt_f_dynamic_calibration': float(final_melt_f),
2450
        'melt_f_before_dynamic_calibration': float(melt_f_initial),
2451
        'run_dynamic_melt_f_calibration_iterations':
2452
            int(dynamic_melt_f_calibration_runs[-1]),
2453
    }
2454

2455
    for k, v in diag_dyn_melt.items():
2✔
2456
        gdir.settings[k] = v
2✔
2457
        # this is only for backwards compatibility
2458
        if settings_filesuffix == '':
2✔
2459
            gdir.add_to_diagnostics(k, v)
2✔
2460

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

2463
    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