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

OGGM / oggm / 5975837719

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

push

github

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

* Update index.rst

* Update index.rst

11441 of 13374 relevant lines covered (85.55%)

3.76 hits per line

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

79.89
/oggm/utils/_funcs.py
1
"""Some useful functions that did not fit into the other modules.
2
"""
3

4
# Builtins
5
import os
9✔
6
import sys
9✔
7
import math
9✔
8
import logging
9✔
9
import warnings
9✔
10
import shutil
9✔
11
from packaging.version import Version
9✔
12

13
# External libs
14
import pandas as pd
9✔
15
import numpy as np
9✔
16
from scipy.ndimage import convolve1d
9✔
17
try:
9✔
18
    from scipy.signal.windows import gaussian
9✔
19
except AttributeError:
×
20
    # Old scipy
21
    from scipy.signal import gaussian
×
22
from scipy.interpolate import interp1d
9✔
23
import shapely.geometry as shpg
9✔
24
from shapely.ops import linemerge
9✔
25
from shapely.validation import make_valid
9✔
26

27
# Optional libs
28
try:
9✔
29
    import geopandas as gpd
9✔
30
except ImportError:
31
    pass
32

33
# Locals
34
import oggm.cfg as cfg
9✔
35
from oggm.cfg import SEC_IN_YEAR, SEC_IN_MONTH
9✔
36
from oggm.utils._downloads import get_demo_file, file_downloader
9✔
37
from oggm.exceptions import InvalidParamsError, InvalidGeometryError
9✔
38

39
# Module logger
40
log = logging.getLogger('.'.join(__name__.split('.')[:-1]))
9✔
41

42
_RGI_METADATA = dict()
9✔
43

44
# Shape factors
45
# TODO: how to handle zeta > 10? at the moment extrapolation
46
# Table 1 from Adhikari (2012) and corresponding interpolation functions
47
_ADHIKARI_TABLE_ZETAS = np.array([0.5, 1, 2, 3, 4, 5, 10])
9✔
48
_ADHIKARI_TABLE_RECTANGULAR = np.array([0.313, 0.558, 0.790, 0.884,
9✔
49
                                        0.929, 0.954, 0.990])
50
_ADHIKARI_TABLE_PARABOLIC = np.array([0.251, 0.448, 0.653, 0.748,
9✔
51
                                      0.803, 0.839, 0.917])
52
ADHIKARI_FACTORS_RECTANGULAR = interp1d(_ADHIKARI_TABLE_ZETAS,
9✔
53
                                        _ADHIKARI_TABLE_RECTANGULAR,
54
                                        fill_value='extrapolate')
55
ADHIKARI_FACTORS_PARABOLIC = interp1d(_ADHIKARI_TABLE_ZETAS,
9✔
56
                                      _ADHIKARI_TABLE_PARABOLIC,
57
                                      fill_value='extrapolate')
58

59

60
def parse_rgi_meta(version=None):
9✔
61
    """Read the meta information (region and sub-region names)"""
62

63
    global _RGI_METADATA
64

65
    if version is None:
7✔
66
        version = cfg.PARAMS['rgi_version']
×
67

68
    if version in _RGI_METADATA:
7✔
69
        return _RGI_METADATA[version]
7✔
70

71
    # Parse RGI metadata
72
    if version == '7':
7✔
73
        rgi7url = 'https://cluster.klima.uni-bremen.de/~fmaussion/misc/rgi7_data/00_rgi70_regions/'
2✔
74
        reg_names = gpd.read_file(file_downloader(rgi7url + '00_rgi70_O1Regions/00_rgi70_O1Regions.dbf'))
2✔
75
        reg_names.index = reg_names['o1region'].astype(int)
2✔
76
        reg_names = reg_names['full_name']
2✔
77
        subreg_names = gpd.read_file(file_downloader(rgi7url + '00_rgi70_O2Regions/00_rgi70_O2Regions.dbf'))
2✔
78
        subreg_names.index = subreg_names['o2region']
2✔
79
        subreg_names = subreg_names['full_name']
2✔
80

81
    elif version in ['4', '5']:
7✔
82
        reg_names = pd.read_csv(get_demo_file('rgi_regions.csv'), index_col=0)
5✔
83
        # The files where different back then
84
        subreg_names = pd.read_csv(get_demo_file('rgi_subregions_V5.csv'),
5✔
85
                                   index_col=0)
86
    else:
87
        reg_names = pd.read_csv(get_demo_file('rgi_regions.csv'), index_col=0)
