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

OGGM / oggm / 30524401593

30 Jul 2026 07:52AM UTC coverage: 84.749%. First build
30524401593

Pull #1908

github

web-flow
Merge e0ce02050 into 180c7c28f
Pull Request #1908: feat: read and write zarr methods to replace read_pickle

4440 of 6148 branches covered (72.22%)

637 of 745 new or added lines in 13 files covered. (85.5%)

14537 of 17153 relevant lines covered (84.75%)

4.16 hits per line

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

78.96
/oggm/workflow.py
1
"""Wrappers for the single tasks, multi processor handling."""
2
# Built ins
3
import logging
10✔
4
import os
10✔
5
import shutil
10✔
6
import warnings
10✔
7
from collections.abc import Sequence
10✔
8
# External libs
9
import multiprocessing
10✔
10
import numpy as np
10✔
11
import pandas as pd
10✔
12
import xarray as xr
10✔
13
from scipy import optimize as optimization
10✔
14

15
# Locals
16
import oggm
10✔
17
from oggm import cfg, tasks, utils
10✔
18
from oggm.core import centerlines, flowline, climate, gis
10✔
19
from oggm.exceptions import InvalidParamsError, InvalidWorkflowError
10✔
20
from oggm.utils import global_task, entity_task
10✔
21

22
# MPI
23
try:
10✔
24
    import oggm.mpi as ogmpi
10✔
25
    _have_ogmpi = True
×
26
except ImportError:
27
    _have_ogmpi = False
28

29
# Module logger
30
log = logging.getLogger(__name__)
10✔
31

32
# Default reference volume tables ("IceBoost v2") used by
33
# calibrate_inversion_from_ref_table, one parquet file per RGI version /
34
# subtype (RGI6, RGI7 glaciers '7G', RGI7 complexes '7C')
35
ICEBOOST_V2_BASE_URL = ('https://cluster.klima.uni-bremen.de/~oggm/'
10✔
36
                        'ice_thickness/iceboost_v2/')
37
ICEBOOST_V2_FILES = {
10✔
38
    '6': 'iceboostv2_compiled_rgi62_v20260701.parquet',
39
    '7G': 'iceboostv2_compiled_rgi70G_v20260701.parquet',
40
    '7C': 'iceboostv2_compiled_rgi70C_v20260701.parquet',
41
}
42

43
# Farinotti et al. (2019) consensus (ITMIX) reference volume table (RGI6 only)
44
CONSENSUS_REF_TABLE_URL = ('https://cluster.klima.uni-bremen.de/~oggm/g2ti/'
10✔
45
                           'rgi62_itmix_df_v20260617.parquet')
46

47
"""
10✔
48
For dealing with multiprocessing with fork. Child processes must reset
49
zarr's async globals so they don't inherit stale references that cause
50
deadlocks when starting new I/O threads.
51
"""
52
try:
10✔
53
    from zarr.core.sync import reset_resources_after_fork as _zarr_reset
10✔
54
    os.register_at_fork(after_in_child=_zarr_reset)
10✔
NEW
55
except (ImportError, AttributeError):
×
NEW
56
    pass
×
57

58
# Multiprocessing Pool
59
_mp_manager = None
10✔
60
_mp_pool = None
10✔
61

62

63
def _init_pool_globals(_cfg_contents, global_lock):
10✔
64
    cfg.unpack_config(_cfg_contents)
×
65
    utils.lock = global_lock
×
66

67

68
def init_mp_pool(reset=False):
10✔
69
    """Necessary because at import time, cfg might be uninitialized"""
70
    global _mp_manager, _mp_pool
71
    if _mp_pool and _mp_manager and not reset:
1✔
72
        return _mp_pool
1✔
73

74
    cfg.CONFIG_MODIFIED = False
1✔
75
    if _mp_pool:
1✔
76
        _mp_pool.terminate()
1✔
77
        _mp_pool.join()  # wait for workers to exit before shutting down manager
1✔
78
        _mp_pool = None
1✔
79
    if _mp_manager:
1✔
80
        cfg.set_manager(None)
1✔
81
        _mp_manager.shutdown()
1✔
82
        _mp_manager = None
1✔
83

84
    if cfg.PARAMS['use_mp_spawn']:
1✔
85
        mp = multiprocessing.get_context('spawn')
1✔
86
    else:
87
        mp = multiprocessing
1✔
88

89
    _mp_manager = mp.Manager()
1✔
90

91
    cfg.set_manager(_mp_manager)
1✔
92
    cfg_contents = cfg.pack_config()
1✔
93

94
    global_lock = _mp_manager.Lock()
1✔
95

96
    mpp = cfg.PARAMS['mp_processes']
1✔
97
    _mp_pool = mp.Pool(mpp, initializer=_init_pool_globals,
1✔
98
                       initargs=(cfg_contents, global_lock))
99
    return _mp_pool
1✔
100

101

102
def _merge_dicts(*dicts):
10✔
103
    r = {}
8✔
104
    for d in dicts:
8✔
105
        r.update(d)
8✔
106
    return r
8✔
107

108

109
class _pickle_copier(object):
10✔
110
    """Pickleable alternative to functools.partial,
111
    Which is not pickleable in python2 and thus doesn't work
112
    with Multiprocessing."""
113

114
    def __init__(self, func, kwargs):
10✔
115
        self.call_func = func
8✔
116
        self.out_kwargs = kwargs
8✔
117

118
    def _call_internal(self, call_func, gdir, kwargs):
10✔
119
        # If the function is None, assume gdir is tuple with task function
120
        if not call_func:
8!
121
            call_func, gdir = gdir
×
122

123
        # Merge main kwargs with per-task kwargs
124
        kwargs = _merge_dicts(self.out_kwargs, kwargs)
8✔
125

126
        # If gdir is a sequence, again assume it's a tuple with per-gdir kwargs.
127
        if isinstance(gdir, Sequence) and not isinstance(gdir, str):
8✔
128
            gdir, gdir_kwargs = gdir
1✔
129
            kwargs.update(gdir_kwargs)
1✔
130

131
        return call_func(gdir, **kwargs)
8✔
132

133
    def __call__(self, arg):
10✔
134
        res = None
8✔
135
        for func in self.call_func:
8✔
136
            func, kwargs = func
8✔
137
            res = self._call_internal(func, arg, kwargs)
8✔
138
        return res
8✔
139

140

141
def reset_multiprocessing():
10✔
142
    """Reset multiprocessing state
143

144
    Call this if you changed configuration parameters mid-run and need them to
145
    be re-propagated to child processes.
146
    """
147
    global _mp_pool, _mp_manager
148
    if _mp_pool:
10✔
149
        _mp_pool.terminate()
1✔
150
        _mp_pool.join()  # wait for workers to fully exit
1✔
151
        _mp_pool = None
1✔
152
    if _mp_manager:
10✔
153
        # next test should start with clean state
154
        cfg.set_manager(None)
1✔
155
        _mp_manager.shutdown()
1✔
156
        _mp_manager = None
1✔
157
    cfg.CONFIG_MODIFIED = False
10✔
158
    # Flush zarr's background async I/O threads so they don't deadlock
159
    # child processes after fork
160
    try:
10✔
161
        import zarr.core.sync as _zs
10✔
162

163
        _iothread = _zs.iothread[0]  # save ref before cleanup clears it
10✔
164
        _loop = _zs.loop[0]
10✔
165
        if _loop is not None and not _loop.is_closed():
10✔
166
            _exc = getattr(_loop, "_default_executor", None)
5✔
167
            if _exc is not None:
5!
168
                _exc.shutdown(wait=True)
5✔
169
                _loop._default_executor = None
5✔
170
        _zs.cleanup_resources()
10✔
171
        # cleanup_resources() waits only 0.2 s for iothread; ensure it is
172
        # truly dead before any subsequent fork (Pool/Manager creation).
173
        if _iothread is not None and _iothread.is_alive():
10!
NEW
174
            _iothread.join(timeout=5.0)  # TODO: Play around with timeout
×
NEW
175
    except Exception:
×
NEW
176
        pass
×
177

178

179
def execute_entity_task(task, gdirs, **kwargs):
10✔
180
    """Execute a task on gdirs.
181

182
    If you asked for multiprocessing, it will do it.
183

184
    If ``task`` has more arguments than `gdir` they have to be keyword
185
    arguments.
186

187
    Parameters
188
    ----------
189
    task : function or sequence of functions
190
         The entity task(s) to apply.
191
         Can be None, in which case each gdir is expected to be a tuple of (task, gdir).
192
         When passing a sequence, each item can also optionally be a tuple of (task, dictionary).
193
         In this case the dictionary items will be passed to the task as kwargs.
194
    gdirs : list of :py:class:`oggm.GlacierDirectory` objects
195
        The glacier directories to process.
196
        Each individual gdir can optionally be a tuple of (gdir, dictionary).
197
        In this case, the values in the dictionary will be passed to the task as
198
        keyword arguments for that specific gdir.
199

200
    Returns
201
    -------
202
    List of results from task. Last task if a list of tasks was given.
203
    """
204

205
    # Normalize task into list of tuples for simplicity
206
    if not isinstance(task, Sequence):
8✔
207
        task = [task]
8✔
208
    tasks = []
8✔
209
    for t in task:
8✔
210
        if isinstance(t, tuple):
8✔
211
            tasks.append(t)
1✔
212
        else:
213
            tasks.append((t, {}))
8✔
214

215
    # Reject global tasks
216
    for t in tasks:
8✔
217
        if t[0].__dict__.get('is_global_task', False):
8!
218
            raise InvalidWorkflowError('execute_entity_task cannot be used on '
×
219
                                       'global tasks.')
220

221
    # Should be iterable
222
    gdirs = utils.tolist(gdirs)
8✔
223
    ng = len(gdirs)
8✔
224
    if ng == 0:
8✔
225
        log.workflow('Called execute_entity_task on 0 glaciers. Returning...')
1✔
226
        return
1✔
227

228
    log.workflow('Execute entity tasks [%s] on %d glaciers',
8✔
229
                 ', '.join([t[0].__name__ for t in tasks]), ng)
230

231
    pc = _pickle_copier(tasks, kwargs)
8✔
232

233
    if _have_ogmpi:
8!
234
        if ogmpi.OGGM_MPI_COMM is not None:
×
235
            return ogmpi.mpi_master_spin_tasks(pc, gdirs)
×
236

237
    if cfg.PARAMS['use_multiprocessing'] and ng > 1:
8✔
238
        mppool = init_mp_pool(cfg.CONFIG_MODIFIED)
1✔
239
        out = mppool.map(pc, gdirs, chunksize=1)
1✔
240
    else:
241
        if ng > 3:
8✔
242
            log.workflow('WARNING: you are trying to run an entity task on '
4✔
243
                         '%d glaciers with multiprocessing turned off. OGGM '
244
                         'will run faster with multiprocessing turned on.', ng)
245
        out = [pc(gdir) for gdir in gdirs]
8✔
246

247
    return out