7✔
88
        f = os.path.join(get_demo_file('rgi_subregions_'
7✔
89
                                       'V{}.csv'.format(version)))
90
        subreg_names = pd.read_csv(f)
7✔
91
        subreg_names.index = ['{:02d}-{:02d}'.format(s1, s2) for s1, s2 in
7✔
92
                              zip(subreg_names['O1'], subreg_names['O2'])]
93
        subreg_names = subreg_names[['Full_name']]
7✔
94

95
    # For idealized
96
    reg_names.loc[0] = ['None']
7✔
97
    subreg_names.loc['00-00'] = ['None']
7✔
98
    _RGI_METADATA[version] = (reg_names, subreg_names)
7✔
99
    return _RGI_METADATA[version]
7✔
100

101

102
def query_yes_no(question, default="yes"):  # pragma: no cover
103
    """Ask a yes/no question via raw_input() and return their answer.
104

105
    "question" is a string that is presented to the user.
106
    "default" is the presumed answer if the user just hits <Enter>.
107
        It must be "yes" (the default), "no" or None (meaning
108
        an answer is required of the user).
109

110
    The "answer" return value is True for "yes" or False for "no".
111

112
    Credits: http://code.activestate.com/recipes/577058/
113
    """
114
    valid = {"yes": True, "y": True, "ye": True,
115
             "no": False, "n": False}
116
    if default is None:
117
        prompt = " [y/n] "
118
    elif default == "yes":
119
        prompt = " [Y/n] "
120
    elif default == "no":
121
        prompt = " [y/N] "
122
    else:
123
        raise ValueError("invalid default answer: '%s'" % default)
124

125
    while True:
126
        sys.stdout.write(question + prompt)
127
        choice = input().lower()
128
        if default is not None and choice == '':
129
            return valid[default]
130
        elif choice in valid:
131
            return valid[choice]
132
        else:
133
            sys.stdout.write("Please respond with 'yes' or 'no' "
134
                             "(or 'y' or 'n').\n")
135

136

137
def tolist(arg, length=None):
9✔
138
    """Makes sure that arg is a list."""
139

140
    if isinstance(arg, str):
9✔
141
        arg = [arg]
1✔
142

143
    try:
9✔
144
        # Shapely stuff
145
        arg = arg.geoms
9✔
146
    except AttributeError:
9✔
147
        pass
9✔
148

149
    try:
9✔
150
        (e for e in arg)
9✔
151
    except TypeError:
9✔
152
        arg = [arg]
9✔
153

154
    arg = list(arg)
9✔
155

156
    if length is not None:
9✔
157

158
        if len(arg) == 1:
9✔
159
            arg *= length
9✔
160
        elif len(arg) == length:
8✔
161
            pass
8✔
162
        else:
163
            raise ValueError('Cannot broadcast len {} '.format(len(arg)) +
×
164
                             'to desired length: {}.'.format(length))
165

166
    return arg
9✔
167

168

169
def haversine(lon1, lat1, lon2, lat2):
9✔
170
    """Great circle distance between two (or more) points on Earth
171

172
    Parameters
173
    ----------
174
    lon1 : float
175
       scalar or array of point(s) longitude
176
    lat1 : float
177
       scalar or array of point(s) longitude
178
    lon2 : float
179
       scalar or array of point(s) longitude
180
    lat2 : float
181
       scalar or array of point(s) longitude
182

183
    Returns
184
    -------
185
    the distances
186

187
    Examples:
188
    ---------
189
    >>> haversine(34, 42, 35, 42)
190
    82633.46475287154
191
    >>> haversine(34, 42, [35, 36], [42, 42])
192
    array([ 82633.46475287, 165264.11172113])
193
    """
194

195
    # convert decimal degrees to radians
196
    lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2])
7✔
197

198
    # haversine formula
199
    dlon = lon2 - lon1
7✔
200
    dlat = lat2 - lat1
7✔
201
    a = np.sin(dlat / 2)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2)**2
7✔
202
    c = 2 * np.arcsin(np.sqrt(a))
7✔
203
    return c * 6371000  # Radius of earth in meters
7✔
204

205

206
def interp_nans(array, default=None):
9✔
207
    """Interpolate NaNs using np.interp.
208

209
    np.interp is reasonable in that it does not extrapolate, it replaces
210
    NaNs at the bounds with the closest valid value.
211
    """
212

213
    _tmp = array.copy()
7✔
214
    nans, x = np.isnan(array), lambda z: z.nonzero()[0]
7✔
215
    if np.all(nans):
7✔
216
        # No valid values