8✔
248

249

250
def execute_parallel_tasks(gdir, tasks):
10✔
251
    """Execute a list of task on a single gdir (experimental!).
252

253
    This is useful when running a non-sequential list of task on a gdir,
254
    mostly for e.g. different experiments with different output files.
255

256
    Parameters
257
    ----------
258
    gdir : :py:class:`oggm.GlacierDirectory`
259
         the directory to process.
260
    tasks : list
261
         the the list of entity tasks to apply.
262
         Optionally, each list element can be a tuple, with the first element
263
         being the task, and the second element a dict that
264
         will be passed to the task function as ``**kwargs``.
265
    """
266

267
    pc = _pickle_copier(None, {})
1✔
268

269
    _tasks = []
1✔
270
    for task in tasks:
1✔
271
        kwargs = {}
1✔
272
        if isinstance(task, Sequence):
1!
273
            task, kwargs = task
1✔
274
        _tasks.append((task, (gdir, kwargs)))
1✔
275

276
    if _have_ogmpi:
1!
277
        if ogmpi.OGGM_MPI_COMM is not None:
×
278
            ogmpi.mpi_master_spin_tasks(pc, _tasks)
×
279
            return
×
280

281
    if cfg.PARAMS['use_multiprocessing']:
1!
282
        mppool = init_mp_pool(cfg.CONFIG_MODIFIED)
×
283
        mppool.map(pc, _tasks, chunksize=1)
×
284
    else:
285
        for task, (gd, kw) in _tasks:
1✔
286
            task(gd, **kw)
1✔
287

288

289
def gdir_from_prepro(entity, from_prepro_level=None,
10✔
290
                     prepro_border=None, prepro_rgi_version=None,
291
                     base_url=None):
292

293
    if prepro_border is None:
5✔
294
        prepro_border = int(cfg.PARAMS['border'])
1✔
295
    if prepro_rgi_version is None:
5✔
296
        prepro_rgi_version = cfg.PARAMS['rgi_version']
4✔
297

298
    if isinstance(entity, pd.Series):
5!
299
        try:
×
300
            rid = entity.RGIId
×
301
        except AttributeError:
×
302
            rid = entity.rgi_id
×
303
    else:
304
        rid = entity
5✔
305

306
    tar_base = utils.get_prepro_gdir(prepro_rgi_version, rid, prepro_border,
5✔
307
                                     from_prepro_level, base_url=base_url)
308
    from_tar = os.path.join(tar_base.replace('.tar', ''), rid + '.tar.gz')
5✔
309
    return oggm.GlacierDirectory(entity, from_tar=from_tar)
5✔
310

311

312
def gdir_from_tar(entity, from_tar):
10✔
313

314
    try:
1✔
315
        rgi_id = entity.RGIId
1✔
316
    except AttributeError:
×
317
        rgi_id = entity
×
318

319
    # The region dir, the new 100-glacier bundle name and the old
320
    # 1000-glacier bundle name use the same slices for RGI6 and RGI7.
321
    # TODO: add support for bundle sizes of 10 and 1
322
    region = rgi_id[:-6]
1✔
323
    new_bundle = f"{region}.{rgi_id[-5:-2]}"
1✔
324
    new_path = os.path.join(from_tar, region, new_bundle + ".tar")
1✔
325
    old_path = os.path.join(from_tar, region, rgi_id[:-3] + ".tar")
1✔
326
    if os.path.exists(new_path):
1✔
327
        from_tar = new_path
1✔
328
    elif os.path.exists(old_path):
1!
329
        from_tar = old_path
1✔
330
    else:
331
        raise FileNotFoundError(
×
332
            "Cannot find bundle tar for {} in {}".format(rgi_id, from_tar)
333
        )
334
    from_tar = os.path.join(from_tar.replace(".tar", ""), rgi_id + ".tar.gz")
1✔
335
    return oggm.GlacierDirectory(entity, from_tar=from_tar)
1✔
336

337

338
def _check_rgi_input(rgidf=None, err_on_lvl2=False):
10✔
339
    """Complain if the input has duplicates."""
340

341
    if rgidf is None:
8!
342
        return
×
343

344
    msg = ('You have glaciers with connectivity level 2 in your list. '
8✔
345
           'OGGM does not provide pre-processed directories for these.')
346

347
    # Check if dataframe or list of strs
348
    is_dataframe = isinstance(rgidf, pd.DataFrame)
8✔
349
    if is_dataframe:
8✔
350
        try:
6✔
351
            rgi_ids = rgidf.RGIId
6✔
352
            # if dataframe we can also check for connectivity
353
            if 'Connect' in rgidf and np.any(rgidf['Connect'] == 2):
6!
354
                if err_on_lvl2:
×
355
                    raise RuntimeError(msg)
×
356
        except AttributeError:
2✔
357
            # RGI7
358
            rgi_ids = rgidf.rgi_id
2✔
359
    else:
360
        rgi_ids = utils.tolist(rgidf)
6✔
361
        # Check for Connectivity level 2 here as well
362
        not_good_ids = pd.read_csv(utils.get_demo_file('rgi6_ids_conn_lvl2.csv'),
6✔
363
                                   index_col=0)
364
        try:
6✔
365
            if err_on_lvl2 and len(not_good_ids.loc[rgi_ids]) > 0:
6!
366
                raise RuntimeError(msg)
×
367
        except KeyError:
5✔
368
            # Were good
369
            pass
5✔
370

371
    u, c = np.unique(rgi_ids, return_counts=True)
8✔
372
    if len(u) < len(rgi_ids):
8✔
373
        raise InvalidWorkflowError('Found duplicates in the list of '
1✔
374
                                   'RGI IDs: {}'.format(u[c > 1]))
375

376

377
def _isdir(path):
10✔
378
    """os.path.isdir, returning False instead of an error on non-string/path-like objects
379
    """
380
    if isinstance(path, bool):
6✔
381
        return False
6✔
382
    if not isinstance(path, (str, bytes, os.PathLike)):
1!
383
        return False
×
384
    try:
1✔
385
        return os.path.isdir(path)
1✔
386
    except TypeError:
×
387
        return False
×
388

389

390
def init_glacier_directories(rgidf=None, *, reset=False, force=False,
10✔
391
                             from_prepro_level=None, prepro_border=None,
392
                             prepro_rgi_version=None, prepro_base_url=None,
393
                             from_tar=False, delete_tar=False):
394
    """Initializes the list of Glacier Directories for this run.
395

396
    This is the very first task to do (always). If the directories are already
397
    available in the working directory, use them. If not, create new ones.
398

399
    **Careful**: when starting from a pre-processed directory with
400
    `from_prepro_level` or `from_tar`, the existing directories will be overwritten!
401

402
    Parameters
403
    ----------
404
    rgidf : GeoDataFrame or list of ids, optional for pre-computed runs
405
        the RGI glacier outlines. If unavailable, OGGM will parse the
406
        information from the glacier directories found in the working
407
        directory. It is required for new runs.
408
    reset : bool
409
        delete the existing glacier directories if found.
410
    force : bool
411
        setting `reset=True` will trigger a yes/no question to the user. Set
412
        `force=True` to avoid this.
413
    from_prepro_level : int
414
        get the gdir data from the official pre-processed pool. If this
415
        argument is set, the existing directories will be overwritten!
416
    prepro_border : int
417
        for `from_prepro_level` only: if you want to override the default
418
        behavior which is to use `cfg.PARAMS['border']`
419
    prepro_rgi_version : str
420
        for `from_prepro_level` only: if you want to override the default
421
        behavior which is to use `cfg.PARAMS['rgi_version']`
422
    prepro_base_url : str
423
        for `from_prepro_level` only: the preprocessed directory url from
424
        which to download the directories (became mandatory in OGGM v1.6)
425
    from_tar : bool or str, default=False
426
        extract the gdir data from a tar file. If set to `True`,
427
        will check for a tar file at the expected location in `base_dir`.
428
        delete the original tar file after extraction. If this
429
        argument is set, the existing directories will be overwritten!
430

431
    Returns
432
    -------
433
    gdirs : list of :py:class:`oggm.GlacierDirectory` objects
434
        the initialised glacier directories
435
    """
436

437
    _check_rgi_input(rgidf, err_on_lvl2=from_prepro_level)
8✔
438

439
    if reset and not force:
8!
440
        reset = utils.query_yes_no('Delete all glacier directories?')
×
441

442
    if from_prepro_level:
8✔
443
        url = utils.get_prepro_base_url(base_url=prepro_base_url,
5✔
444
                                        border=prepro_border,
445
                                        prepro_level=from_prepro_level,
446
                                        rgi_version=prepro_rgi_version)
447
        if cfg.PARAMS['has_internet'] and not utils.url_exists(url):
5!
448
            raise InvalidParamsError("base url seems unreachable with these "
×
449
                                     "parameters: {}".format(url))
450
        if ('oggm_v1.4' in url and
5✔
451
                from_prepro_level >= 3 and
452
                not cfg.PARAMS['prcp_fac']):
453
            log.warning('You seem to be using v1.4 directories with a more '
1✔
454
                        'recent version of OGGM. While this is possible, be '
455
                        'aware that some defaults parameters have changed. '
456
                        'See the documentation for details: '
457
                        'http://docs.oggm.org/en/stable/whats-new.html')
458

459
    # if reset delete also the log directory
460
    if reset:
8✔
461
        fpath = os.path.join(cfg.PATHS['working_dir'], 'log')
1✔
462
        if os.path.exists(fpath):
1!
463
            shutil.rmtree(fpath)
×
464

465
    if rgidf is None:
8!
466
        # Infer the glacier directories from folders available in working dir
467
        if reset:
×
468
            raise ValueError('Cannot use reset without setting rgidf')
×
469
        log.workflow('init_glacier_directories by parsing all available '
×
470
                     'folders (this takes time: if possible, provide rgidf '
471
                     'instead).')
472
        # The dirs should be there already
473
        gl_dir = os.path.join(cfg.PATHS['working_dir'], 'per_glacier')
×
474
        gdirs = []
×
475
        for root, _, files in os.walk(gl_dir):
×
476
            if files and ('outlines.shp' in files or
×
477
                          'outlines.tar.gz' in files):
478
                gdirs.append(oggm.GlacierDirectory(os.path.basename(root)))
×
479
    else:
480
        # Create glacier directories from input
481
        # Check if dataframe or list of str
482
        try:
8✔
483
            entities = []
8✔
484
            for _, entity in rgidf.iterrows():
8✔
485
                entities.append(entity)
6✔
486
        except AttributeError:
5✔
487
            entities = utils.tolist(rgidf)
5✔
488

489
        if from_prepro_level is not None:
8✔
490
            log.workflow('init_glacier_directories from prepro level {} on '
5✔
491
                         '{} glaciers.'.format(from_prepro_level,
492
                                               len(entities)))
493
            gdirs = execute_entity_task(gdir_from_prepro, entities,
5✔
494
                                        from_prepro_level=from_prepro_level,
495
                                        prepro_border=prepro_border,
496
                                        prepro_rgi_version=prepro_rgi_version,
497
                                        base_url=prepro_base_url)
498
        else:
499
            # We can set the intersects file automatically here
500
            if (cfg.PARAMS['use_intersects'] and
6✔
501
                    len(cfg.PARAMS['intersects_gdf']) == 0 and
502
                    not from_tar):
503
                try:
2✔
504
                    rgi_ids = np.unique(np.sort([entity.rgi_id for entity in
2✔
505
                                                 entities]))
506
                    if len(rgi_ids[0]) == 23:
2!
507
                        # RGI7
508
                        assert rgi_ids[0].split('-')[1] == 'v7.0'
2✔
509
                        if rgi_ids[0].split('-')[2] == 'C':
2!
510
                            # No need for interstects
511
                            fp = []
2✔
512
                            rgi_version = '70C'
2✔
513
                        else:
514
                            rgi_version = '70G'
×
515
                            fp = utils.get_rgi_intersects_entities(rgi_ids,
×
516
                                                                   version=rgi_version)
517

518
                    else:
519
                        rgi_version = rgi_ids[0].split('-')[0][-2:]
×
520
                        if rgi_version == '60':
×
521
                            rgi_version = '62'
×
522
                        fp = utils.get_rgi_intersects_entities(rgi_ids,
×
523
                                                               version=rgi_version)
524
                    cfg.set_intersects_db(fp)
2✔
525
                except AttributeError:
×
526
                    # RGI V6
527
                    try:
×
528
                        rgi_ids = np.unique(np.sort([entity.RGIId for entity in
×
529
                                                     entities]))
530
                        rgi_version = rgi_ids[0].split('-')[0][-2:]
×
531
                        if rgi_version == '60':
×
532
                            rgi_version = '62'
×
533
                        fp = utils.get_rgi_intersects_entities(rgi_ids,
×
534
                                                               version=rgi_version)
535
                        cfg.set_intersects_db(fp)
×
536
                    except AttributeError:
×
537
                        # List of str
538
                        pass
×
539

540
            if _isdir(from_tar):
6✔
541
                gdirs = execute_entity_task(gdir_from_tar, entities,
1✔
542
                                            from_tar=from_tar)
543
            else:
544
                gdirs = execute_entity_task(utils.GlacierDirectory, entities,
6✔
545
                                            reset=reset,
546
                                            from_tar=from_tar,
547
                                            delete_tar=delete_tar)
548

549
    return gdirs
8✔
550

551

552
@global_task(log)
10✔
553
def gis_prepro_tasks(gdirs):
10✔
554
    """Run all flowline preprocessing tasks on a list of glaciers.
555

556
    Parameters
557
    ----------
558
    gdirs : list of :py:class:`oggm.GlacierDirectory` objects
559
        the glacier directories to process
560
    """
561

562
    task_list = [
4✔
563
        tasks.define_glacier_region,
564
        tasks.glacier_masks,
565
        tasks.compute_centerlines,
566
        tasks.initialize_flowlines,
567
        tasks.compute_downstream_line,
568
        tasks.compute_downstream_bedshape,
569
        tasks.catchment_area,
570
        tasks.catchment_intersections,
571
        tasks.catchment_width_geom,
572
        tasks.catchment_width_correction
573
    ]
574
    for task in task_list:
4✔
575
        execute_entity_task(task, gdirs)
4✔
576

577

578
@global_task(log)
10✔
579
def climate_tasks(gdirs, settings_filesuffix='', input_filesuffix=None,
10✔
580
                  overwrite_gdir=False, override_missing=None):
581
    """Run all climate related entity tasks on a list of glaciers.
582
    Parameters
583
    ----------
584
    gdirs : list of :py:class:`oggm.GlacierDirectory` objects
585
        the glacier directories to process
586
    input_filesuffix: str
587
        the filesuffix of the input inversion flowlines which should be used
588
        (useful for conducting multiple experiments in the same gdir)
589
    """
590

591
    # Process climate data
592
    execute_entity_task(tasks.process_climate_data, gdirs,
2✔
593
                        settings_filesuffix=settings_filesuffix)
594
    # mass balance and the apparent mass balance
595
    execute_entity_task(tasks.mb_calibration_from_geodetic_mb, gdirs,
2✔
596
                        settings_filesuffix=settings_filesuffix,
597
                        override_missing=override_missing,
598
                        overwrite_gdir=overwrite_gdir)
599
    execute_entity_task(tasks.apparent_mb_from_any_mb, gdirs,
2✔
600
                        settings_filesuffix=settings_filesuffix,
601
                        input_filesuffix=input_filesuffix,)
602

603

604
@global_task(log)
10✔
605
def inversion_tasks(gdirs, settings_filesuffix='', input_filesuffix=None,
10✔
606
                    output_filesuffix=None,
607
                    glen_a=None, fs=None, filter_inversion_output=True,
608
                    add_to_log_file=True):
609
    """Run all ice thickness inversion tasks on a list of glaciers.
610

611
    Quite useful to deal with calving glaciers as well.
612

613
    Parameters
614
    ----------
615
    gdirs : list of :py:class:`oggm.GlacierDirectory` objects
616
        the glacier directories to process
617
    settings_filesuffix: str
618
        You can use a different set of settings by providing a filesuffix. This
619
        is useful for sensitivity experiments.
620
    input_filesuffix: str
621
        The filesuffix of the input inversion flowlines. If None the
622
        settings_filesuffix will be used.
623
    output_filesuffix: str
624
        The filesuffix used for saving resulting inversion files to the gdir. If
625
        None the settings_filesuffix will be used.
626
    add_to_log_file : bool
627
        if the called entity tasks should write into log of gdir. Default True
628
    """
629

630
    if input_filesuffix is None:
7✔
631
        input_filesuffix = settings_filesuffix
4✔
632

633
    if output_filesuffix is None:
7✔
634
        output_filesuffix = settings_filesuffix
4✔
635

636
    # We use the settings of the first gdir for defining general parameters
637
    gdirs[0].settings_filesuffix = settings_filesuffix
7✔
638

639
    if gdirs[0].settings['use_kcalving_for_inversion']:
7✔
640
        # Differentiate between calving and non-calving glaciers
641
        gdirs_nc = []
3✔
642
        gdirs_c = []
3✔
643
        for gd in gdirs:
3✔
644
            if gd.is_tidewater:
3✔
645
                gdirs_c.append(gd)
3✔
646
            else:
647
                gdirs_nc.append(gd)
2✔
648

649
        log.workflow('Starting inversion tasks for {} tidewater and {} '
3✔
650
                     'non-tidewater glaciers.'.format(len(gdirs_c),
651
                                                      len(gdirs_nc)))
652

653
        if gdirs_nc:
3✔
654
            execute_entity_task(tasks.prepare_for_inversion, gdirs_nc,
2✔
655
                                settings_filesuffix=settings_filesuffix,
656
                                # only use input_filesuffix for first task as
657
                                # subsequent task use the results of previous
658
                                # tasks
659
                                input_filesuffix=input_filesuffix,
660
                                output_filesuffix=output_filesuffix,
661
                                add_to_log_file=add_to_log_file)
662
            execute_entity_task(tasks.mass_conservation_inversion, gdirs_nc,
2✔
663
                                settings_filesuffix=settings_filesuffix,
664
                                input_filesuffix=output_filesuffix,
665
                                output_filesuffix=output_filesuffix,
666
                                glen_a=glen_a, fs=fs,
667
                                add_to_log_file=add_to_log_file)
668
            if filter_inversion_output:
2!
669
                execute_entity_task(tasks.filter_inversion_output, gdirs_nc,
2✔
670
                                    settings_filesuffix=settings_filesuffix,
671
                                    input_filesuffix=output_filesuffix,
672
                                    output_filesuffix=output_filesuffix,
673
                                    add_to_log_file=add_to_log_file)
674

675
        if gdirs_c:
3!
676
            execute_entity_task(tasks.find_inversion_calving_from_any_mb,
3✔
677
                                gdirs_c,
678
                                settings_filesuffix=settings_filesuffix,
679
                                input_filesuffix=output_filesuffix,
680
                                output_filesuffix=output_filesuffix,
681
                                glen_a=glen_a, fs=fs,
682
                                add_to_log_file=add_to_log_file)
683
    else:
684
        execute_entity_task(tasks.prepare_for_inversion, gdirs,
7✔
685
                            settings_filesuffix=settings_filesuffix,
686
                            # only use input_filesuffix for first task as
687
                            # subsequent task use the results of previous
688
                            # tasks
689
                            input_filesuffix=input_filesuffix,
690
                            output_filesuffix=output_filesuffix,
691
                            add_to_log_file=add_to_log_file)
692
        execute_entity_task(tasks.mass_conservation_inversion, gdirs,
7✔
693
                            settings_filesuffix=settings_filesuffix,
694
                            input_filesuffix=output_filesuffix,
695
                            output_filesuffix=output_filesuffix,
696
                            glen_a=glen_a, fs=fs,
697
                            add_to_log_file=add_to_log_file)
698
        if filter_inversion_output:
7!
699
            execute_entity_task(tasks.filter_inversion_output, gdirs,
7✔
700
                                settings_filesuffix=settings_filesuffix,
701
                                input_filesuffix=output_filesuffix,
702
                                output_filesuffix=output_filesuffix,
703
                                add_to_log_file=add_to_log_file)
704

705

706
def _read_ref_table_file(fpath):
10✔
707
    """Read a reference volume table from a parquet or hdf file."""
708
    if str(fpath).endswith('.parquet'):
4!
709
        return pd.read_parquet(fpath)
4✔
710
    return pd.read_hdf(fpath)
×
711

712

713
def _resolve_ref_volume_table(gdirs, ref_table):
10✔
714
    """Load and normalise a reference volume table.
715

716
    Parameters
717
    ----------
718
    gdirs : list of :py:class:`oggm.GlacierDirectory` objects
719
        used to pick the default table based on the RGI version
720
    ref_table : None or pd.DataFrame or str
721
        the reference table to use (see
722
        :py:func:`calibrate_inversion_from_ref_table`)
723

724
    Returns
725
    -------
726
    (df, ref_col) : the reference dataframe (indexed by RGI id) and the name
727
        of the column holding the reference volume, in m3.
728
    """
729
    if ref_table is None:
4!
730
        ref_table = 'iceboost'
×
731

732
    if isinstance(ref_table, pd.DataFrame):
4!
733
        df = ref_table.copy()
×
734
    elif ref_table == 'iceboost':
4!
735
        # Pick the IceBoost v2 table matching the RGI version.
736
        # gdir.rgi_version is '6x' for RGI6 and '70G'/'70C' for RGI7.
737
        rgi_version = gdirs[0].rgi_version
×
738
        key = '6' if rgi_version[0] == '6' else rgi_version[0] + rgi_version[-1]
×
739
        try:
×
740
            fname = ICEBOOST_V2_FILES[key]
×
741
        except KeyError:
×
742
            raise InvalidParamsError(
×
743
                'No IceBoost reference volume table available for RGI version '
744
                '"{}". Provide one with the `ref_table` argument.'
745
                ''.format(rgi_version))
746
        fpath = utils.file_downloader(ICEBOOST_V2_BASE_URL + fname)
×
747
        df = _read_ref_table_file(fpath)
×
748
    elif ref_table == 'consensus':
4!
749
        # Farinotti et al. (2019) consensus (ITMIX) estimate (RGI6 only)
750
        fpath = utils.file_downloader(CONSENSUS_REF_TABLE_URL)
4✔
751
        df = _read_ref_table_file(fpath)
4✔
752
    else:
753
        # A local path or a URL to a parquet or hdf file
754
        fpath = ref_table
×
755
        if '://' in str(ref_table):
×
756
            fpath = utils.file_downloader(ref_table)
×
757
        df = _read_ref_table_file(fpath)
×
758

759
    # Normalise the reference volume to a column in m3
760
    if 'vol_itmix_m3' in df.columns:
4!
761
        # Legacy consensus (ITMIX) table, already in m3
762
        ref_col = 'vol_itmix_m3'
4✔
763
    elif 'vol_km3' in df.columns:
×
764
        # IceBoost tables store the volume in km3
765
        ref_col = 'vol_ref_m3'
×
766
        df[ref_col] = df['vol_km3'] * 1e9
×
767
    else:
768
        raise InvalidParamsError(
×
769
            'The reference volume table must contain either a `vol_km3` or a '
770
            '`vol_itmix_m3` column, but has: {}'.format(list(df.columns)))
771

772
    return df, ref_col
4✔
773

774

775
@global_task(log)
10✔
776
def calibrate_inversion_from_ref_table(gdirs, settings_filesuffix='',
10✔
777
                                       observations_filesuffix='',
778
                                       overwrite_observations=True,
779
                                       ref_volume_m3=None,
780
                                       ref_volume_year=None,
781
                                       rgi_ids_in_ref_volume=None,
782
                                       input_filesuffix=None,
783
                                       output_filesuffix=None,
784
                                       ref_table=None,
785
                                       ignore_missing=True,
786
                                       fs=0, a_bounds=(0.1, 10),
787
                                       apply_fs_on_mismatch=False,
788
                                       error_on_mismatch=True,
789
                                       filter_inversion_output=True,
790
                                       add_to_log_file=True):
791
    """Fit the total volume of the glaciers to a reference volume table.
792

793
    This method finds the "best Glen A" to match all glaciers in gdirs with
794
    a valid inverted volume.
795

796
    Parameters
797
    ----------
798
    gdirs : list of :py:class:`oggm.GlacierDirectory` objects
799
        the glacier directories to process
800
    settings_filesuffix: str
801
        You can use a different set of settings by providing a filesuffix. This
802
        is useful for sensitivity experiments.
803
    observations_filesuffix: str
804
        You can provide a filesuffix for the reference volume to use. If you
805
        provide ref_volume_m3, then this values will be stored in the
806
        observations file, if ref_volume_m3 is not already present. If you want
807
        to force to use the provided values and override the current ones, set
808
        overwrite_observations to True.
809
    overwrite_observations : bool
810
        If you want to overwrite already existing observation values in the
811
        provided observations file set this to True. If this is False the
812
        volumes saved in the observation file are used as the reference.
813
        Default is True.
814
    ref_volume_m3 : float
815
        Option to give an own total glacier volume to match to
816
    ref_volume_year : int or None
817
        The year when the reference volume is valid. If None the RGI date is
818
        used.
819
    rgi_ids_in_ref_volume : list or None
820
        If the reference volume is only valid for part of the provided gdirs,
821
        because some glaciers do not have a volume estimate available. But in
822
        the end the inversion will be performed on all gdirs. If None all gdirs
823
        are used. Default is None.
824
    input_filesuffix: str
825
        The filesuffix of the input inversion flowlines. If None the
826
        settings_filesuffix will be used.
827
    output_filesuffix: str
828
        The filesuffix used for saving resulting inversion files to the gdir. If
829
        None the settings_filesuffix will be used.
830
    ref_table : None or str or pd.DataFrame
831
        the reference volume table to calibrate against. One of:
832
        - ``'iceboost'`` (the default, also selected when None): the IceBoost
833
          v2 product matching the RGI version of the glaciers is downloaded
834
          and used.
835
        - ``'consensus'``: the Farinotti et al. (2019) consensus (ITMIX)
836
          estimate (RGI6 only).
837
        - a pandas DataFrame, or a path/URL to a parquet or hdf file: a custom
838
          table, indexed by RGI id. It must contain either a ``vol_km3``
839
          column (volume in km3, as in the IceBoost products, the recommended
840
          format) or a ``vol_itmix_m3`` column (volume in m3, as in the
841
          consensus estimate).
842
    ignore_missing : bool
843
        set this to true to silence the error if some glaciers could not be
844
        found in the reference table.
845
    fs : float
846
        invert with sliding (default: no)
847
    a_bounds: tuple
848
        factor to apply to default A
849
    apply_fs_on_mismatch: false
850
        on mismatch, try to apply an arbitrary value of fs (fs = 5.7e-20 from
851
        Oerlemans) and try to optimize A again.
852
    error_on_mismatch: bool
853
        sometimes the given bounds do not allow to find a zero mismatch:
854
        this will normally raise an error, but you can switch this off,
855
        use the closest value instead and move on.
856
    filter_inversion_output : bool
857
        whether or not to apply terminus thickness filtering on the inversion
858
        output (needs the downstream lines to work).
859
    add_to_log_file : bool
860
        if the called entity tasks should write into log of gdir. Default True
861

862
    Returns
863
    -------
864
    a dataframe with the individual glacier volumes
865
    """
866

867
    if input_filesuffix is None:
6✔
868
        input_filesuffix = settings_filesuffix
6✔
869

870
    if output_filesuffix is None:
6✔
871
        output_filesuffix = settings_filesuffix
6✔
872

873
    gdirs = utils.tolist(gdirs)
6✔
874
    rids = [gdir.rgi_id for gdir in gdirs]
6✔
875

876
    for gdir in gdirs:
6✔
877
        gdir.observations_filesuffix = observations_filesuffix
6✔
878

879
    # check if reference volume referes to all gdirs
880
    if rgi_ids_in_ref_volume is not None:
6✔
881
        gdirs_use = [gdir for gdir in gdirs
2✔
882
                     if gdir.rgi_id in rgi_ids_in_ref_volume]
883
        rids_use = [gdir.rgi_id for gdir in gdirs_use]
2✔
884
    else:
885
        gdirs_use = gdirs
6✔
886
        rids_use = rids
6✔
887

888
    if overwrite_observations:
6✔
889
        # A per-glacier reference table is only needed when matching individual
890
        # volumes. When matching a single total volume (ref_volume_m3) and
891
        # no table was explicitly provided, we skip loading/downloading it.
892
        if ref_volume_m3 is not None and ref_table is None:
6✔
893
            df = pd.DataFrame(index=rids_use)
5✔
894
            ref_col = None
5✔
895
        else:
896
            # Get the ref data for the glaciers we have
897
            df, ref_col = _resolve_ref_volume_table(gdirs, ref_table)
4✔
898

899
            found_ids = df.index.intersection(rids)
4✔
900
            if not ignore_missing and (len(found_ids) != len(rids)):
4!
901
                raise InvalidWorkflowError('Could not find matching indices in the '
×
902
                                           'reference table for all provided '
903
                                           'glaciers. Set ignore_missing=True to '
904
                                           'ignore this error.')
905

906
            df = df.reindex(rids)
4✔
907
    else:
908
        ref_volume_m3_file = sum([gdir.observations['ref_volume_m3']['value']
2✔
909
                                  for gdir in gdirs_use])
910
        if ref_volume_m3 is None:
2✔
911
            # if no reference volume is provided use the one from the obs-file
912
            ref_volume_m3 = ref_volume_m3_file
2✔
913
        elif np.isclose(ref_volume_m3_file, ref_volume_m3, rtol=1e-2):
2!
914
            # ok the provided ref volume is the same as stored in the obs-file
915
            pass
×
916
        elif not overwrite_observations:
2!
917
            raise InvalidWorkflowError(
2✔
918
                'You have provided an reference volume, but their is already '
919
                'one stored in the current observations file (filesuffix = '
920
                f'{observations_filesuffix})! If you want to overwrite set '
921
                f'overwrite_observations = True.')
922
        else:
923
            for gdir in gdirs:
×
924
                if 'ref_volume_m3' in gdir.observations:
×
925
                    gdir.observations['ref_volume_m3']['value'] = None
×
926

927
        df = pd.DataFrame(index=rids_use)
2✔
928
        ref_col = None
2✔
929

930
    # Optimize the diff to ref, using the settings of the first gdir
931
    gdirs_use[0].settings_filesuffix = settings_filesuffix
6✔
932
    def_a = gdirs_use[0].settings['inversion_glen_a']
6✔
933

934
    def compute_vol(x):
6✔
935
        inversion_tasks(gdirs_use, settings_filesuffix=settings_filesuffix,
6✔
936
                        input_filesuffix=input_filesuffix,
937
                        output_filesuffix=output_filesuffix,
938
                        glen_a=x*def_a, fs=fs,
939
                        filter_inversion_output=filter_inversion_output,
940
                        add_to_log_file=add_to_log_file)
941
        odf = df.copy()
6✔
942
        odf['oggm'] = execute_entity_task(tasks.get_inversion_volume, gdirs_use,
6✔
943
                                          input_filesuffix=output_filesuffix,
944
                                          add_to_log_file=add_to_log_file)
945
        # if the user provides a glacier volume all glaciers are considered,
946
        # dropna() below excludes glaciers with no reference volume available
947
        if ref_volume_m3 is None:
6✔
948
            return odf.dropna(subset=[ref_col, 'oggm'])
4✔
949
        else:
950
            return odf
5✔
951

952
    def to_minimize(x):
6✔
953
        log.workflow('Reference volume optimisation with '
6✔
954
                     'A factor: {} and fs: {}'.format(x, fs))
955
        odf = compute_vol(x)
6✔
956
        if ref_volume_m3 is None:
6✔
957
            return odf[ref_col].sum() - odf.oggm.sum()
4✔
958
        else:
959
            return ref_volume_m3 - odf.oggm.sum()
5✔
960

961
    try:
6✔
962
        out_fac, r = optimization.brentq(to_minimize, *a_bounds, rtol=1e-2,
6✔
963
                                         full_output=True)
964
        if r.converged:
6!
965
            log.workflow('calibrate_inversion_from_ref_table '
6✔
966
                         'converged after {} iterations and fs={}. The '
967
                         'resulting Glen A factor is {}.'
968
                         ''.format(r.iterations, fs, out_fac))
969
        else:
970
            raise ValueError('Unexpected error in optimization.brentq')
×
971
    except ValueError:
1✔
972
        # Ok can't find an A. Log for debug:
973
        odf1 = compute_vol(a_bounds[0]).sum() * 1e-9
1✔
974
        odf2 = compute_vol(a_bounds[1]).sum() * 1e-9
1✔
975
        if ref_volume_m3 is None:
1!
976
            ref_vol_1 = odf1[ref_col]
1✔
977
            ref_vol_2 = odf2[ref_col]
1✔
978
        else:
979
            ref_vol_1 = ref_volume_m3 * 1e-9
×
980
            ref_vol_2 = ref_volume_m3 * 1e-9
×
981
        msg = ('calibration from reference table CAN\'T converge with fs={}.\n'
1✔
982
               'Bound values (km3):\nRef={:.3f} OGGM={:.3f} for A factor {}\n'
983
               'Ref={:.3f} OGGM={:.3f} for A factor {}'
984
               ''.format(fs,
985
                         ref_vol_1, odf1.oggm, a_bounds[0],
986
                         ref_vol_2, odf2.oggm, a_bounds[1]))
987
        if apply_fs_on_mismatch and fs == 0 and odf2.oggm > ref_vol_2:
1✔
988
            do_filter = filter_inversion_output
1✔
989
            return calibrate_inversion_from_ref_table(
1✔
990
                gdirs,
991
                settings_filesuffix=settings_filesuffix,
992
                observations_filesuffix=observations_filesuffix,
993
                overwrite_observations=overwrite_observations,
994
                ref_volume_m3=ref_volume_m3,
995
                ref_volume_year=ref_volume_year,
996
                rgi_ids_in_ref_volume=rgi_ids_in_ref_volume,
997
                input_filesuffix=input_filesuffix,
998
                output_filesuffix=output_filesuffix,
999
                ref_table=ref_table,
1000
                ignore_missing=ignore_missing,
1001
                fs=5.7e-20, a_bounds=a_bounds,
1002
                apply_fs_on_mismatch=False,
1003
                error_on_mismatch=error_on_mismatch,
1004
                filter_inversion_output=do_filter,
1005
                add_to_log_file=add_to_log_file)
1006
        if error_on_mismatch:
1✔
1007
            raise ValueError(msg)
1✔
1008

1009
        out_fac = a_bounds[int(abs(ref_vol_1 - odf1.oggm) >
1✔
1010
                               abs(ref_vol_2 - odf2.oggm))]
1011
        log.workflow(msg)
1✔
1012
        log.workflow('We use A factor = {} and fs = {} and move on.'
1✔
1013
                     ''.format(out_fac, fs))
1014

1015
    # Compute the final volume with the correct A for all gdirs
1016
    if len(rids_use) != len(rids):
6✔
1017
        df = pd.DataFrame(index=rids)
2✔
1018
    inversion_tasks(gdirs, settings_filesuffix=settings_filesuffix,
6✔
1019
                    input_filesuffix=input_filesuffix,
1020
                    output_filesuffix=output_filesuffix,
1021
                    glen_a=out_fac*def_a, fs=fs,
1022
                    filter_inversion_output=filter_inversion_output,
1023
                    add_to_log_file=add_to_log_file)
1024
    df['vol_oggm_m3'] = execute_entity_task(tasks.get_inversion_volume, gdirs,
6✔
1025
                                            input_filesuffix=output_filesuffix,
1026
                                            add_to_log_file=add_to_log_file)
1027
    # add the actually derived volume to the observations file
1028
    for gdir in gdirs:
6✔
1029
        # ensure the observation is written into the correct file
1030
        gdir.observations_filesuffix = observations_filesuffix
6✔
1031

1032
        vol_single = df.vol_oggm_m3.loc[gdir.rgi_id]
6✔
1033
        if ref_volume_year is None:
6✔
1034
            year_single = gdir.rgi_date + 1
5✔
1035
        else:
1036
            year_single = ref_volume_year
3✔
1037
        if 'ref_volume_m3' in gdir.observations:
6✔
1038
            current_vol = gdir.observations['ref_volume_m3']
6✔
1039
            current_vol['value'] = vol_single
6✔
1040
        else:
1041
            current_vol = {'value': vol_single}
5✔
1042
        current_vol['year'] = year_single
6✔
1043
        gdir.observations['ref_volume_m3'] = current_vol
6✔
1044

1045
    return df
6✔
1046

1047

1048
@global_task(log)
10✔
1049
def calibrate_inversion_from_consensus(gdirs, settings_filesuffix='',
10✔
1050
                                       observations_filesuffix='',
1051
                                       overwrite_observations=False,
1052
                                       input_filesuffix=None,
1053
                                       output_filesuffix=None,
1054
                                       ignore_missing=True,
1055
                                       fs=0, a_bounds=(0.1, 10),
1056
                                       apply_fs_on_mismatch=False,
1057
                                       error_on_mismatch=True,
1058
                                       filter_inversion_output=True,
1059
                                       ref_volume_m3=None,
1060
                                       ref_volume_year=None,
1061
                                       add_to_log_file=True):
1062
    """Fit the total volume of the glaciers to the 2019 consensus estimate.
1063

1064
    .. deprecated::
1065
        Use :py:func:`calibrate_inversion_from_ref_table` instead, which
1066
        supports more recent reference volume products. This function is kept
1067
        for backwards compatibility and keeps calibrating against the
1068
        Farinotti et al. (2019) consensus estimate.
1069

1070
    This method finds the "best Glen A" to match all glaciers in gdirs with
1071
    a valid inverted volume.
1072

1073
    Parameters
1074
    ----------
1075
    gdirs : list of :py:class:`oggm.GlacierDirectory` objects
1076
        the glacier directories to process
1077
    settings_filesuffix: str
1078
        You can use a different set of settings by providing a filesuffix. This
1079
        is useful for sensitivity experiments.
1080
    observations_filesuffix: str
1081
        You can provide a filesuffix for the reference volume to use. If you
1082
        provide ref_volume_m3, then this values will be stored in the
1083
        observations file, if ref_volume_m3 is not already present. If you want
1084
        to force to use the provided values and override the current ones, set
1085
        overwrite_observations to True.
1086
    overwrite_observations : bool
1087
        If you want to overwrite already existing observation values in the
1088
        provided observations file set this to True. If this is False the
1089
        volumes saved in the observation file are used as the reference.
1090
        Default is True.
1091
    input_filesuffix: str
1092
        The filesuffix of the input inversion flowlines. If None the
1093
        settings_filesuffix will be used.
1094
    output_filesuffix: str
1095
        The filesuffix used for saving resulting inversion files to the gdir. If
1096
        None the settings_filesuffix will be used.
1097
    ignore_missing : bool
1098
        set this to true to silence the error if some glaciers could not be
1099
        found in the consensus estimate.
1100
    fs : float
1101
        invert with sliding (default: no)
1102
    a_bounds: tuple
1103
        factor to apply to default A
1104
    apply_fs_on_mismatch: false
1105
        on mismatch, try to apply an arbitrary value of fs (fs = 5.7e-20 from
1106
        Oerlemans) and try to optimize A again.
1107
    error_on_mismatch: bool
1108
        sometimes the given bounds do not allow to find a zero mismatch:
1109
        this will normally raise an error, but you can switch this off,
1110
        use the closest value instead and move on.
1111
    filter_inversion_output : bool
1112
        whether or not to apply terminus thickness filtering on the inversion
1113
        output (needs the downstream lines to work).
1114
    ref_volume_m3 : float
1115
        Option to give an own total glacier volume to match to
1116
    ref_volume_year : int or None
1117
        The year when the reference volume is valid. If None the RGI date is
1118
        used.
1119
    add_to_log_file : bool
1120
        if the called entity tasks should write into log of gdir. Default True
1121

1122
    Returns
1123
    -------
1124
    a dataframe with the individual glacier volumes
1125
    """
1126

1127
    warnings.warn('`calibrate_inversion_from_consensus` is deprecated. Use '
2✔
1128
                  '`calibrate_inversion_from_ref_table` instead, which '
1129
                  'supports more recent reference volume products. To keep '
1130
                  'calibrating against the Farinotti et al. (2019) consensus '
1131
                  'estimate, pass that table explicitly via `ref_table`.',
1132
                  FutureWarning)
1133

1134
    # Preserve the historical behaviour: calibrate against the Farinotti
1135
    # et al. (2019) consensus (ITMIX) table.
1136
    return calibrate_inversion_from_ref_table(
2✔
1137
        gdirs,
1138
        settings_filesuffix=settings_filesuffix,
1139
        observations_filesuffix=observations_filesuffix,
1140
        overwrite_observations=overwrite_observations,
1141
        input_filesuffix=input_filesuffix,
1142
        output_filesuffix=output_filesuffix,
1143
        ref_table='consensus',
1144
        ignore_missing=ignore_missing,
1145
        fs=fs,
1146
        a_bounds=a_bounds,
1147
        apply_fs_on_mismatch=apply_fs_on_mismatch,
1148
        error_on_mismatch=error_on_mismatch,
1149
        filter_inversion_output=filter_inversion_output,
1150
        ref_volume_m3=ref_volume_m3,
1151
        ref_volume_year=ref_volume_year,
1152
        add_to_log_file=add_to_log_file,
1153
    )
1154

1155

1156
@entity_task(log, writes=['inversion_output'])
10✔
1157
def calibrate_inversion_from_volume(gdir,
10✔
1158
                                    settings_filesuffix='',
1159
                                    observations_filesuffix='',
1160
                                    overwrite_observations=True,
1161
                                    input_filesuffix=None,
1162
                                    output_filesuffix=None,
1163
                                    ref_volume_m3=None,
1164
                                    ref_volume_year=None,
1165
                                    fs=0, a_bounds=(0.1, 10),
1166
                                    apply_fs_on_mismatch=False,
1167
                                    error_on_mismatch=True,
1168
                                    filter_inversion_output=True):