217
        if default is None:
3✔
218
            raise ValueError('No points available to interpolate: '
×
219
                             'please set default.')
220
        _tmp[:] = default
3✔
221
    else:
222
        _tmp[nans] = np.interp(x(nans), x(~nans), array[~nans])
7✔
223

224
    return _tmp
7✔
225

226

227
def smooth1d(array, window_size=None, kernel='gaussian'):
9✔
228
    """Apply a centered window smoothing to a 1D array.
229

230
    Parameters
231
    ----------
232
    array : ndarray
233
        the array to apply the smoothing to
234
    window_size : int
235
        the size of the smoothing window
236
    kernel : str
237
        the type of smoothing (`gaussian`, `mean`)
238

239
    Returns
240
    -------
241
    the smoothed array (same dim as input)
242
    """
243

244
    # some defaults
245
    if window_size is None:
7✔
246
        if len(array) >= 9:
7✔
247
            window_size = 9
7✔
248
        elif len(array) >= 7:
7✔
249
            window_size = 7
7✔
250
        elif len(array) >= 5:
2✔
251
            window_size = 5
2✔
252
        elif len(array) >= 3:
×
253
            window_size = 3
×
254

255
    if window_size % 2 == 0:
7✔
256
        raise ValueError('Window should be an odd number.')
×
257

258
    if isinstance(kernel, str):
7✔
259
        if kernel == 'gaussian':
7✔
260
            kernel = gaussian(window_size, 1)
7✔
261
        elif kernel == 'mean':
1✔
262
            kernel = np.ones(window_size)
1✔
263
        else:
264
            raise NotImplementedError('Kernel: ' + kernel)
265
    kernel = kernel / np.asarray(kernel).sum()
7✔
266
    return convolve1d(array, kernel, mode='mirror')
7✔
267

268

269
def line_interpol(line, dx):
9✔
270
    """Interpolates a shapely LineString to a regularly spaced one.
271

272
    Shapely's interpolate function does not guaranty equally
273
    spaced points in space. This is what this function is for.
274

275
    We construct new points on the line but at constant distance from the
276
    preceding one.
277

278
    Parameters
279
    ----------
280
    line: a shapely.geometry.LineString instance
281
    dx: the spacing
282

283
    Returns
284
    -------
285
    a list of equally distanced points
286
    """
287

288
    # First point is easy
289
    points = [line.interpolate(dx / 2.)]
7✔
290

291
    # Continue as long as line is not finished
292
    while True:
7✔
293
        pref = points[-1]
7✔
294
        pbs = pref.buffer(dx).boundary.intersection(line)
7✔
295
        if pbs.geom_type == 'Point':
7✔
296
            pbs = [pbs]
7✔
297
        elif pbs.geom_type == 'LineString':
7✔
298
            # This is rare
299
            pbs = [shpg.Point(c) for c in pbs.coords]
×
300
            assert len(pbs) == 2
×
301
        elif pbs.geom_type == 'GeometryCollection':
7✔
302
            # This is rare
303
            opbs = []
×
304
            for p in pbs.geoms:
×
305
                if p.geom_type == 'Point':
×
306
                    opbs.append(p)
×
307
                elif p.geom_type == 'LineString':
×
308
                    opbs.extend([shpg.Point(c) for c in p.coords])
×
309
            pbs = opbs
×
310
        else:
311
            if pbs.geom_type != 'MultiPoint':
7✔
312
                raise RuntimeError('line_interpol: we expect a MultiPoint '
×
313
                                   'but got a {}.'.format(pbs.geom_type))
314

315
        try:
7✔
316
            # Shapely v2 compat
317
            pbs = pbs.geoms
7✔
318
        except AttributeError:
7✔
319
            pass
7✔
320

321
        # Out of the point(s) that we get, take the one farthest from the top
322
        refdis = line.project(pref)
7✔
323
        tdis = np.array([line.project(pb) for pb in pbs])
7✔
324
        p = np.where(tdis > refdis)[0]
7✔
325
        if len(p) == 0:
7✔
326
            break
7✔
327
        points.append(pbs[int(p[0])])
7✔
328

329
    return points
7✔
330

331

332
def md(ref, data, axis=None):
9✔
333
    """Mean Deviation."""
334
    return np.mean(np.asarray(data) - ref, axis=axis)
2✔
335

336

337
def mad(ref, data, axis=None):
9✔
338
    """Mean Absolute Deviation."""
339
    return np.mean(np.abs(np.asarray(data) - ref), axis=axis)
×
340

341