1169
    """Fit the volume of a single glacier to a reference volume estimate.
1170

1171
    This is the entity task version of calibrate_inversion_from_ref_table.
1172
    It finds the "best Glen A" to match the reference volume for a single glacier.
1173

1174
    Parameters
1175
    ----------
1176
    gdir : :py:class:`oggm.GlacierDirectory`
1177
        the glacier directory to process
1178
    settings_filesuffix: str
1179
        You can use a different set of settings by providing a filesuffix. This
1180
        is useful for sensitivity experiments.
1181
    observations_filesuffix: str
1182
        You can provide a filesuffix for the reference volume to use. If you
1183
        provide ref_volume_m3, then this values will be stored in the
1184
        observations file, if ref_volume_m3 is not already present. If you want
1185
        to force to use the provided values and override the current ones, set
1186
        overwrite_observations to True.
1187
    overwrite_observations : bool
1188
        If you want to overwrite already existing observation values in the
1189
        provided observations file set this to True. If this is False the
1190
        volumes saved in the observation file are used as the reference.
1191
        Default is True.
1192
    input_filesuffix: str
1193
        The filesuffix of the input inversion flowlines. If None the
1194
        settings_filesuffix will be used.
1195
    output_filesuffix: str
1196
        The filesuffix used for saving resulting inversion files to the gdir. If
1197
        None the settings_filesuffix will be used.
1198
    ref_volume_m3 : float
1199
        the reference volume in m3 to match. If float, take it,
1200
        if pd.Series, select the glacier, if None, error.
1201
    ref_volume_year : int or None
1202
        The year when the reference volume is valid. If None the RGI date is
1203
        used.
1204
    fs : float
1205
        invert with sliding (default: no)
1206
    a_bounds: tuple
1207
        factor to apply to default A
1208
    apply_fs_on_mismatch: bool
1209
        on mismatch, try to apply an arbitrary value of fs (fs = 5.7e-20 from
1210
        Oerlemans) and try to optimize A again.
1211
    error_on_mismatch: bool
1212
        sometimes the given bounds do not allow to find a zero mismatch:
1213
        this will normally raise an error, but you can switch this off,
1214
        use the closest value instead and move on.
1215
    filter_inversion_output : bool
1216
        whether or not to apply terminus thickness filtering on the inversion
1217
        output (needs the downstream lines to work).
1218

1219
    Returns
1220
    -------
1221
    dict with the glacier volume and the calibrated parameters
1222
    """
1223

1224
    if input_filesuffix is None:
1✔
1225
        input_filesuffix = settings_filesuffix
1✔
1226

1227
    if output_filesuffix is None:
1✔
1228
        output_filesuffix = settings_filesuffix
1✔
1229

1230
    if isinstance(ref_volume_m3, pd.Series):
1✔
1231
        try:
1✔
1232
            ref_volume_m3 = ref_volume_m3.loc[gdir.rgi_id]
1✔
1233
        except KeyError:
×
1234
            raise InvalidParamsError(f'vol_ref_m3 series has no entry '
×
1235
                                     f'for {gdir.rgi_id}.')
1236

1237
    if not overwrite_observations:
1✔
1238
        ref_volume_m3_file = gdir.observations['ref_volume_m3']['value']
1✔
1239
        if ref_volume_m3 is None:
1✔
1240
            # if no reference volume is provided use the one from the obs-file
1241
            ref_volume_m3 = ref_volume_m3_file
1✔
1242
        elif np.isclose(ref_volume_m3_file, ref_volume_m3, rtol=1e-2):
1✔
1243
            # ok the provided ref volume is the same as stored in the obs-file
1244
            pass
1✔
1245
        else:
1246
            raise InvalidWorkflowError(
1✔
1247
                'You have provided an reference volume, but their is already '
1248
                'one stored in the current observations file (filesuffix = '
1249
                f'{observations_filesuffix})! If you want to overwrite set '
1250
                f'overwrite_observations = True.')
1251

1252
    if ref_volume_m3 is None:
1!
1253
        raise InvalidParamsError('vol_ref_m3 must be provided (float or Series).')
×
1254

1255
    # Optimize the diff to ref
1256
    def_a = gdir.settings['inversion_glen_a']
1✔
1257

1258
    if gdir.settings['use_kcalving_for_inversion']:
1✔
1259
        raise NotImplementedError('Calving not implemented yet')
1260

1261
    def compute_vol(x):
1✔
1262
        # Run inversion tasks for this glacier
1263
        tasks.prepare_for_inversion(gdir,
1✔
1264
                                    settings_filesuffix=settings_filesuffix,
1265
                                    # only use input_filesuffix for first task as
1266
                                    # subsequent task use the results of previous
1267
                                    # tasks
1268
                                    input_filesuffix=input_filesuffix,
1269
                                    output_filesuffix=output_filesuffix,
1270
                                    add_to_log_file=False)
1271
        tasks.mass_conservation_inversion(gdir,
1✔
1272
                                          settings_filesuffix=settings_filesuffix,
1273
                                          input_filesuffix=output_filesuffix,
1274
                                          output_filesuffix=output_filesuffix,
1275
                                          glen_a=x*def_a, fs=fs,
1276
                                          add_to_log_file=False)
1277
        if filter_inversion_output:
1!
1278
            tasks.filter_inversion_output(gdir,
1✔
1279
                                          settings_filesuffix=settings_filesuffix,
1280
                                          input_filesuffix=output_filesuffix,
1281
                                          output_filesuffix=output_filesuffix,
1282
                                          add_to_log_file=False)
1283
        vol = tasks.get_inversion_volume(gdir,
1✔
1284
                                         input_filesuffix=output_filesuffix,
1285
                                         add_to_log_file=False)
1286
        return vol
1✔
1287

1288
    def to_minimize(x):
1✔
1289
        log.info(f'Volume calibration for {gdir.rgi_id} with '
1✔
1290
                 f'A factor: {x} and fs: {fs}')
1291
        vol = compute_vol(x)
1✔
1292
        return ref_volume_m3 - vol
1✔
1293

1294
    try:
1✔
1295
        out_fac, r = optimization.brentq(to_minimize, *a_bounds,
1✔
1296
                                         rtol=1e-2,
1297
                                         full_output=True)
1298
        if r.converged:
1!
1299
            log.info(f'calibrate_inversion_from_volume for {gdir.rgi_id} '
1✔
1300
                     f'converged after {r.iterations} iterations and fs={fs}. The '
1301
                     f'resulting Glen A factor is {out_fac}.')
1302
        else:
1303
            raise ValueError('Unexpected error in optimization.brentq')
×
1304
    except ValueError:
1✔
1305
        # Ok can't find an A. Log for debug:
1306
        vol1 = compute_vol(a_bounds[0])
1✔
1307
        vol2 = compute_vol(a_bounds[1])
1✔
1308
        msg = (f'calibration from volume CAN\'T converge for {gdir.rgi_id} with fs={fs}.\n'
1✔
1309
               f'Bound values (m3):\nRef={ref_volume_m3:.0f} OGGM={vol1:.0f} for A factor {a_bounds[0]}\n'
1310
               f'Ref={ref_volume_m3:.0f} OGGM={vol2:.0f} for A factor {a_bounds[1]}')
1311
        if apply_fs_on_mismatch and fs == 0 and vol2 > ref_volume_m3:
1!
1312
            return calibrate_inversion_from_volume(
1✔
1313
                gdir,
1314
                settings_filesuffix=settings_filesuffix,
1315
                observations_filesuffix=observations_filesuffix,
1316
                overwrite_observations=overwrite_observations,
1317
                input_filesuffix=input_filesuffix,
1318
                output_filesuffix=output_filesuffix,
1319
                ref_volume_m3=ref_volume_m3,
1320
                ref_volume_year=ref_volume_year,
1321
                fs=5.7e-20, a_bounds=a_bounds,
1322
                apply_fs_on_mismatch=False, error_on_mismatch=error_on_mismatch,
1323
                filter_inversion_output=filter_inversion_output)
1324
        if error_on_mismatch:
×
1325
            raise ValueError(msg)
×
1326

1327
        out_fac = a_bounds[int(abs(ref_volume_m3 - vol1) >
×
1328
                               abs(ref_volume_m3 - vol2))]
1329
        log.info(msg)
×
1330
        log.info(f'We use A factor = {out_fac} and fs = {fs} and move on.')
×
1331

1332
    # Compute the final volume with the correct A
1333
    tasks.prepare_for_inversion(gdir,
1✔
1334
                                settings_filesuffix=settings_filesuffix,
1335
                                # only use input_filesuffix for first task as
1336
                                # subsequent task use the results of previous
1337
                                # tasks
1338
                                input_filesuffix=input_filesuffix,
1339
                                output_filesuffix=output_filesuffix,)
1340
    tasks.mass_conservation_inversion(gdir,
1✔
1341
                                      settings_filesuffix=settings_filesuffix,
1342
                                      input_filesuffix=output_filesuffix,
1343
                                      output_filesuffix=output_filesuffix,
1344
                                      glen_a=out_fac*def_a, fs=fs)
1345
    if filter_inversion_output:
1!
1346
        tasks.filter_inversion_output(gdir,
1✔
1347
                                      settings_filesuffix=settings_filesuffix,
1348
                                      input_filesuffix=output_filesuffix,
1349
                                      output_filesuffix=output_filesuffix,
1350
                                      )
1351

1352
    final_vol = tasks.get_inversion_volume(gdir,
1✔
1353
                                           input_filesuffix=output_filesuffix,
1354
                                           )
1355

1356
    # save in observation file
1357
    gdir.observations['ref_volume_m3'] = {
1✔
1358
        'value': final_vol,
1359
        'year': gdir.rgi_date + 1 if ref_volume_year is None else ref_volume_year,
1360
    }
1361

1362
    return {
1✔
1363
        'vol_oggm_m3': final_vol,
1364
        'glen_a': out_fac * def_a,
1365
        'fs': fs,
1366
        'a_factor': out_fac
1367
    }
1368

1369

1370
@global_task(log)
10✔
1371
def invert_from_params(gdirs,  settings_filesuffix='',
10✔
1372
                       input_filesuffix=None,
1373
                       output_filesuffix=None,
1374
                       params_df=None,
1375
                       fs=None, glen_a=None,
1376
                       filter_inversion_output=True,
1377
                       add_to_log_file=True):
1378
    """instead of optimising the parameters, get them from a file.
1379

1380
    Useful e.g. for pre computed parameters for RGI7.
1381

1382
    Parameters
1383
    ----------
1384
    gdirs : list of :py:class:`oggm.GlacierDirectory` objects
1385
        the glacier directories to process
1386
    settings_filesuffix: str
1387
        You can use a different set of settings by providing a filesuffix. This
1388
        is useful for sensitivity experiments.
1389
    input_filesuffix: str
1390
        The filesuffix of the input inversion flowlines. If None the
1391
        settings_filesuffix will be used.
1392
    output_filesuffix: str
1393
        The filesuffix used for saving resulting inversion files to the gdir. If
1394
        None the settings_filesuffix will be used.
1395
    params_df : str
1396
        the dataframe to use (currently regional)
1397
    glen_a : float
1398
        if params file is not provided, use this value
1399
        (defaults to cfg.params)
1400
    fs : float
1401
        if params file is not provided, use this value
1402
    filter_inversion_output : bool
1403
        whether or not to apply terminus thickness filtering on the inversion
1404
        output (needs the downstream lines to work).
1405

1406
    Returns
1407
    -------
1408
    a dataframe with the individual glacier volumes
1409
    """
1410

1411
    if input_filesuffix is None:
1!
1412
        input_filesuffix = settings_filesuffix
1✔
1413

1414
    if output_filesuffix is None:
1!
1415
        output_filesuffix = settings_filesuffix
1✔
1416

1417
    gdirs = utils.tolist(gdirs)
1✔
1418

1419
    df = pd.DataFrame({
1✔
1420
        'rgi_region': [gd.rgi_region for gd in gdirs]
1421
    }, index=[gd.rgi_id for gd in gdirs])
1422
    df.index.name = 'rgi_id'
1✔
1423

1424
    if params_df is not None:
1✔
1425
        rgi_regs = set(df.rgi_region)
1✔
1426
        if len(rgi_regs) > 1:
1!
1427
            raise InvalidParamsError('Glaciers from multiple RGI regions '
×
1428
                                     'are not supported.')
1429
        rgi_reg = int(rgi_regs.pop())
1✔
1430
        glen_a = params_df.loc[rgi_reg, 'inversion_glen_a']
1✔
1431
        fs = params_df.loc[rgi_reg, 'inversion_fs']
1✔
1432

1433
    log.workflow(f"Applying A factor = {glen_a/gdirs[0].settings['glen_a']} "
1✔
1434
                 f"and fs = {fs}")
1435

1436
    # Compute the final volume with the correct A
1437
    inversion_tasks(gdirs, settings_filesuffix=settings_filesuffix,
1✔
1438
                    input_filesuffix=input_filesuffix,
1439
                    output_filesuffix=output_filesuffix,
1440
                    glen_a=glen_a, fs=fs,
1441
                    filter_inversion_output=filter_inversion_output,
1442
                    add_to_log_file=add_to_log_file)
1443
    df['vol_oggm_m3'] = execute_entity_task(tasks.get_inversion_volume, gdirs,
1✔
1444
                                            input_filesuffix=output_filesuffix,
1445
                                            add_to_log_file=add_to_log_file)
1446
    return df
1✔
1447

1448

1449
@global_task(log)
10✔
1450
def merge_glacier_tasks(gdirs, settings_filesuffix='',
10✔
1451
                        main_rgi_id=None, return_all=False, buffer=None,
1452
                        **kwargs):
1453
    """Shortcut function: run all tasks to merge tributaries to a main glacier
1454

1455
    Parameters
1456
    ----------
1457
    gdirs : list of :py:class:`oggm.GlacierDirectory`
1458
        all glaciers, main and tributary. Preprocessed and initialised
1459
    main_rgi_id: str
1460
        RGI ID of the main glacier of interest. If None is provided merging
1461
        will start based upon the largest glacier
1462
    return_all : bool
1463
        if main_rgi_id is given and return_all = False: only the main glacier
1464
        is returned
1465
        if main_rgi_is given and return_all = True, the main glacier and every
1466
        remaining glacier from the initial gdirs list is returned, possible
1467
        merged as well.
1468
    buffer : float
1469
        buffer around a flowline to first better find an overlap with another
1470
        flowline. And second assure some distance between the lines at a
1471
        junction. Will default to `cfg.PARAMS['kbuffer']`.
1472
    kwargs: keyword argument for the recursive merging
1473

1474
    Returns
1475
    -------
1476
    merged_gdirs: list of all merged :py:class:`oggm.GlacierDirectory`
1477
    """
1478

1479
    if len(gdirs) > 100:
×
1480
        raise InvalidParamsError('this could take time! I should include an '
×
1481
                                 'optional parameter to ignore this.')
1482

1483
    # sort all glaciers descending by area
1484
    gdirs.sort(key=lambda x: x.rgi_area_m2, reverse=True)
×
1485

1486
    # if main glacier is asked, put it in first position
1487
    if main_rgi_id is not None:
×
1488
        gdir_main = [gd for gd in gdirs if gd.rgi_id == main_rgi_id][0]
×
1489
        gdirs.remove(gdir_main)
×
1490
        gdirs = [gdir_main] + gdirs
×
1491

1492
    merged_gdirs = []
×
1493
    while len(gdirs) > 1:
×
1494
        # main glacier is always the first: either given or the largest one
1495
        gdir_main = gdirs.pop(0)
×
1496
        gdir_merged, gdirs = _recursive_merging(gdirs, gdir_main, **kwargs)
×
1497
        merged_gdirs.append(gdir_merged)
×
1498

1499
    # now we have gdirs which contain all the necessary flowlines,
1500
    # time to clean them up
1501
    for gdir in merged_gdirs:
×
1502
        flowline.clean_merged_flowlines(
×
1503
            gdir, settings_filesuffix=settings_filesuffix, buffer=buffer)
1504

1505
    if main_rgi_id is not None and return_all is False:
×
1506
        return [gd for gd in merged_gdirs if main_rgi_id in gd.rgi_id][0]
×
1507

1508
    # add the remaining glacier to the final list
1509
    merged_gdirs = merged_gdirs + gdirs
×
1510

1511
    return merged_gdirs
×
1512

1513

1514
def _recursive_merging(gdirs, gdir_main, glcdf=None, dem_source=None,
10✔
1515
                       filename='climate_historical', input_filesuffix=''):
1516
    """ Recursive function to merge all tributary glaciers.
1517

1518
    This function should start with the largest glacier and then be called
1519
    upon all smaller glaciers.
1520

1521
    Parameters
1522
    ----------
1523
    gdirs : list of :py:class:`oggm.GlacierDirectory`
1524
        all glaciers, main and tributary. Preprocessed and initialised
1525
    gdir_main: :py:class:`oggm.GlacierDirectory`
1526
        the current main glacier where the others are merge to
1527
    glcdf: geopandas.GeoDataFrame
1528
        which contains the main glaciers, will be downloaded if None
1529
    filename: str
1530
        Baseline climate file
1531
    dem_source: str
1532
        the DEM source to use
1533
    input_filesuffix: str
1534
        Filesuffix to the climate file
1535

1536
    Returns
1537
    -------
1538
    merged_gdir: :py:class:`oggm.GlacierDirectory`
1539
        the mergeed current main glacier
1540
    gdirs : list of :py:class:`oggm.GlacierDirectory`
1541
        updated list of glaciers, removed the already merged ones
1542
    """
1543
    # find glaciers which intersect with the main
1544
    tributaries = centerlines.intersect_downstream_lines(gdir_main,
×
1545
                                                         candidates=gdirs)
1546
    if len(tributaries) == 0:
×
1547
        # if no tributaries: nothing to do
1548
        return gdir_main, gdirs
×
1549

1550
    # separate those glaciers which are not already found to be a tributary
1551
    gdirs = [gd for gd in gdirs if gd not in tributaries]
×
1552

1553
    gdirs_to_merge = []
×
1554

1555
    for trib in tributaries:
×
1556
        # for each tributary: check if we can merge additional glaciers to it
1557
        merged, gdirs = _recursive_merging(gdirs, trib, glcdf=glcdf,
×
1558
                                           filename=filename,
1559
                                           input_filesuffix=input_filesuffix,
1560
                                           dem_source=dem_source)
1561
        gdirs_to_merge.append(merged)
×
1562

1563
    # create merged glacier directory
1564
    gdir_merged = utils.initialize_merged_gdir(
×
1565
        gdir_main, tribs=gdirs_to_merge, glcdf=glcdf, filename=filename,
1566
        input_filesuffix=input_filesuffix, dem_source=dem_source)
1567

1568
    flowline.merge_to_one_glacier(gdir_merged, gdirs_to_merge,
×
1569
                                  filename=filename,
1570
                                  input_filesuffix=input_filesuffix)
1571

1572
    return gdir_merged, gdirs
×
1573

1574

1575
@global_task(log)
10✔
1576
def merge_gridded_data(gdirs, output_folder=None,
10✔
1577
                       output_filename='gridded_data_merged',
1578
                       output_grid=None,
1579
                       input_file='gridded_data',
1580
                       input_filesuffix='',
1581
                       included_variables='all',
1582
                       preserve_totals=True,
1583
                       smooth_radius=None,
1584
                       use_glacier_mask=True,
1585
                       add_topography=False,
1586
                       keep_dem_file=False,
1587
                       interp='nearest',
1588
                       use_multiprocessing=True,
1589
                       return_dataset=True,
1590
                       reset=False):
1591
    """ This function takes a list of glacier directories and combines their
1592
    gridded_data into a new NetCDF file and saves it into the output_folder. It
1593
    also could merge data from different source files if you provide a list
1594
    of input_file(s) (together with a list of input_filesuffix and a list of
1595
    included_variables).
1596

1597
    Attention: You always should check the first gdir from gdirs as this
1598
    defines the projection of the resulting dataset and the data which is
1599
    merged, if included_variables is set to 'all'.
1600

1601
    Parameters
1602
    ----------
1603
    gdirs : list of :py:class:`oggm.GlacierDirectory` objects
1604
        The glacier directories which should be combined. If an additonal
1605
        dimension than x or y is given (e.g. time) we assume it has the same
1606
        length for all gdirs (we currently do not check). The first gdir in the
1607
        list serves as the template for the merged gridded_data (it defines the
1608
        used projection, if you want to merge all variables they are taken from
1609
        the input data of the first gdir).
1610
    output_folder : str
1611
        Folder where the intermediate files and the final combined gridded data
1612
        should be stored. Default is cfg.PATHS['working_dir']
1613
    output_filename : str
1614
        The name for the resulting file. Default is 'gridded_data_merged'.
1615
    output_grid : salem.gis.Grid
1616
        You can provide a custom grid on which the gridded data should be
1617
        merged on. If None, a combined grid of all gdirs will be constructed.
1618
        Default is None.
1619
    input_file : str or list
1620
        The file(s) which should be merged. If a list is provided the data of
1621
        all files is merged into the same dataset. Default is 'gridded_data'.
1622
    input_filesuffix : str or list
1623
        Potential filesuffix for the input file(s). If input_file is a list,
1624
        input_filesuffix should also be a list of the same length.
1625
        Default is ''.
1626
    included_variables : str or list or list of lists
1627
        The variable(s) which should be merged from the input_file(s). For one
1628
        variable it can be provided as str, otherwise as a list. If set to
1629
        'all' we merge everything. If input_file is a list, include_variables
1630
        should be a list of lists with the same length, where the lists define
1631
        the variables for the individual input_files. Furthermore, if you only
1632
        want to merge a subset of the variables you can define the variable as
1633
        a tuple with the first element being the variable name and the second
1634
        element being the selected coordinates as a dictionary (e.g.
1635
        ('variable', {'time': [0, 1, 2]})). Default is 'all'.
1636
    preserve_totals : bool
1637
        If True we preserve the total value of all float-variables of the
1638
        original file. The total value is defined as the sum of all grid cell
1639
        values times the area of the grid cell (e.g. preserving ice volume).
1640
        Default is True.
1641
    smooth_radius : int
1642
        pixel size of the gaussian smoothing, only used if preserve_totals is
1643
        True. Default is to use cfg.PARAMS['smooth_window'] (i.e. a size in
1644
        meters). Set to zero to suppress smoothing.
1645
    use_glacier_mask : bool
1646
        If True only the data cropped by the glacier mask is included in the
1647
        merged file. You must make sure that the variable 'glacier_mask' exists
1648
        in the input_file(s) (which is the oggm default). Default is True.
1649
    add_topography : bool or str
1650
        If True we try to add the default DEM source of the first glacier
1651
        directory of gdirs. Alternatively you could define a DEM source
1652
        directly as string. Default is False.
1653
    keep_dem_file : bool
1654
        If we add a topography to the merged gridded_data we save the DEM as
1655
        a tiff in the output_folder as an intermediate step. If keep_dem_file
1656
        is True we will keep this file, otherwise we delete it at the end.
1657
        Default is False.
1658
    interp : str
1659
        The interpolation method used by salem.Grid.map_gridded_data. Currently
1660
        available 'nearest' (default), 'linear', or 'spline'.
1661
    use_multiprocessing : bool
1662
        If True the merging is done in parallel using multiprocessing. This
1663
        could require a lot of memory. Default is True.
1664
    return_dataset : bool
1665
        If True the merged dataset is returned. Default is True.
1666
    reset : bool
1667
        If the file defined in output_filename already exists and reset is
1668
        False an error is raised. If reset is True and the file exists it is
1669
        deleted before merging. Default is False.
1670
    """