342
def rmsd(ref, data, axis=None):
9✔
343
    """Root Mean Square Deviation."""
344
    return np.sqrt(np.mean((np.asarray(ref) - data)**2, axis=axis))
6✔
345

346

347
def rmsd_bc(ref, data):
9✔
348
    """Root Mean Squared Deviation of bias-corrected time series.
349

350
    I.e: rmsd(ref - mean(ref), data - mean(data)).
351
    """
352
    return rmsd(ref - np.mean(ref), data - np.mean(data))
×
353

354

355
def rel_err(ref, data):
9✔
356
    """Relative error. Ref should be non-zero"""
357
    return (np.asarray(data) - ref) / ref
×
358

359

360
def corrcoef(ref, data):
9✔
361
    """Peason correlation coefficient."""
362
    return np.corrcoef(ref, data)[0, 1]
1✔
363

364

365
def clip_scalar(value, vmin, vmax):
9✔
366
    """A faster numpy.clip ON SCALARS ONLY.
367

368
    See https://github.com/numpy/numpy/issues/14281
369
    """
370
    return vmin if value < vmin else vmax if value > vmax else value
9✔
371

372

373
def weighted_average_1d(data, weights):
9✔
374
    """A faster weighted average without dimension checks.
375

376
    We use it because it turned out to be quite a bottleneck in calibration
377
    """
378
    scl = np.sum(weights)
8✔
379
    if scl == 0:
8✔
380
        raise ZeroDivisionError("Weights sum to zero, can't be normalized")
1✔
381
    return np.multiply(data, weights).sum() / scl
8✔
382

383

384
if Version(np.__version__) < Version('1.17'):
9✔
385
    clip_array = np.clip
×
386
else:
387
    # TODO: reassess this when https://github.com/numpy/numpy/issues/14281
388
    # is solved
389
    clip_array = np.core.umath.clip
9✔
390

391

392
# A faster numpy.clip when only one value is clipped (here: min).
393
clip_min = np.core.umath.maximum
9✔
394

395
# A faster numpy.clip when only one value is clipped (here: max).
396
clip_max = np.core.umath.minimum
9✔
397

398

399
def nicenumber(number, binsize, lower=False):
9✔
400
    """Returns the next higher or lower "nice number", given by binsize.
401

402
    Examples:
403
    ---------
404
    >>> nicenumber(12, 10)
405
    20
406
    >>> nicenumber(19, 50)
407
    50
408
    >>> nicenumber(51, 50)
409
    100
410
    >>> nicenumber(51, 50, lower=True)
411
    50
412
    """
413

414
    e, _ = divmod(number, binsize)
7✔
415
    if lower:
7✔
416
        return e * binsize
7✔
417
    else:
418
        return (e + 1) * binsize
7✔
419

420

421
def signchange(ts):
9✔
422
    """Detect sign changes in a time series.
423

424
    http://stackoverflow.com/questions/2652368/how-to-detect-a-sign-change-
425
    for-elements-in-a-numpy-array
426

427
    Returns
428
    -------
429
    An array with 0s everywhere and 1's when the sign changes
430
    """
431
    asign = np.sign(ts)
1✔
432
    sz = asign == 0
1✔
433
    while sz.any():
1✔
434
        asign[sz] = np.roll(asign, 1)[sz]
×
435
        sz = asign == 0
×
436
    out = ((np.roll(asign, 1) - asign) != 0).astype(int)
1✔
437
    if asign.iloc[0] == asign.iloc[1]:
1✔
438
        out.iloc[0] = 0
1✔
439
    return out
1✔
440

441

442
def polygon_intersections(gdf):
9✔
443
    """Computes the intersections between all polygons in a GeoDataFrame.
444

445
    Parameters
446
    ----------
447
    gdf : Geopandas.GeoDataFrame
448

449
    Returns
450
    -------
451
    a Geodataframe containing the intersections
452
    """
453

454
    out_cols = ['id_1', 'id_2', 'geometry']
7✔
455
    out = gpd.GeoDataFrame(columns=out_cols)
7✔
456

457
    gdf = gdf.reset_index()
7✔
458

459
    for i, major in gdf.iterrows():
7✔
460

461
        # Exterior only
462
        major_poly = major.geometry.exterior
7✔
463

464
        # Remove the major from the list
465
        agdf = gdf.loc[gdf.index != i]
7✔
466

467
        # Keep catchments which intersect
468
        gdfs = agdf.loc[agdf.intersects(major_poly)]
7✔
469

470
        for j, neighbor in gdfs.iterrows():
7✔
471

472
            # No need to check if we already found the intersect
473
            if j in out.id_1 or j in out.id_2:
7✔
474
                continue
7✔
475

476
            # Exterior only
477
            neighbor_poly = neighbor.geometry.exterior
7✔
478

479
            # Ok, the actual intersection
480
            mult_intersect = major_poly.intersection(neighbor_poly)
7✔
481

482
            # All what follows is to catch all possibilities
483
            # Should not happen in our simple geometries but ya never know
484
            if isinstance(mult_intersect, shpg.Point):
7✔
485
                continue
×
486
            if isinstance(mult_intersect, shpg.linestring.LineString):
7✔
487
                mult_intersect = [mult_intersect]
×
488

489
            try:
7✔
490
                # Shapely v2 compat
491
                mult_intersect = mult_intersect.geoms
7✔
492
            except AttributeError:
×
493
                pass
×
494

495
            if len(mult_intersect) == 0:
7✔
496
                continue
×
497
            mult_intersect = [m for m in mult_intersect if
7✔
498
                              not isinstance(m, shpg.Point)]
499
            if len(mult_intersect) == 0:
7✔
500
                continue
×
501
            mult_intersect = linemerge(mult_intersect)
7✔
502
            if isinstance(mult_intersect, shpg.linestring.LineString):
7✔
503
                mult_intersect = [mult_intersect]
7✔
504

505
            try:
7✔
506
                # Compat with shapely > 2.0
507
                mult_intersect = mult_intersect.geoms
7✔
508
            except AttributeError:
7✔
509
                pass
7✔
510

511
            for line in mult_intersect:
7✔
512
                if not isinstance(line, shpg.linestring.LineString):
7✔
513
                    raise RuntimeError('polygon_intersections: we expect'
×
514
                                       'a LineString but got a '
515
                                       '{}.'.format(line.geom_type))
516
                line = gpd.GeoDataFrame([[i, j, line]],
7✔
517
                                        columns=out_cols)
518
                out = pd.concat([out, line])
7✔
519

520
    return out
7✔
521

522

523
def recursive_valid_polygons(geoms, crs):
9✔
524
    """Given a list of shapely geometries, makes sure all geometries are valid
525

526
    All will be valid polygons of area > 10000m2"""
527
    new_geoms = []
×
528
    for geom in geoms:
×
529
        new_geom = make_valid(geom)
×
530
        try:
×
531
            new_geoms.extend(recursive_valid_polygons(list(new_geom.geoms), crs))
×
532
        except AttributeError:
×
533
            new_s = gpd.GeoSeries(new_geom)
×
534
            new_s.crs = crs
×
535
            if new_s.to_crs({'proj': 'cea'}).area.iloc[0] >= 10000:
×
536
                new_geoms.append(new_geom)
×
537
    assert np.all([type(geom) == shpg.Polygon for geom in new_geoms])
×
538
    return new_geoms
×
539

540

541
def multipolygon_to_polygon(geometry, gdir=None):
9✔
542
    """Sometimes an RGI geometry is a multipolygon: this should not happen.
543

544
    It was vor vey old versions, pretty sure this is not needed anymore.
545

546
    Parameters
547
    ----------
548
    geometry : shpg.Polygon or shpg.MultiPolygon
549
        the geometry to check
550
    gdir : GlacierDirectory, optional
551
        for logging
552

553
    Returns
554
    -------
555
    the corrected geometry
556
    """
557

558
    # Log
559
    rid = gdir.rgi_id + ': ' if gdir is not None else ''
7✔
560

561
    if 'Multi' in geometry.geom_type:
7✔
562
        # needed for shapely version > 0.2.0
563
        # previous code was: parts = np.array(geometry)
564
        parts = []
×
565
        for p in geometry.geoms:
×
566
            parts.append(p)
×
567
        parts = np.array(parts)
×
568

569
        for p in parts:
×
570
            assert p.geom_type == 'Polygon'
×
571
        areas = np.array([p.area for p in parts])
×
572
        parts = parts[np.argsort(areas)][::-1]
×
573
        areas = areas[np.argsort(areas)][::-1]
×
574

575
        # First case (was RGIV4):
576
        # let's assume that one poly is exterior and that
577
        # the other polygons are in fact interiors
578
        exterior = parts[0].exterior
×
579
        interiors = []
×
580
        was_interior = 0
×
581
        for p in parts[1:]:
×
582
            if parts[0].contains(p):
×
583
                interiors.append(p.exterior)
×
584
                was_interior += 1
×
585
        if was_interior > 0:
×
586
            # We are done here, good
587
            geometry = shpg.Polygon(exterior, interiors)
×
588
        else:
589
            # This happens for bad geometries. We keep the largest
590
            geometry = parts[0]
×
591
            if np.any(areas[1:] > (areas[0] / 4)):
×
592
                log.info('Geometry {} lost quite a chunk.'.format(rid))
×
593

594
    if geometry.geom_type != 'Polygon':
7✔
595
        raise InvalidGeometryError('Geometry {} is not a Polygon.'.format(rid))
×
596
    return geometry
7✔
597

598

599
def floatyear_to_date(yr):
9✔
600
    """Converts a float year to an actual (year, month) pair.
601

602
    Note that this doesn't account for leap years (365-day no leap calendar),
603
    and that the months all have the same length.
604

605
    Parameters
606
    ----------
607
    yr : float
608
        The floating year
609
    """
610

611
    try:
9✔
612
        sec, out_y = math.modf(yr)
9✔
613
        out_y = int(out_y)
9✔
614
        sec = round(sec * SEC_IN_YEAR)
9✔
615
        if sec == SEC_IN_YEAR:
9✔
616
            # Floating errors
617
            out_y += 1
1✔
618
            sec = 0
1✔
619
        out_m = int(sec / SEC_IN_MONTH) + 1
9✔
620
    except TypeError:
8✔
621
        # TODO: inefficient but no time right now
622
        out_y = np.zeros(len(yr), np.int64)
8✔
623
        out_m = np.zeros(len(yr), np.int64)
8✔
624
        for i, y in enumerate(yr):
8✔
625
            y, m = floatyear_to_date(y)
8✔
626
            out_y[i] = y
8✔
627
            out_m[i] = m
8✔
628
    return out_y, out_m
9✔
629

630

631
def date_to_floatyear(y, m):
9✔
632
    """Converts an integer (year, month) pair to a float year.
633

634
    Note that this doesn't account for leap years (365-day no leap calendar),
635
    and that the months all have the same length.
636

637
    Parameters
638
    ----------
639
    y : int
640
        the year
641
    m : int
642
        the month
643
    """
644

645
    return (np.asanyarray(y) + (np.asanyarray(m) - 1) *
9✔
646
            SEC_IN_MONTH / SEC_IN_YEAR)
647

648

649
def hydrodate_to_calendardate(y, m, start_month=None):
9✔
650
    """Converts a hydrological (year, month) pair to a calendar date.
651

652
    Parameters
653
    ----------
654
    y : int
655
        the year
656
    m : int
657
        the month
658
    start_month : int
659
        the first month of the hydrological year
660
    """
661

662
    if start_month is None:
1✔
663
        raise InvalidParamsError('In order to avoid confusion, we now force '
×
664
                                 'callers of this function to specify the '
665
                                 'hydrological convention they are using.')
666

667
    e = 13 - start_month
1✔
668
    try:
1✔
669
        if m <= e:
1✔
670
            if start_month == 1:
1✔
671
                out_y = y
1✔
672
            else:
673
                out_y = y - 1
1✔
674
            out_m = m + start_month - 1
1✔
675
        else:
676
            out_y = y
1✔
677
            out_m = m - e
1✔
678
    except (TypeError, ValueError):
1✔
679
        # TODO: inefficient but no time right now
680
        out_y = np.zeros(len(y), np.int64)
1✔
681
        out_m = np.zeros(len(y), np.int64)
1✔
682
        for i, (_y, _m) in enumerate(zip(y, m)):
1✔
683
            _y, _m = hydrodate_to_calendardate(_y, _m, start_month=start_month)
1✔
684
            out_y[i] = _y
1✔
685
            out_m[i] = _m
1✔
686
    return out_y, out_m
1✔
687

688

689
def calendardate_to_hydrodate(y, m, start_month=None):
9✔
690
    """Converts a calendar (year, month) pair to a hydrological date.
691

692
    Parameters
693
    ----------
694
    y : int
695
        the year
696
    m : int
697
        the month
698
    start_month : int
699
        the first month of the hydrological year
700
    """
701

702
    if start_month is None:
8✔
703
        raise InvalidParamsError('In order to avoid confusion, we now force '
×
704
                                 'callers of this function to specify the '
705
                                 'hydrological convention they are using.')
706

707
    try:
8✔
708
        if m >= start_month:
8✔
709
            if start_month == 1:
5✔
710
                out_y = y
1✔
711
            else:
712
                out_y = y + 1
5✔
713
            out_m = m - start_month + 1
5✔
714
        else:
715
            out_y = y
8✔
716
            out_m = m + 13 - start_month
8✔
717
    except (TypeError, ValueError):
8✔
718
        # TODO: inefficient but no time right now
719
        out_y = np.zeros(len(y), np.int64)
8✔
720
        out_m = np.zeros(len(y), np.int64)
8✔
721
        for i, (_y, _m) in enumerate(zip(y, m)):
8✔
722
            _y, _m = calendardate_to_hydrodate(_y, _m, start_month=start_month)
8✔
723
            out_y[i] = _y
8✔
724
            out_m[i] = _m
8✔
725
    return out_y, out_m
8✔
726

727

728
def monthly_timeseries(y0, y1=None, ny=None, include_last_year=False):
9✔
729
    """Creates a monthly timeseries in units of float years.
730

731
    Parameters
732
    ----------
733
    """
734

735
    if y1 is not None:
9✔
736
        years = np.arange(np.floor(y0), np.floor(y1) + 1)
9✔
737
    elif ny is not None:
1✔
738
        years = np.arange(np.floor(y0), np.floor(y0) + ny)
1✔
739
    else:
740
        raise ValueError("Need at least two positional arguments.")
1✔
741
    months = np.tile(np.arange(12) + 1, len(years))
9✔
742
    years = years.repeat(12)
9✔
743
    out = date_to_floatyear(years, months)
9✔
744
    if not include_last_year:
9✔
745
        out = out[:-11]
9✔
746
    return out
9✔
747

748

749
def filter_rgi_name(name):
9✔
750
    """Remove spurious characters and trailing blanks from RGI glacier name.
751

752
    This seems to be unnecessary with RGI V6
753
    """
754

755
    try:
7✔
756
        if name is None or len(name) == 0:
7✔
757
            return ''
4✔
758
    except TypeError:
3✔
759
        return ''
3✔
760

761
    if name[-1] in ['À', 'È', 'è', '\x9c', '3', 'Ð', '°', '¾',
7✔
762
                    '\r', '\x93', '¤', '0', '`', '/', 'C', '@',
763
                    'Å', '\x06', '\x10', '^', 'å', ';']:
764
        return filter_rgi_name(name[:-1])
7✔
765

766
    return name.strip().title()
7✔
767

768

769
def shape_factor_huss(widths, heights, is_rectangular):
9✔
770
    """Shape factor for lateral drag according to Huss and Farinotti (2012).
771

772
    The shape factor is only applied for parabolic sections.
773

774
    Parameters
775
    ----------
776
    widths: ndarray of floats
777
        widths of the sections
778
    heights: float or ndarray of floats
779
        height of the sections
780
    is_rectangular: bool or ndarray of bools
781
        determines, whether section has a rectangular or parabolic shape
782

783
    Returns
784
    -------
785
    shape factor (no units)
786
    """
787

788
    # Ensure bool (for masking)
789
    is_rect = is_rectangular.astype(bool)
×
790
    shape_factors = np.ones(widths.shape)
×
791

792
    with warnings.catch_warnings():
×
793
        warnings.filterwarnings("ignore", category=RuntimeWarning)
×
794
        shape_factors[~is_rect] = (widths / (2 * heights + widths))[~is_rect]
×
795

796
    # For very small thicknesses ignore
797
    shape_factors[heights <= 1.] = 1.
×
798
    # Security
799
    shape_factors = clip_array(shape_factors, 0.2, 1.)
×
800

801
    return shape_factors
×
802

803

804
def shape_factor_adhikari(widths, heights, is_rectangular):
9✔
805
    """Shape factor for lateral drag according to Adhikari (2012).
806

807
    TODO: other factors could be used when sliding is included
808

809
    Parameters
810
    ----------
811
    widths: ndarray of floats
812
        widths of the sections
813
    heights: ndarray of floats
814
        heights of the sections
815
    is_rectangular: ndarray of bools
816
        determines, whether section has a rectangular or parabolic shape
817

818
    Returns
819
    -------
820
    shape factors (no units), ndarray of floats
821
    """
822

823
    # Ensure bool (for masking)
824
    is_rectangular = is_rectangular.astype(bool)
1✔
825

826
    # Catch for division by 0 (corrected later)
827
    with warnings.catch_warnings():
1✔
828
        warnings.filterwarnings('ignore', category=RuntimeWarning)
1✔
829
        zetas = widths / 2. / heights
1✔
830

831
    shape_factors = np.ones(widths.shape)
1✔
832

833
    # TODO: higher order interpolation? (e.g. via InterpolatedUnivariateSpline)
834
    shape_factors[is_rectangular] = ADHIKARI_FACTORS_RECTANGULAR(
1✔
835
        zetas[is_rectangular])
836
    shape_factors[~is_rectangular] = ADHIKARI_FACTORS_PARABOLIC(
1✔
837
        zetas[~is_rectangular])
838

839
    shape_factors = clip_array(shape_factors, 0.2, 1.)
1✔
840
    # Set NaN values resulting from zero height to a shape factor of 1
841
    shape_factors[np.isnan(shape_factors)] = 1.
1✔
842

843
    return shape_factors
1✔
844

845

846
def cook_rgidf(gi_gdf, o1_region, o2_region='01', version='60', ids=None,
9✔
847
               bgndate='20009999', id_suffix='', assign_column_values=None):
848
    """Convert a glacier inventory into a dataset looking like the RGI (OGGM ready).
849

850
    Parameters
851
    ----------
852
    gi_gdf : :py:geopandas.GeoDataFrame
853
        the GeoDataFrame of the user's glacier inventory.
854
    o1_region : str
855
        Glacier RGI region code, which is important for some OGGM applications.
856
        For example, oggm.shop.its_live() need it to locate the right dataset.
857
        Needs to be assigned.
858
    o2_region : str or list of str
859
        Glacier RGI subregion code (default: 01)
860
    bgndate : str or list of str
861
        The date of the outlines. This is quite important for the glacier
862
        evolution runs, which start at the RGI date. Format: ``'YYYYMMDD'``,
863
        (MMDD is not used).
864
    version : str
865
        Glacier inventory version code, which is necessary to generate the RGIId.
866
        The default is '60'.
867
    ids : list of IDs as integers. The default is None, which generates the
868
        RGI ids automatically following the glacier order.
869
    id_suffix : str or None
870
         Add a suffix to the glacier ids. The default is None, no suffix
871
    assign_column_values : dict or None
872
        Assign predefined values from the original data to the RGI dataframe.
873
        Dict format :
874
        - key: name of the column in the original dataframe
875
        - value: name of the column in the RGI-like file to assign the values to
876

877
    Returns
878
    -------
879
    cooked_rgidf : :py:geopandas.GeoDataFrame
880
        glacier inventory into a dataset looking like the RGI (OGGM ready)
881
    """
882

883
    # Check input
884
    if ids is None:
1✔
885
        ids = range(1, len(gi_gdf)+1)
1✔
886

887
    # Construct a fake rgiid list following RGI format
888
    str_ids = ['RGI{}-{}.{:05d}{}'.format(version, o1_region, int(i), id_suffix)
1✔
889
               for i in ids]
890

891
    # Check the coordination system.
892
    # RGI use the geographic coordinate.
893
    # So if the original glacier inventory is not in geographic coordinate,
894
    # we need to convert both of the central point and the glacier outline
895
    # to geographic coordinate (WGS 84)
896
    if gi_gdf.crs != 'epsg:4326':
1✔
897
        gi_gdf = gi_gdf.to_crs('epsg:4326')
1✔
898

899
    # Calculate the central point of the glaciers
900
    geom = gi_gdf.geometry.values
1✔
901
    centroids = gi_gdf.geometry.representative_point()
1✔
902
    clon, clat = centroids.x, centroids.y
1✔
903

904
    # Prepare data for the output GeoDataFrame
905
    data = {'RGIId': str_ids, 'CenLon': clon, 'CenLat': clat, 'GLIMSId': '',
1✔
906
            'BgnDate': bgndate, 'EndDate': '9999999',
907
            'O1Region': o1_region, 'O2Region': o2_region,
908
            'Area': -9999., 'Zmin': -9999., 'Zmax': -9999., 'Zmed': -9999.,
909
            'Slope': -9999., 'Aspect': -9999, 'Lmax': -9999,
910
            'Status': 0, 'Connect': 0, 'Form': 0, 'TermType': 0, 'Surging': 0,
911
            'Linkages': 1, 'check_geom': None, 'Name': ''}
912

913
    # Construct the output GeoDataFrame
914
    cooked_rgidf = gpd.GeoDataFrame(data=data, geometry=geom, crs='epsg:4326')
1✔
915

916
    # If there are specific column in the original glacier inventory we want to keep
917
    if assign_column_values is not None:
1✔
918
        for key, val in assign_column_values.items():
1✔
919
            cooked_rgidf[val] = gi_gdf[key].values
1✔
920

921
    return cooked_rgidf
1✔
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