1671

1672
    # check if output_folder exists, otherwise creates it
1673
    if output_folder is None:
3✔
1674
        output_folder = cfg.PATHS['working_dir']
2✔
1675
    utils.mkdir(output_folder)
3✔
1676

1677
    # for some data we want to set zero values outside of outline to nan
1678
    # (e.g. for visualization purposes)
1679
    vars_setting_zero_to_nan = ['distributed_thickness', 'simulated_thickness',
3✔
1680
                                'consensus_ice_thickness',
1681
                                'millan_ice_thickness']
1682

1683
    # check if file already exists
1684
    fpath = os.path.join(output_folder, f'{output_filename}.nc')
3✔
1685
    if os.path.exists(fpath):
3✔
1686
        if reset:
2!
1687
            os.remove(fpath)
2✔
1688
        else:
1689
            raise InvalidWorkflowError(f'The file {output_filename}.nc already'
×
1690
                                       f' exists in the output folder. If you '
1691
                                       f'want to replace it set reset=True!')
1692

1693
    if not isinstance(input_file, list):
3✔
1694
        input_file = [input_file]
3✔
1695
    if not isinstance(input_filesuffix, list):
3✔
1696
        input_filesuffix = [input_filesuffix]
3✔
1697
    if not isinstance(included_variables, list):
3✔
1698
        # special case if only one variable should be merged
1699
        included_variables = [included_variables]
2✔
1700
    if len(input_file) == 1:
3✔
1701
        # in the case of one input file we still convert included_variables
1702
        # into a list of lists
1703
        included_variables = [included_variables]
3✔
1704

1705
    if output_grid is None:
3✔
1706
        # create a combined salem.Grid object, which serves as canvas/boundaries of
1707
        # the combined glacier region
1708
        output_grid = utils.combine_grids(gdirs)
3✔
1709

1710
    if add_topography:
3✔
1711
        # ok, lets get a DEM and add it to the final file
1712
        if isinstance(add_topography, str):
1!
1713
            dem_source = add_topography
×
1714
            dem_gdir = None
×
1715
        else:
1716
            dem_source = None
1✔
1717
            dem_gdir = gdirs[0]
1✔
1718
        gis.get_dem_for_grid(output_grid, output_folder,
1✔
1719
                             source=dem_source, gdir=dem_gdir)
1720
        # unwrapped is needed to execute process_dem without the entity_task
1721
        # overhead (this would need a valid gdir)
1722
        gis.process_dem.unwrapped(gdir=None, grid=output_grid,
1✔
1723
                                  fpath=output_folder,
1724
                                  output_filename=output_filename)
1725
        if not keep_dem_file:
1!
1726
            fpath = os.path.join(output_folder, 'dem.tif')
1✔
1727
            if os.path.exists(fpath):
1!
1728
                os.remove(fpath)
1✔
1729

1730
            # also delete diagnostics
1731
            fpath = os.path.join(output_folder, 'dem_diagnostics.json')
1✔
1732
            if os.path.exists(fpath):
1!
1733
                os.remove(fpath)
1✔
1734

1735
    with gis.GriddedNcdfFile(grid=output_grid, fpath=output_folder,
3✔
1736
                             basename=output_filename) as nc:
1737

1738
        # adding the data of one file after another to the merged dataset
1739
        for in_file, in_filesuffix, included_var in zip(input_file,
3✔
1740
                                                        input_filesuffix,
1741
                                                        included_variables):
1742

1743
            # if want to save all variables, take them from the first gdir
1744
            if 'all' in included_var:
3!
1745
                with xr.open_dataset(
×
1746
                        gdirs[0].get_filepath(in_file,
1747
                                              filesuffix=in_filesuffix)) as ds:
1748
                    included_var = list(ds.data_vars)
×
1749

1750
            # add one variable after another
1751
            for var in included_var:
3✔
1752
                # check if we only want to merge a subset of the variable
1753
                if isinstance(var, tuple):
3✔
1754
                    var, slice_of_var = var
1✔
1755
                else:
1756
                    slice_of_var = None
3✔
1757

1758
                # do not merge topo variables, for this we have add_topography
1759
                if var in ['topo', 'topo_smoothed', 'topo_valid_mask']:
3!
1760
                    continue
×
1761

1762
                # check dimensions, if other than y or x it is added to file
1763
                with xr.open_dataset(
3✔
1764
                        gdirs[0].get_filepath(in_file,
1765
                                              filesuffix=in_filesuffix)) as ds:
1766
                    ds_templ = ds
3✔
1767
                dims = ds_templ[var].dims
3✔
1768

1769
                dim_lengths = []
3✔
1770
                for dim in dims:
3✔
1771
                    if dim == 'y':
3✔
1772
                        dim_lengths.append(output_grid.ny)
3✔
1773
                    elif dim == 'x':
3✔
1774
                        dim_lengths.append(output_grid.nx)
3✔
1775
                    else:
1776
                        if slice_of_var is not None:
1!
1777
                            # only keep selected part of the variable
1778
                            if dim in slice_of_var:
1!
1779
                                dim_var = ds_templ[var][dim].sel(
1✔
1780
                                    {dim: slice_of_var[dim]})
1781
                            else:
1782
                                dim_var = ds_templ[var][dim]
×
1783
                        else:
1784
                            dim_var = ds_templ[var][dim]
×
1785
                        if dim not in nc.dimensions:
1!
1786
                            nc.createDimension(dim, len(dim_var))
1✔
1787
                            v = nc.createVariable(dim, 'f4', (dim,), zlib=True)
1✔
1788
                            # add attributes
1789
                            for attr in dim_var.attrs:
1!
1790
                                setattr(v, attr, dim_var.attrs[attr])
×
1791
                            if slice_of_var is not None:
1!
1792
                                if dim in slice_of_var:
1!
1793
                                    v[:] = slice_of_var[dim]
1✔
1794
                                else:
1795
                                    v[:] = dim_var.values
×
1796
                            else:
1797
                                v[:] = dim_var.values
×
1798
                            # also add potential coords (e.g. calender_year)
1799
                            for coord in dim_var.coords:
1✔
1800
                                if coord != dim:
1✔
1801
                                    if slice_of_var is not None:
1!
1802
                                        if dim in slice_of_var:
1!
1803
                                            coord_val = ds_templ[coord].sel(
1✔
1804
                                                {dim: slice_of_var[dim]}).values
1805
                                        else:
1806
                                            coord_val = ds_templ[coord].values
×
1807
                                    else:
1808
                                        coord_val = ds_templ[coord].values
×
1809
                                    tmp_coord = nc.createVariable(
1✔
1810
                                        coord, 'f4', (dim,))
1811
                                    tmp_coord[:] = coord_val
1✔
1812
                                    for attr in ds_templ[coord].attrs:
1!
1813
                                        setattr(tmp_coord, attr,
×
1814
                                                ds_templ[coord].attrs[attr])
1815
                        dim_lengths.append(len(dim_var))
1✔
1816

1817
                # before merging add variable attributes to final file
1818
                v = nc.createVariable(var, 'f4', dims, zlib=True)
3✔
1819
                for attr in ds_templ[var].attrs:
3✔
1820
                    setattr(v, attr, ds_templ[var].attrs[attr])
3✔
1821

1822
                kwargs_reproject = dict(
3✔
1823
                    variable=var,
1824
                    target_grid=output_grid,
1825
                    filename=in_file,
1826
                    filesuffix=in_filesuffix,
1827
                    use_glacier_mask=use_glacier_mask,
1828
                    interp=interp,
1829
                    preserve_totals=preserve_totals,
1830
                    smooth_radius=smooth_radius,
1831
                    slice_of_variable=slice_of_var,
1832
                )
1833

1834
                if use_multiprocessing:
3✔
1835
                    r_data = execute_entity_task(
3✔
1836
                        gis.reproject_gridded_data_variable_to_grid,
1837
                        gdirs,
1838
                        **kwargs_reproject
1839
                    )
1840

1841
                    # if we continue_on_error and their was a file or a variable
1842
                    # missing some entries could be None, here we filter them
1843
                    r_data = list(filter(lambda e: e is not None, r_data))
3✔
1844

1845
                    r_data = np.sum(r_data, axis=0)
3✔
1846
                    if var in vars_setting_zero_to_nan:
3✔
1847
                        r_data = np.where(r_data == 0, np.nan, r_data)
3✔
1848

1849
                    v[:] = r_data
3✔
1850
                else:
1851
                    # if we do not use multiprocessing we have to loop over the
1852
                    # gdirs and add the data one after another
1853
                    r_data = np.zeros(dim_lengths)
1✔
1854
                    for gdir in gdirs:
1✔
1855
                        tmp_data = gis.reproject_gridded_data_variable_to_grid(
1✔
1856
                            gdir, **kwargs_reproject)
1857
                        if tmp_data is not None:
1!
1858
                            r_data += tmp_data
1✔
1859

1860
                    if var in vars_setting_zero_to_nan:
1✔
1861
                        r_data = np.where(r_data == 0, np.nan, r_data)
1✔
1862

1863
                    v[:] = r_data
1✔
1864

1865
        # and some metadata to the merged dataset
1866
        nc.nr_of_merged_glaciers = len(gdirs)
3✔
1867
        nc.rgi_ids = [gdir.rgi_id for gdir in gdirs]
3✔
1868

1869
    # finally we set potential additional time coordinates correctly again
1870
    fp = os.path.join(output_folder, output_filename + '.nc')
3✔
1871
    ds_was_adapted = False
3✔
1872

1873
    with xr.open_dataset(fp) as ds:
3✔
1874
        for time_var in ['calendar_year', 'calendar_month',
3✔
1875
                         'hydro_year', 'hydro_month']:
1876
            if time_var in ds.data_vars:
3✔
1877
                ds = ds.set_coords(time_var)
1✔
1878
                ds_was_adapted = True
1✔
1879
        ds_adapted = ds.load()
3✔
1880

1881
    if ds_was_adapted:
3✔
1882
        ds_adapted.to_netcdf(fp)
1✔
1883

1884
    if return_dataset:
3✔
1885
        return ds_adapted
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc