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

OGGM / oggm / 4510759150

pending completion
4510759150

Pull #1549

github

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

11167 of 13069 relevant lines covered (85.45%)

3.56 hits per line

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

90.92
/oggm/core/centerlines.py
1
""" Compute the centerlines according to Kienholz et al (2014) - with
2
modifications.
3

4
The output is a list of Centerline objects, stored as a list in a pickle.
5
The order of the list is important since the lines are
6
sorted per order (hydrological flow level), from the lower orders (upstream)
7
to the higher orders (downstream). Several tools later on rely on this order
8
so don't mess with it.
9

10
References::
11

12
    Kienholz, C., Rich, J. L., Arendt, a. a., and Hock, R. (2014).
13
        A new method for deriving glacier centerlines applied to glaciers in
14
        Alaska and northwest Canada. The Cryosphere, 8(2), 503-519.
15
        doi:10.5194/tc-8-503-2014
16
"""
17
# Built ins
18
import warnings
9✔
19
import logging
9✔
20
import copy
9✔
21
from itertools import groupby
9✔
22
from collections import Counter
9✔
23
from packaging.version import Version
9✔
24

25
# External libs
26
import numpy as np
9✔
27
import pandas as pd
9✔
28
import shapely.ops
9✔
29
import scipy.signal
9✔
30
import shapely.geometry as shpg
9✔
31
from scipy.interpolate import RegularGridInterpolator
9✔
32
from scipy.ndimage import (gaussian_filter1d, distance_transform_edt,
9✔
33
                           label, find_objects)
34

35
# Optional libs
36
try:
9✔
37
    import salem
9✔
38
except ImportError:
39
    pass
40
try:
9✔
41
    import geopandas as gpd
9✔
42
except ImportError:
43
    pass
44
try:
9✔
45
    from skimage import measure
9✔
46
    from skimage.graph import route_through_array
8✔
47
except ImportError:
48
    pass
49

50
# Locals
51
import oggm.cfg as cfg
9✔
52
from oggm.cfg import GAUSSIAN_KERNEL
9✔
53
from oggm import utils
9✔
54
from oggm.utils import (tuple2int, line_interpol, interp_nans, lazy_property,
9✔
55
                        SuperclassMeta)
56
from oggm import entity_task
9✔
57
from oggm.exceptions import (InvalidParamsError, InvalidGeometryError,
9✔
58
                             GeometryError, InvalidDEMError)
59

60
# Module logger
61
log = logging.getLogger(__name__)
9✔
62

63
# Variable needed later
64
LABEL_STRUCT = np.array([[0, 1, 0],
9✔
65
                         [1, 1, 1],
66
                         [0, 1, 0]])
67

68

69
class Centerline(object, metaclass=SuperclassMeta):
9✔
70
    """Geometry (line and widths) and flow rooting properties, but no thickness
71
    """
72

73
    def __init__(self, line, dx=None, surface_h=None, orig_head=None,
9✔
74
                 rgi_id=None, map_dx=None):
75
        """ Initialize a Centerline
76

77
        Parameters
78
        ----------
79
        line : :py:class:`shapely.geometry.LineString`
80
            The geometrically calculated centerline
81
        dx : float
82
            Grid spacing of the initialised flowline in pixel coordinates
83
        surface_h :  :py:class:`numpy.ndarray`
84
            elevation [m] of the points on ``line``
85
        orig_head : :py:class:`shapely.geometry.Point`
86
            geometric point of the lines head
87
        rgi_id : str
88
            The glacier's RGI identifier
89
        map_dx : float
90
            the map's grid resolution. Centerline.dx_meter = dx * map_dx
91
        """
92

93
        self.line = None  # Shapely LineString
9✔
94
        self.head = None  # Shapely Point
9✔
95
        self.tail = None  # Shapely Point
9✔
96
        self.dis_on_line = None
9✔
97
        self.nx = None
9✔
98
        if line is not None:
9✔
99
            self.set_line(line)  # Init all previous properties
9✔
100
        else:
101
            self.nx = len(surface_h)
4✔
102
            self.dis_on_line = np.arange(self.nx) * dx
4✔
103

104
        self.order = None  # Hydrological flow level (~ Strahler number)
9✔
105

106
        # These are computed at run time by compute_centerlines
107
        self.flows_to = None  # pointer to a Centerline object (when available)
9✔
108
        self.flows_to_point = None  # point of the junction in flows_to
9✔
109
        self.inflows = []  # list of Centerline instances (when available)
9✔
110
        self.inflow_points = []  # junction points
9✔
111

112
        # Optional attrs
113
        self.dx = dx  # dx in pixels (assumes the line is on constant dx
9✔
114
        self.map_dx = map_dx  # the pixel spacing
9✔
115
        try:
9✔
116
            self.dx_meter = self.dx * self.map_dx
9✔
117
        except TypeError:
9✔
118
            # For backwards compatibility we allow this for now
119
            self.dx_meter = None
9✔
120
        self._surface_h = surface_h
9✔
121
        self._widths = None
9✔
122
        self.is_rectangular = None
9✔
123
        self.is_trapezoid = None
9✔
124
        self.orig_head = orig_head  # Useful for debugging and for filtering
9✔
125
        self.geometrical_widths = None  # these are kept for plotting and such
9✔
126
        self.apparent_mb = None  # Apparent MB, NOT weighted by width.
9✔
127
        self.rgi_id = rgi_id  # Useful if line is used with another glacier
9✔
128
        self.flux = None  # Flux (kg m-2)
9✔
129
        self.flux_needs_correction = False  # whether this branch was baaad
9✔
130
        self.flux_out = None  # Flux (kg m-2) flowing out of the centerline
9✔
131

132
    def set_flows_to(self, other, check_tail=True, to_head=False):
9✔
133
        """Find the closest point in "other" and sets all the corresponding
134
        attributes. Btw, it modifies the state of "other" too.
135

136
        Parameters
137
        ----------
138
        other : :py:class:`oggm.Centerline`
139
            another flowline where self should flow to
140
        """
141

142
        self.flows_to = other
8✔
143

144
        if check_tail:
8✔
145
            # Project the point and Check that its not too close
146
            prdis = other.line.project(self.tail, normalized=False)
8✔
147
            ind_closest = np.argmin(np.abs(other.dis_on_line - prdis)).item()
8✔
148
            n = len(other.dis_on_line)
8✔
149
            if n >= 9:
8✔
150
                ind_closest = utils.clip_scalar(ind_closest, 4, n-5)
8✔
151
            elif n >= 7:
×
152
                ind_closest = utils.clip_scalar(ind_closest, 3, n-4)
×
153
            elif n >= 5:
×
154
                ind_closest = utils.clip_scalar(ind_closest, 2, n-3)
×
155
            p = shpg.Point(other.line.coords[int(ind_closest)])
8✔
156
            self.flows_to_point = p
8✔
157
        elif to_head:
6✔
158
            self.flows_to_point = other.head
1✔
159
        else:
160
            # just the closest
161
            self.flows_to_point = _projection_point(other, self.tail)
5✔
162
        other.inflow_points.append(self.flows_to_point)
8✔
163
        other.inflows.append(self)
8✔
164

165
    def set_line(self, line):
9✔
166
        """Update the Shapely LineString coordinate.
167

168
        Parameters
169
        ----------
170
        line : :py:class`shapely.geometry.LineString`
171
        """
172

173
        self.nx = len(line.coords)
9✔
174
        self.line = line
9✔
175
        dis = [line.project(shpg.Point(co)) for co in line.coords]
9✔
176
        self.dis_on_line = np.array(dis)
9✔
177
        xx, yy = line.xy
9✔
178
        self.head = shpg.Point(xx[0], yy[0])
9✔
179
        self.tail = shpg.Point(xx[-1], yy[-1])
9✔
180

181
    @lazy_property
9✔
182
    def flows_to_indice(self):
9✔
183
        """Indices instead of geometry"""
184

185
        ind = []
8✔
186
        tofind = self.flows_to_point.coords[0]
8✔
187
        for i, p in enumerate(self.flows_to.line.coords):
8✔
188
            if p == tofind:
8✔
189
                ind.append(i)
8✔
190
        assert len(ind) == 1, 'We expect exactly one point to be found here.'
8✔
191
        return ind[0]
8✔
192

193
    @lazy_property
9✔
194
    def inflow_indices(self):
9✔
195
        """Indices instead of geometries"""
196

197
        inds = []
5✔
198
        for p in self.inflow_points:
5✔
199
            ind = [i for (i, pi) in enumerate(self.line.coords)
5✔
200
                   if (p.coords[0] == pi)]
201
            inds.append(ind[0])
5✔
202
        assert len(inds) == len(self.inflow_points), ('For every inflow point '
5✔
203
                                                      'there should be '
204
                                                      'exactly one inflow '
205
                                                      'indice')
206
        return inds
5✔
207

208
    @lazy_property
9✔
209
    def normals(self):
9✔
210
        """List of (n1, n2) normal vectors at each point.
211

212
        We use second order derivatives for smoother widths.
213
        """
214

215
        pcoords = np.array(self.line.coords)
7✔
216

217
        normals = []
7✔
218
        # First
219
        normal = np.array(pcoords[1, :] - pcoords[0, :])
7✔
220
        normals.append(_normalize(normal))
7✔
221
        # Second
222
        normal = np.array(pcoords[2, :] - pcoords[0, :])
7✔
223
        normals.append(_normalize(normal))
7✔
224
        # Others
225
        for (bbef, bef, cur, aft, aaft) in zip(pcoords[:-4, :],
7✔
226
                                               pcoords[1:-3, :],
227
                                               pcoords[2:-2, :],
228
                                               pcoords[3:-1, :],
229
                                               pcoords[4:, :]):
230
            normal = np.array(aaft + 2*aft - 2*bef - bbef)
7✔
231
            normals.append(_normalize(normal))
7✔
232
        # One before last
233
        normal = np.array(pcoords[-1, :] - pcoords[-3, :])
7✔
234
        normals.append(_normalize(normal))
7✔
235
        # Last
236
        normal = np.array(pcoords[-1, :] - pcoords[-2, :])
7✔
237
        normals.append(_normalize(normal))
7✔
238

239
        return normals
7✔
240

241
    @property
9✔
242
    def widths(self):
9✔
243
        """Needed for overriding later"""
244
        return self._widths
7✔
245

246
    @property
9✔
247
    def widths_m(self):
9✔
248
        return self.widths * self.map_dx
1✔
249

250
    @widths.setter
9✔
251
    def widths(self, value):
9✔
252
        self._widths = value
7✔
253

254
    @property
9✔
255
    def surface_h(self):
9✔
256
        """Needed for overriding later"""
257
        return self._surface_h
7✔
258

259
    @surface_h.setter
9✔
260
    def surface_h(self, value):
9✔
261
        self._surface_h = value
1✔
262

263
    def reset_flux(self):
9✔
264
        self.flux = np.zeros(len(self.surface_h))  # Flux (kg m-2)
7✔
265
        self.flux_needs_correction = False  # whether this branch was baaad
7✔
266
        self.flux_out = None  # Flux (kg m-2) flowing out of the centerline
7✔
267
        self.apparent_mb = None
7✔
268

269
    def set_apparent_mb(self, mb, is_calving=None):
9✔
270
        """Set the apparent mb and flux for the flowline.
271

272
        MB is expected in kg m-2 yr-1 (= mm w.e. yr-1)
273

274
        This should happen in line order, otherwise it will be wrong.
275

276
        Parameters
277
        ----------
278
        is_calving : bool
279
            if calving line the last grid cell is seen as a pure calving cell
280
            (the ice flux through the last cell is equal the calving flux),
281
            in the other case the flux is calculated incorporating the smb of
282
            the last grid cell (the ice flux through the last cell is equal to
283
            the smb)
284
        """
285
        if is_calving is None:
7✔
286
            raise InvalidParamsError('is_calving needs to be True or False')
×
287

288
        self.apparent_mb = mb
7✔
289

290
        # Add MB to current flux and sum
291
        # no more changes should happen after that
292
        smb = mb * self.widths * self.dx
7✔
293
        if is_calving:
7✔
294
            # in a calving case we see the last grid cell as the calving cell
295
            # and the extra added cell has no meaning
296
            smb_add = np.concatenate((smb, [0]))
4✔
297
        else:
298
            # differentiate between positive and negative smb, negative is
299
            # shifted one position and subtracted only after the ice flew
300
            # through the cell
301
            smb_pos = np.concatenate((np.where(smb > 0, smb, 0), [0]))
7✔
302
            smb_neg = np.concatenate(([0], np.where(smb < 0, smb, 0)))
7✔
303
            smb_add = smb_pos + smb_neg
7✔
304
        flux_ext = np.concatenate((self.flux, [0]))
7✔
305
        flux_needs_correction = False
7✔
306
        flux = np.cumsum(flux_ext + smb_add)
7✔
307

308
        # We filter lines with a negative flux at the last grid point, the
309
        # threshold of -1e-5 is needed to avoid problems with numeric precision
310
        if flux[-2] < -1e-5:
7✔
311
            flux_needs_correction = True
3✔
312

313
        self.flux = flux[:-1]
7✔
314
        self.flux_out = flux[-1]
7✔
315
        self.flux_needs_correction = flux_needs_correction
7✔
316

317
        # Add to outflow. That's why it should happen in order
318
        if self.flows_to is not None:
7✔
319
            n = len(self.flows_to.line.coords)
7✔
320
            ide = self.flows_to_indice
7✔
321
            if n >= 9:
7✔
322
                gk = GAUSSIAN_KERNEL[9]
7✔
323
                self.flows_to.flux[ide-4:ide+5] += gk * flux[-1]
7✔
324
            elif n >= 7:
×
325
                gk = GAUSSIAN_KERNEL[7]
×
326
                self.flows_to.flux[ide-3:ide+4] += gk * flux[-1]
×
327
            elif n >= 5:
×
328
                gk = GAUSSIAN_KERNEL[5]
×
329
                self.flows_to.flux[ide-2:ide+3] += gk * flux[-1]
×
330

331

332
def _filter_heads(heads, heads_height, radius, polygon):
9✔
333
    """Filter the head candidates following Kienholz et al. (2014), Ch. 4.1.2
334

335
    Parameters
336
    ----------
337
    heads : list of shapely.geometry.Point instances
338
        The heads to filter out (in raster coordinates).
339
    heads_height : list
340
        The heads altitudes.
341
    radius : float
342
        The radius around each head to search for potential challengers
343
    polygon : shapely.geometry.Polygon class instance
344
        The glacier geometry (in raster coordinates).
345

346
    Returns
347
    -------
348
    a list of shapely.geometry.Point instances with the "bad ones" removed
349
    """
350

351
    heads = copy.copy(heads)
5✔
352
    heads_height = copy.copy(heads_height)
5✔
353

354
    i = 0
5✔
355
    # I think a "while" here is ok: we remove the heads forwards only
356
    while i < len(heads):
5✔
357
        head = heads[i]
5✔
358
        pbuffer = head.buffer(radius)
5✔
359
        inter_poly = pbuffer.intersection(polygon.exterior)
5✔
360
        if inter_poly.geom_type in ['MultiPolygon',
5✔
361
                                    'GeometryCollection',
362
                                    'MultiLineString']:
363
            #  In the case of a junction point, we have to do a check
364
            # http://lists.gispython.org/pipermail/community/
365
            # 2015-January/003357.html
366
            if inter_poly.geom_type == 'MultiLineString':
5✔
367
                inter_poly = shapely.ops.linemerge(inter_poly)
5✔
368

369
            if inter_poly.geom_type != 'LineString':
5✔
370
                # keep the local polygon only
371
                for sub_poly in inter_poly.geoms:
5✔
372
                    if sub_poly.intersects(head):
5✔
373
                        inter_poly = sub_poly
5✔
374
                        break
5✔
375
        elif inter_poly.geom_type == 'LineString':
5✔
376
            inter_poly = shpg.Polygon(np.asarray(inter_poly.xy).T)
5✔
377
        elif inter_poly.geom_type == 'Polygon':
×
378
            pass
×
379
        else:
380
            extext = ('Geometry collection not expected: '
×
381
                      '{}'.format(inter_poly.geom_type))
382
            raise InvalidGeometryError(extext)
×
383

384
        # Find other points in radius and in polygon
385
        _heads = [head]
5✔
386
        _z = [heads_height[i]]
5✔
387
        for op, z in zip(heads[i+1:], heads_height[i+1:]):
5✔
388
            if inter_poly.intersects(op):
5✔
389
                _heads.append(op)
5✔
390
                _z.append(z)
5✔
391

392
        # If alone, go to the next point
393
        if len(_heads) == 1:
5✔
394
            i += 1
5✔
395
            continue
5✔
396

397
        # If not, keep the highest
398
        _w = np.argmax(_z)
5✔
399

400
        for head in _heads:
5✔
401
            if not (head is _heads[_w]):
5✔
402
                heads_height = np.delete(heads_height, heads.index(head))
5✔
403
                heads.remove(head)
5✔
404

405
    return heads, heads_height
5✔
406

407

408
def _filter_lines(lines, heads, k, r):
9✔
409
    """Filter the centerline candidates by length.
410

411
    Kienholz et al. (2014), Ch. 4.3.1
412

413
    Parameters
414
    ----------
415
    lines : list of shapely.geometry.LineString instances
416
        The lines to filter out (in raster coordinates).
417
    heads :  list of shapely.geometry.Point instances
418
        The heads corresponding to the lines.
419
    k : float
420
        A buffer (in raster coordinates) to cut around the selected lines
421
    r : float
422
        The lines shorter than r will be filtered out.
423

424
    Returns
425
    -------
426
    (lines, heads) a list of the new lines and corresponding heads
427
    """
428

429
    olines = []
5✔
430
    oheads = []
5✔
431
    ilines = copy.copy(lines)
5✔
432

433
    lastline = None
5✔
434
    while len(ilines) > 0:  # loop as long as we haven't filtered all lines
5✔
435
        if len(olines) > 0:  # enter this after the first step only
5✔
436

437
            toremove = lastline.buffer(k)  # buffer centerlines the last line
5✔
438
            tokeep = []
5✔
439
            for l in ilines:
5✔
440
                # loop over all remaining lines and compute their diff
441
                # to the last longest line
442
                diff = l.difference(toremove)
5✔
443
                if diff.geom_type == 'MultiLineString':
5✔
444
                    # Remove the lines that have no head
445
                    diff = list(diff.geoms)
2✔
446
                    for il in diff:
2✔
447
                        hashead = False
2✔
448
                        for h in heads:
2✔
449
                            if il.intersects(h):
2✔
450
                                hashead = True
2✔
451
                                diff = il
2✔
452
                                break
2✔
453
                        if hashead:
2✔
454
                            break
2✔
455
                        else:
456
                            diff = None
×
457
                # keep this head line only if it's long enough
458
                if diff is not None and diff.length > r:
5✔
459
                    # Fun fact. The heads can be cut by the buffer too
460
                    diff = shpg.LineString(l.coords[0:2] + diff.coords[2:])
5✔
461
                    tokeep.append(diff)
5✔
462
            ilines = tokeep
5✔
463

464
        # it could happen that we're done at this point
465
        if len(ilines) == 0:
5✔
466
            break
1✔
467

468
        # Otherwise keep the longest one and continue
469
        lengths = np.array([])
5✔
470
        for l in ilines:
5✔
471
            lengths = np.append(lengths, l.length)
5✔
472
        ll = ilines[np.argmax(lengths)]
5✔
473

474
        ilines.remove(ll)
5✔
475
        if len(olines) > 0:
5✔
476
            # the cut line's last point is not guaranteed
477
            # to on straight coordinates. Remove it
478
            olines.append(shpg.LineString(np.asarray(ll.xy)[:, 0:-1].T))
5✔
479
        else:
480
            olines.append(ll)
5✔
481
        lastline = ll
5✔
482

483
    # add the corresponding head to each line
484
    for l in olines:
5✔
485
        for h in heads:
5✔
486
            if l.intersects(h):
5✔
487
                oheads.append(h)
5✔
488
                break
5✔
489

490
    return olines, oheads
5✔
491

492

493
def _filter_lines_slope(lines, heads, topo, gdir, min_slope):
9✔
494
    """Filter the centerline candidates by slope: if they go up, remove
495

496
    Kienholz et al. (2014), Ch. 4.3.1
497

498
    Parameters
499
    ----------
500
    lines : list of shapely.geometry.LineString instances
501
        The lines to filter out (in raster coordinates).
502
    topo : the glacier topography
503
    gdir : the glacier directory for simplicity
504
    min_slope: rad
505

506
    Returns
507
    -------
508
    (lines, heads) a list of the new lines and corresponding heads
509
    """
510

511
    dx_cls = cfg.PARAMS['flowline_dx']
5✔
512
    lid = int(cfg.PARAMS['flowline_junction_pix'])
5✔
513
    sw = cfg.PARAMS['flowline_height_smooth']
5✔
514

515
    # Bilinear interpolation
516
    # Geometries coordinates are in "pixel centered" convention, i.e
517
    # (0, 0) is also located in the center of the pixel
518
    xy = (np.arange(0, gdir.grid.ny-0.1, 1),
5✔
519
          np.arange(0, gdir.grid.nx-0.1, 1))
520
    interpolator = RegularGridInterpolator(xy, topo.astype(np.float64))
5✔
521

522
    olines = [lines[0]]
5✔
523
    oheads = [heads[0]]
5✔
524
    for line, head in zip(lines[1:], heads[1:]):
5✔
525

526
        # The code below mimics what initialize_flowlines will do
527
        # this is a bit smelly but necessary
528
        points = line_interpol(line, dx_cls)
5✔
529

530
        # For tributaries, remove the tail
531
        points = points[0:-lid]
5✔
532

533
        new_line = shpg.LineString(points)
5✔
534

535
        # Interpolate heights
536
        x, y = new_line.xy
5✔
537
        hgts = interpolator((np.array(y), np.array(x)))
5✔
538

539
        # If smoothing, this is the moment
540
        hgts = gaussian_filter1d(hgts, sw)
5✔
541

542
        # Finally slope
543
        slope = np.arctan(-np.gradient(hgts, dx_cls*gdir.grid.dx))
5✔
544

545
        # And altitude range
546
        z_range = np.max(hgts) - np.min(hgts)
5✔
547

548
        # arbitrary threshold with which we filter the lines, otherwise bye bye
549
        if np.sum(slope >= min_slope) >= 5 and z_range > 10:
5✔
550
            olines.append(line)
5✔
551
            oheads.append(head)
5✔
552

553
    return olines, oheads
5✔
554

555

556
def _projection_point(centerline, point):
9✔
557
    """Projects a point on a line and returns the closest integer point
558
    guaranteed to be on the line, and guaranteed to be far enough from the
559
    head and tail.
560

561
    Parameters
562
    ----------
563
    centerline : Centerline instance
564
    point : Shapely Point geometry
565

566
    Returns
567
    -------
568
    (flow_point, ind_closest): Shapely Point and indice in the line
569
    """
570
    prdis = centerline.line.project(point, normalized=False)
5✔
571
    ind_closest = np.argmin(np.abs(centerline.dis_on_line - prdis)).item()
5✔
572
    flow_point = shpg.Point(centerline.line.coords[int(ind_closest)])
5✔
573
    return flow_point
5✔
574

575

576
def _join_lines(lines, heads):
9✔
577
    """Re-joins the lines that have been cut by _filter_lines
578

579
     Compute the rooting scheme.
580

581
    Parameters
582
    ----------
583
    lines: list of shapely lines instances
584

585
    Returns
586
    -------
587
    Centerline instances, updated with flow routing properties
588
     """
589

590
    olines = [Centerline(l, orig_head=h) for l, h
5✔
591
              in zip(lines[::-1], heads[::-1])]
592
    nl = len(olines)
5✔
593
    if nl == 1:
5✔
594
        return olines
2✔
595

596
    # per construction the line cannot flow in a line placed before in the list
597
    for i, l in enumerate(olines):
5✔
598

599
        last_point = shpg.Point(*l.line.coords[-1])
5✔
600

601
        totest = olines[i+1:]
5✔
602
        dis = [last_point.distance(t.line) for t in totest]
5✔
603
        flow_to = totest[np.argmin(dis)]
5✔
604

605
        flow_point = _projection_point(flow_to, last_point)
5✔
606

607
        # Interpolate to finish the line, bute force:
608
        # we interpolate 20 points, round them, remove consecutive duplicates
609
        endline = shpg.LineString([last_point, flow_point])
5✔
610
        endline = shpg.LineString([endline.interpolate(x, normalized=True)
5✔
611
                                   for x in np.linspace(0., 1., num=20)])
612
        # we keep all coords without the first AND the last
613
        grouped = groupby(map(tuple, np.rint(endline.coords)))
5✔
614
        endline = [x[0] for x in grouped][1:-1]
5✔
615

616
        # We're done
617
        l.set_line(shpg.LineString(l.line.coords[:] + endline))
5✔
618
        l.set_flows_to(flow_to, check_tail=False)
5✔
619

620
        # The last one has nowhere to flow
621
        if i+2 == nl:
5✔
622
            break
5✔
623

624
    return olines[::-1]
5✔
625

626

627
def line_order(line):
9✔
628
    """Recursive search for the line's hydrological level.
629

630
    Parameters
631
    ----------
632
    line: a Centerline instance
633

634
    Returns
635
    -------
636
    The line's order
637
    """
638

639
    if len(line.inflows) == 0:
7✔
640
        return 0
7✔
641
    else:
642
        levels = [line_order(s) for s in line.inflows]
7✔
643
        return np.max(levels) + 1
7✔
644

645

646
def line_inflows(line, keep=True):
9✔
647
    """Recursive search for all inflows of the given line.
648

649
    Parameters
650
    ----------
651
    line: a Centerline instance
652
    keep : bool
653
        whether or not the line itself should be kept
654

655
    Returns
656
    -------
657
    A list of lines (including the line itself) sorted in order
658
    """
659

660
    out = set([line])
2✔
661
    for l in line.inflows:
2✔
662
        out = out.union(line_inflows(l))
2✔
663

664
    out = np.array(list(out))
2✔
665
    out = list(out[np.argsort([o.order for o in out])])
2✔
666
    if not keep:
2✔
667
        out.remove(line)
1✔
668
    return out
2✔
669

670

671
def _make_costgrid(mask, ext, z):
9✔
672
    """Computes a costgrid following Kienholz et al. (2014) Eq. (2)
673

674
    Parameters
675
    ----------
676
    mask : numpy.array
677
        The glacier mask.
678
    ext : numpy.array
679
        The glacier boundaries' mask.
680
    z : numpy.array
681
        The terrain height.
682

683
    Returns
684
    -------
685
    numpy.array of the costgrid
686
    """
687

688
    dis = np.where(mask, distance_transform_edt(mask), np.NaN)
5✔
689
    z = np.where(mask, z, np.NaN)
5✔
690

691
    dmax = np.nanmax(dis)
5✔
692
    zmax = np.nanmax(z)
5✔
693
    zmin = np.nanmin(z)
5✔
694
    cost = ((dmax - dis) / dmax * cfg.PARAMS['f1']) ** cfg.PARAMS['a'] + \
5✔
695
           ((z - zmin) / (zmax - zmin) * cfg.PARAMS['f2']) ** cfg.PARAMS['b']
696

697
    # This is new: we make the cost to go over boundaries
698
    # arbitrary high to avoid the lines to jump over adjacent boundaries
699
    cost[np.where(ext)] = np.nanmax(cost[np.where(ext)]) * 50
5✔
700

701
    return np.where(mask, cost, np.Inf)
5✔
702

703

704
def _get_terminus_coord(gdir, ext_yx, zoutline):
9✔
705
    """This finds the terminus coordinate of the glacier.
706

707
     There is a special case for marine terminating glaciers/
708
     """
709

710
    perc = cfg.PARAMS['terminus_search_percentile']
5✔
711
    deltah = cfg.PARAMS['terminus_search_altitude_range']
5✔
712

713
    if gdir.is_tidewater and (perc > 0):
5✔
714
        # There is calving
715

716
        # find the lowest percentile
717
        plow = np.percentile(zoutline, perc).astype(np.int64)
3✔
718

719
        # the minimum altitude in the glacier
720
        mini = np.min(zoutline)
3✔
721

722
        # indices of where in the outline the altitude is lower than the qth
723
        # percentile and lower than 100m higher, than the minimum altitude
724
        ind = np.where((zoutline < plow) & (zoutline < (mini + deltah)))[0]
3✔
725

726
        # We take the middle of this area
727
        try:
3✔
728
            ind_term = ind[np.round(len(ind) / 2.).astype(int)]
3✔
729
        except IndexError:
×
730
            # Sometimes the default perc is not large enough
731
            try:
×
732
                # Repeat
733
                perc *= 2
×
734
                plow = np.percentile(zoutline, perc).astype(np.int64)
×
735
                mini = np.min(zoutline)
×
736
                ind = np.where((zoutline < plow) &
×
737
                               (zoutline < (mini + deltah)))[0]
738
                ind_term = ind[np.round(len(ind) / 2.).astype(int)]
×
739
            except IndexError:
×
740
                # Last resort
741
                ind_term = np.argmin(zoutline)
×
742
    else:
743
        # easy: just the minimum
744
        ind_term = np.argmin(zoutline)
5✔
745

746
    return np.asarray(ext_yx)[:, ind_term].astype(np.int64)
5✔
747

748

749
def _normalize(n):
9✔
750
    """Computes the normals of a vector n.
751

752
    Returns
753
    -------
754
    the two normals (n1, n2)
755
    """
756
    nn = n / np.sqrt(np.sum(n*n))
7✔
757
    n1 = np.array([-nn[1], nn[0]])
7✔
758
    n2 = np.array([nn[1], -nn[0]])
7✔
759
    return n1, n2
7✔
760

761

762
def _get_centerlines_heads(gdir, ext_yx, zoutline, single_fl,
9✔
763
                           glacier_mask, topo, geom, poly_pix):
764

765
    # Size of the half window to use to look for local maximas
766
    maxorder = np.rint(cfg.PARAMS['localmax_window'] / gdir.grid.dx)
5✔
767
    maxorder = utils.clip_scalar(maxorder, 5., np.rint((len(zoutline) / 5.)))
5✔
768
    heads_idx = scipy.signal.argrelmax(zoutline, mode='wrap',
5✔
769
                                       order=int(maxorder))
770
    if single_fl or len(heads_idx[0]) <= 1:
5✔
771
        # small glaciers with one or less heads: take the absolute max
772
        heads_idx = (np.atleast_1d(np.argmax(zoutline)),)
2✔
773

774
    # Remove the heads that are too low
775
    zglacier = topo[np.where(glacier_mask)]
5✔
776
    head_threshold = np.percentile(zglacier, (1./3.)*100)
5✔
777
    _heads_idx = heads_idx[0][np.where(zoutline[heads_idx] > head_threshold)]
5✔
778
    if len(_heads_idx) == 0:
5✔
779
        # this is for baaad ice caps where the outline is far off in altitude
780
        _heads_idx = [heads_idx[0][np.argmax(zoutline[heads_idx])]]
×
781
    heads_idx = _heads_idx
5✔
782
    heads = np.asarray(ext_yx)[:, heads_idx]
5✔
783
    heads_z = zoutline[heads_idx]
5✔
784
    # careful, the coords are in y, x order!
785
    heads = [shpg.Point(x, y) for y, x in zip(heads[0, :],
5✔
786
                                              heads[1, :])]
787

788
    # get radius of the buffer according to Kienholz eq. (1)
789
    radius = cfg.PARAMS['q1'] * geom['polygon_area'] + cfg.PARAMS['q2']
5✔
790
    radius = utils.clip_scalar(radius, 0, cfg.PARAMS['rmax'])
5✔
791
    radius /= gdir.grid.dx  # in raster coordinates
5✔
792
    # Plus our criteria, quite useful to remove short lines:
793
    radius += cfg.PARAMS['flowline_junction_pix'] * cfg.PARAMS['flowline_dx']
5✔
794
    log.debug('(%s) radius in raster coordinates: %.2f',
5✔
795
              gdir.rgi_id, radius)
796

797
    # OK. Filter and see.
798
    log.debug('(%s) number of heads before radius filter: %d',
5✔
799
              gdir.rgi_id, len(heads))
800
    heads, heads_z = _filter_heads(heads, heads_z, radius, poly_pix)
5✔
801
    log.debug('(%s) number of heads after radius filter: %d',
5✔
802
              gdir.rgi_id, len(heads))
803

804
    return heads
5✔
805

806

807
def _line_extend(uline, dline, dx):
9✔
808
    """Adds a downstream line to a flowline
809

810
    Parameters
811
    ----------
812
    uline: a shapely.geometry.LineString instance
813
    dline: a shapely.geometry.LineString instance
814
    dx: the spacing
815

816
    Returns
817
    -------
818
    (line, line) : two shapely.geometry.LineString instances. The first
819
    contains the newly created (long) line, the second only the interpolated
820
    downstream part (useful for other analyses)
821
    """
822

823
    # First points is easy
824
    points = [shpg.Point(c) for c in uline.coords]
5✔
825

826
    if len(points) == 0:
5✔
827
        # eb flowline
828
        dpoints = [shpg.Point(dline.coords[0])]
3✔
829
        points = [shpg.Point(dline.coords[0])]
3✔
830
    else:
831
        dpoints = []
5✔
832

833
    # Continue as long as line is not finished
834
    while True:
5✔
835
        pref = points[-1]
5✔
836
        pbs = pref.buffer(dx).boundary.intersection(dline)
5✔
837
        if pbs.geom_type in ['LineString', 'GeometryCollection']:
5✔
838
            # Very rare
839
            pbs = pref.buffer(dx+1e-12).boundary.intersection(dline)
×
840
        if pbs.geom_type == 'Point':
5✔
841
            pbs = [pbs]
5✔
842

843
        try:
5✔
844
            # Shapely v2 compat
845
            pbs = pbs.geoms
5✔
846
        except AttributeError:
5✔
847
            pass
5✔
848

849
        # Out of the point(s) that we get, take the one farthest from the top
850
        refdis = dline.project(pref)
5✔
851
        tdis = np.array([dline.project(pb) for pb in pbs])
5✔
852
        p = np.where(tdis > refdis)[0]
5✔
853
        if len(p) == 0:
5✔
854
            break
5✔
855
        points.append(pbs[int(p[0])])
5✔
856
        dpoints.append(pbs[int(p[0])])
5✔
857

858
    return shpg.LineString(points), shpg.LineString(dpoints)
5✔
859

860

861
@entity_task(log, writes=['centerlines', 'gridded_data'])
9✔
862
def compute_centerlines(gdir, heads=None):
9✔
863
    """Compute the centerlines following Kienholz et al., (2014).
864

865
    They are then sorted according to the modified Strahler number:
866
    http://en.wikipedia.org/wiki/Strahler_number
867

868
    This function does not initialize a :py:class:`oggm.Centerline` but
869
    calculates routes along the topography and makes a
870
    :py:class:`shapely.Linestring` object from them.
871

872
    Parameters
873
    ----------
874
    gdir : :py:class:`oggm.GlacierDirectory`
875
        where to write the data
876
    heads : list, optional
877
        list of shapely.geometry.Points to use as line heads (default is to
878
        compute them like Kienholz did)
879
    """
880

881
    # Params
882
    single_fl = not cfg.PARAMS['use_multiple_flowlines']
5✔
883
    do_filter_slope = cfg.PARAMS['filter_min_slope']
5✔
884
    min_slope = 'min_slope_ice_caps' if gdir.is_icecap else 'min_slope'
5✔
885
    min_slope = np.deg2rad(cfg.PARAMS[min_slope])
5✔
886

887
    # Force single flowline for ice caps
888
    if gdir.is_icecap:
5✔
889
        single_fl = True
1✔
890

891
    if 'force_one_flowline' in cfg.PARAMS:
5✔
892
        raise InvalidParamsError('`force_one_flowline` is deprecated')
×
893

894
    # open
895
    geom = gdir.read_pickle('geometries')
5✔
896
    grids_file = gdir.get_filepath('gridded_data')
5✔
897
    with utils.ncDataset(grids_file) as nc:
5✔
898
        # Variables
899
        glacier_mask = nc.variables['glacier_mask'][:]
5✔
900
        glacier_ext = nc.variables['glacier_ext'][:]
5✔
901
        topo = nc.variables['topo_smoothed'][:]
5✔
902
    poly_pix = geom['polygon_pix']
5✔
903

904
    # Find for local maximas on the outline
905
    x, y = tuple2int(poly_pix.exterior.xy)
5✔
906
    ext_yx = tuple(reversed(poly_pix.exterior.xy))
5✔
907
    zoutline = topo[y[:-1], x[:-1]]  # last point is first point
5✔
908

909
    # For diagnostics
910
    is_first_call = False
5✔
911
    if heads is None:
5✔
912
        # This is the default for when no filter is yet applied
913
        is_first_call = True
5✔
914
        heads = _get_centerlines_heads(gdir, ext_yx, zoutline, single_fl,
5✔
915
                                       glacier_mask, topo, geom, poly_pix)
916

917
    # Cost array
918
    costgrid = _make_costgrid(glacier_mask, glacier_ext, topo)
5✔
919

920
    # Terminus
921
    t_coord = _get_terminus_coord(gdir, ext_yx, zoutline)
5✔
922

923
    # Compute the routes
924
    lines = []
5✔
925
    for h in heads:
5✔
926
        h_coord = np.asarray(h.xy)[::-1].astype(np.int64)
5✔
927
        indices, _ = route_through_array(costgrid, h_coord, t_coord)
5✔
928
        lines.append(shpg.LineString(np.array(indices)[:, [1, 0]]))
5✔
929
    log.debug('(%s) computed the routes', gdir.rgi_id)
5✔
930

931
    # Filter the shortest lines out
932
    dx_cls = cfg.PARAMS['flowline_dx']
5✔
933
    radius = cfg.PARAMS['flowline_junction_pix'] * dx_cls
5✔
934
    radius += 6 * dx_cls
5✔
935
    olines, oheads = _filter_lines(lines, heads, cfg.PARAMS['kbuffer'], radius)
5✔
936
    log.debug('(%s) number of heads after lines filter: %d',
5✔
937
              gdir.rgi_id, len(olines))
938

939
    # Filter the lines which are going up instead of down
940
    if do_filter_slope:
5✔
941
        olines, oheads = _filter_lines_slope(olines, oheads, topo,
5✔
942
                                             gdir, min_slope)
943
        log.debug('(%s) number of heads after slope filter: %d',
5✔
944
                  gdir.rgi_id, len(olines))
945

946
    # And rejoin the cut tails
947
    olines = _join_lines(olines, oheads)
5✔
948

949
    # Adds the line level
950
    for cl in olines:
5✔
951
        cl.order = line_order(cl)
5✔
952

953
    # And sort them per order !!! several downstream tasks  rely on this
954
    cls = []
5✔
955
    for i in np.argsort([cl.order for cl in olines]):
5✔
956
        cls.append(olines[i])
5✔
957

958
    # Final check
959
    if len(cls) == 0:
5✔
960
        raise GeometryError('({}) no valid centerline could be '
×
961
                            'found!'.format(gdir.rgi_id))
962

963
    # Write the data
964
    gdir.write_pickle(cls, 'centerlines')
5✔
965

966
    if is_first_call:
5✔
967
        # For diagnostics of filtered centerlines
968
        gdir.add_to_diagnostics('n_orig_centerlines', len(cls))
5✔
969

970

971
@entity_task(log, writes=['downstream_line'])
9✔
972
def compute_downstream_line(gdir):
9✔
973
    """Computes the Flowline along the unglaciated downstream topography
974

975
    The idea is simple: starting from the glacier tail, compute all the routes
976
    to all local minima found at the domain edge. The cheapest is "The One".
977

978
    The rest of the job (merging centerlines + downstream into
979
    one single glacier is realized by
980
    :py:func:`~oggm.tasks.init_present_time_glacier`).
981

982
    Parameters
983
    ----------
984
    gdir : :py:class:`oggm.GlacierDirectory`
985
        where to write the data
986
    """
987

988
    # For tidewater glaciers no need for all this
989
    if gdir.is_tidewater:
5✔
990
        return
3✔
991

992
    with utils.ncDataset(gdir.get_filepath('gridded_data')) as nc:
5✔
993
        topo = nc.variables['topo_smoothed'][:]
5✔
994
        glacier_ext = nc.variables['glacier_ext'][:] == 1
5✔
995

996
    # Look for the starting points
997
    try:
5✔
998
        # Normal OGGM flowlines
999
        p = gdir.read_pickle('centerlines')[-1].tail
5✔
1000
        head = (int(p.y), int(p.x))
5✔
1001
    except FileNotFoundError:
3✔
1002
        # Squeezes lines
1003
        p = np.where((topo[glacier_ext].min() == topo) & glacier_ext)
3✔
1004
        head = (p[0][0], p[1][0])
3✔
1005

1006
    # Make going up very costy
1007
    topo = topo**8
5✔
1008

1009
    # We add an artificial cost as distance from the glacier
1010
    # This should have to much influence on mountain glaciers but helps for
1011
    # tidewater-candidates
1012
    topo = topo + distance_transform_edt(1 - glacier_ext)
5✔
1013

1014
    # Variables we gonna need: the outer side of the domain
1015
    xmesh, ymesh = np.meshgrid(np.arange(0, gdir.grid.nx, 1, dtype=np.int64),
5✔
1016
                               np.arange(0, gdir.grid.ny, 1, dtype=np.int64))
1017
    _h = [topo[:, 0], topo[0, :], topo[:, -1], topo[-1, :]]
5✔
1018
    _x = [xmesh[:, 0], xmesh[0, :], xmesh[:, -1], xmesh[-1, :]]
5✔
1019
    _y = [ymesh[:, 0], ymesh[0, :], ymesh[:, -1], ymesh[-1, :]]
5✔
1020

1021
    # Find the way out of the domain
1022
    min_cost = np.Inf
5✔
1023
    min_len = np.Inf
5✔
1024
    line = None
5✔
1025
    for h, x, y in zip(_h, _x, _y):
5✔
1026
        ids = scipy.signal.argrelmin(h, order=10, mode='wrap')
5✔
1027
        if np.all(h == 0) or len(ids[0]) == 0:
5✔
1028
            # Test every fifth (we don't really care)
1029
            ids = [np.arange(0, len(h), 5)]
×
1030
        for i in ids[0]:
5✔
1031
            lids, cost = route_through_array(topo, head, (y[i], x[i]))
5✔
1032
            if ((cost < min_cost) or
5✔
1033
                    ((cost <= min_cost) and (len(lids) < min_len))):
1034
                min_cost = cost
5✔
1035
                min_len = len(lids)
5✔
1036
                line = shpg.LineString(np.array(lids)[:, [1, 0]])
5✔
1037
    if line is None:
5✔
1038
        raise GeometryError('Downstream line not found')
×
1039

1040
    cl = gdir.read_pickle('inversion_flowlines')[-1]
5✔
1041
    if cl.line is not None:
5✔
1042
        # normal OGGM lines
1043
        lline, dline = _line_extend(cl.line, line, cl.dx)
5✔
1044
        out = dict(full_line=lline, downstream_line=dline)
5✔
1045
    else:
1046
        # Eb flowlines - we trick
1047
        _, dline = _line_extend(shpg.LineString(), line, cl.dx)
3✔
1048
        out = dict(full_line=None, downstream_line=dline)
3✔
1049

1050
    gdir.write_pickle(out, 'downstream_line')
5✔
1051

1052

1053
def _approx_parabola(x, y, y0=0):
9✔
1054
    """Fit a parabola to the equation y = a x**2 + y0
1055

1056
    Parameters
1057
    ----------
1058
    x : array
1059
       the x axis variabls
1060
    y : array
1061
       the dependent variable
1062
    y0 : float, optional
1063
       the intercept
1064

1065
    Returns
1066
    -------
1067
    [a, 0, y0]
1068
    """
1069
    # y=ax**2+y0
1070
    x, y = np.array(x), np.array(y)
4✔
1071
    a = np.sum(x ** 2 * (y - y0)) / np.sum(x ** 4)
4✔
1072
    return np.array([a, 0, y0])
4✔
1073

1074

1075
def _parabola_error(x, y, f):
9✔
1076
    # f is an array represents polynom
1077
    x, y = np.array(x), np.array(y)
4✔
1078
    with np.errstate(divide='ignore', invalid='ignore'):
4✔
1079
        out = sum(abs((np.polyval(f, x) - y) / y)) / len(x)
4✔
1080
    return out
4✔
1081

1082

1083
class HashablePoint(shpg.Point):
9✔
1084
    def __hash__(self):
9✔
1085
        return hash(tuple((self.x, self.y)))
×
1086

1087

1088
def _parabolic_bed_from_topo(gdir, idl, interpolator):
9✔
1089
    """this returns the parabolic bedshape for all points on idl"""
1090

1091
    # Volume area scaling formula for the probable ice thickness
1092
    h_mean = 0.034 * gdir.rgi_area_km2**0.375 * 1000
5✔
1093
    gnx, gny = gdir.grid.nx, gdir.grid.ny
5✔
1094

1095
    # Far Factor
1096
    r = 40
5✔
1097
    # number of points
1098
    cs_n = 20
5✔
1099

1100
    # normals
1101
    ns = [i[0] for i in idl.normals]
5✔
1102
    cs = []
5✔
1103
    donot_compute = []
5✔
1104

1105
    for pcoords, n in zip(idl.line.coords, ns):
5✔
1106
        xi, yi = pcoords
5✔
1107
        vx, vy = n
5✔
1108
        modul = np.sqrt(vx ** 2 + vy ** 2)
5✔
1109
        ci = []
5✔
1110
        _isborder = False
5✔
1111
        for ro in np.linspace(0, r / 2.0, cs_n):
5✔
1112
            t = ro / modul
5✔
1113
            cp1 = HashablePoint(xi + t * vx, yi + t * vy)
5✔
1114
            cp2 = HashablePoint(xi - t * vx, yi - t * vy)
5✔
1115

1116
            # check if out of the frame
1117
            if not (0 < cp2.y < gny - 1) or \
5✔
1118
                    not (0 < cp2.x < gnx - 1) or \
1119
                    not (0 < cp1.y < gny - 1) or \
1120
                    not (0 < cp1.x < gnx - 1):
1121
                _isborder = True
5✔
1122

1123
            ci.append((cp1, ro))
5✔
1124
            ci.append((cp2, -ro))
5✔
1125

1126
        ci = list(set(ci))
5✔
1127
        cs.append(ci)
5✔
1128
        donot_compute.append(_isborder)
5✔
1129

1130
    bed = []
5✔
1131
    terrain_heights = []
5✔
1132
    for ic, (cc, dontcomp) in enumerate(zip(cs, donot_compute)):
5✔
1133

1134
        if dontcomp:
5✔
1135
            bed.append(np.NaN)
5✔
1136
            terrain_heights.append(np.NaN)
5✔
1137
            continue
5✔
1138

1139
        z = []
4✔
1140
        ro = []
4✔
1141
        for i in cc:
4✔
1142
            z.append(interpolator((i[0].y, i[0].x)))
4✔
1143
            ro.append(i[1])
4✔
1144
        aso = np.argsort(ro)
4✔
1145
        ro, z = np.array(ro)[aso], np.array(z)[aso]
4✔
1146

1147
        # find top of parabola
1148
        roHead = ro[np.argmin(z)]
4✔
1149
        zero = np.argmin(z)  # it is index of roHead/zHead
4✔
1150
        zHead = np.amin(z)
4✔
1151

1152
        dsts = abs(h_mean + zHead - z)
4✔
1153

1154
        # find local minima in set of distances
1155
        extr = scipy.signal.argrelextrema(dsts, np.less, mode='wrap')
4✔
1156
        if len(extr[0]) == 0:
4✔
1157
            bed.append(np.NaN)
×
1158
            terrain_heights.append(np.NaN)
×
1159
            continue
×
1160

1161
        # from local minima find that with the minimum |x|
1162
        idx = extr[0][np.argmin(abs(ro[extr]))]
4✔
1163

1164
        # x<0 => x=0
1165
        # (|x|+x)/2
1166
        roN = ro[int((abs(zero - abs(zero - idx)) + zero - abs(
4✔
1167
            zero - idx)) / 2):zero + abs(zero - idx) + 1]
1168
        zN = z[int((abs(zero - abs(zero - idx)) + zero - abs(
4✔
1169
            zero - idx)) / 2):zero + abs(zero - idx) + 1]
1170
        roNx = roN - roHead
4✔
1171
        # zN=zN-zHead#
1172

1173
        p = _approx_parabola(roNx, zN, y0=zHead)
4✔
1174

1175
        # define terrain height as the maximum height difference of points used
1176
        # for the parabolic fitting and the bottom height
1177
        terrain_heights.append(float(np.max(zN - zHead)))
4✔
1178

1179
        # shift parabola to the ds-line
1180
        p2 = np.copy(p)
4✔
1181
        p2[2] = z[ro == 0]
4✔
1182

1183
        err = _parabola_error(roN, zN, p2) * 100
4✔
1184

1185
        # The original implementation of @anton-ub stored all three parabola
1186
        # params. We just keep the one important here for now
1187
        if err < 1.5:
4✔
1188
            bed.append(p2[0])
4✔
1189
        else:
1190
            bed.append(np.NaN)
4✔
1191

1192
    terrain_heights = np.asarray(terrain_heights)
5✔
1193
    assert len(terrain_heights) == idl.nx, 'len(terrain_heights) == idl.nx'
5✔
1194

1195
    bed = np.asarray(bed)
5✔
1196
    assert len(bed) == idl.nx, 'len(bed) == idl.nx'
5✔
1197
    pvalid = np.sum(np.isfinite(bed)) / len(bed) * 100
5✔
1198
    log.debug('(%s) percentage of valid parabolas in downstream: %d',
5✔
1199
              gdir.rgi_id, int(pvalid))
1200

1201
    # Scale for dx (we worked in grid coords but need meters)
1202
    bed = bed / gdir.grid.dx**2
5✔
1203

1204
    # interpolation, filling the gaps
1205
    default = cfg.PARAMS['default_parabolic_bedshape']
5✔
1206
    bed_int = interp_nans(bed, default=default)
5✔
1207
    default = 100.  # assume a default terrain height of 100 m if all NaN
5✔
1208
    terrain_heights = interp_nans(terrain_heights, default=default)
5✔
1209

1210
    # We forbid super small shapes (important! This can lead to huge volumes)
1211
    # Sometimes the parabola fits in flat areas are very good, implying very
1212
    # flat parabolas.
1213
    bed_int = utils.clip_min(bed_int, cfg.PARAMS['downstream_min_shape'])
5✔
1214

1215
    # Smoothing
1216
    bed_ma = pd.Series(bed_int)
5✔
1217
    bed_ma = bed_ma.rolling(window=5, center=True, min_periods=1).mean()
5✔
1218
    return bed_ma.values, terrain_heights
5✔
1219

1220

1221
def _trapezoidal_bottom_width_from_terrain_cross_section_area(
9✔
1222
        terrain_heights, Ps, lambdas, w0_min):
1223
    """This function calculates a bottom width for a trapezoidal downstream
1224
    line in a way that the terrain cross section area is preserved. The area to
1225
    preserve is defined by the fitted parabolic shape and the terrain height.
1226

1227
    Parabolic formulas involved (Ap = area, h = terrain_height,
1228
    w = surface width, Ps = bed-shape parameter):
1229
        Ap = 2 / 3 * h * w
1230
        Ps = 4 * h / w^2
1231

1232
    Trapezoidal formulas involved (At = area, h = terrain_height,
1233
    w = surface width, w0 = bottom width, lambda = defines angle of wall):
1234
        At = (w + w0) * h / 2
1235
        w = w0 + lambda * h
1236

1237
    Putting all formulas together and setting Ap = At we get:
1238
        w0 = 4 / 3 * sqrt(h / Ps) - 1 / 2 * lambda * h
1239
    """
1240

1241
    w0s = utils.clip_min(4 / 3 * np.sqrt(terrain_heights / Ps) -
5✔
1242
                         1 / 2 * lambdas * terrain_heights,
1243
                         w0_min)
1244

1245
    return w0s
5✔
1246

1247

1248
@entity_task(log, writes=['downstream_line'])
9✔
1249
def compute_downstream_bedshape(gdir):
9✔
1250
    """The bedshape obtained by fitting a parabola to the line's normals.
1251
    Further a trapezoidal shape is fitted to match the cross section area of
1252
    the valley. Which downstream shape (parabola or trapezoidal) is used
1253
    by the later call to init_present_day_glacier can be
1254
    selected with cfg.PARAMS['downstream_line_shape'].
1255

1256
    Also computes the downstream's altitude.
1257

1258
    Parameters
1259
    ----------
1260
    gdir : :py:class:`oggm.GlacierDirectory`
1261
        where to write the data
1262
    """
1263

1264
    # For tidewater glaciers no need for all this
1265
    if gdir.is_tidewater:
5✔
1266
        return
3✔
1267

1268
    # We make a flowline out of the downstream for simplicity
1269
    tpl = gdir.read_pickle('inversion_flowlines')[-1]
5✔
1270
    cl = gdir.read_pickle('downstream_line')['downstream_line']
5✔
1271
    cl = Centerline(cl, dx=tpl.dx, map_dx=gdir.grid.dx)
5✔
1272

1273
    # Topography
1274
    with utils.ncDataset(gdir.get_filepath('gridded_data')) as nc:
5✔
1275
        topo = nc.variables['topo_smoothed'][:]
5✔
1276
        x = nc.variables['x'][:]
5✔
1277
        y = nc.variables['y'][:]
5✔
1278
    xy = (np.arange(0, len(y)-0.1, 1), np.arange(0, len(x)-0.1, 1))
5✔
1279
    interpolator = RegularGridInterpolator(xy, topo.astype(np.float64))
5✔
1280

1281
    bs, terrain_heights = _parabolic_bed_from_topo(gdir, cl, interpolator)
5✔
1282
    assert len(bs) == cl.nx, 'len(bs) == cl.nx'
5✔
1283
    assert np.all(np.isfinite(bs)), 'np.all(np.isfinite(bs))'
5✔
1284

1285
    # Interpolate heights for later
1286
    xx, yy = cl.line.xy
5✔
1287
    hgts = interpolator((yy, xx))
5✔
1288
    assert len(hgts) >= 5, 'len(hgts) >= 5'
5✔
1289

1290
    # If smoothing, this is the moment
1291
    hgts = gaussian_filter1d(hgts, cfg.PARAMS['flowline_height_smooth'])
5✔
1292

1293
    # calculate bottom width of trapezoidal shapes
1294
    lambdas = np.ones(len(bs)) * cfg.PARAMS['trapezoid_lambdas']
5✔
1295
    w0_min = cfg.PARAMS['trapezoid_min_bottom_width']
5✔
1296
    w0s = _trapezoidal_bottom_width_from_terrain_cross_section_area(
5✔
1297
        terrain_heights, bs, lambdas, w0_min)
1298
    assert len(w0s) == cl.nx, 'len(w0s) == cl.nx'
5✔
1299
    assert np.all(np.isfinite(w0s)), 'np.all(np.isfinite(w0s))'
5✔
1300
    assert np.all(w0s >= w0_min), 'np.all(w0s >= w0_min)'
5✔
1301

1302
    # write output
1303
    out = gdir.read_pickle('downstream_line')
5✔
1304
    out['bedshapes'] = bs
5✔
1305
    out['surface_h'] = hgts
5✔
1306
    out['w0s'] = w0s
5✔
1307
    gdir.write_pickle(out, 'downstream_line')
5✔
1308

1309

1310
def _mask_to_polygon(mask, gdir=None):
9✔
1311
    """Converts a mask to a single polygon.
1312

1313
    The mask should be a single entity with nunataks: I didn't test for more
1314
    than one "blob".
1315

1316
    Parameters
1317
    ----------
1318
    mask: 2d array with ones and zeros
1319
        the mask to convert
1320
    gdir: GlacierDirectory
1321
        for logging
1322

1323
    Returns
1324
    -------
1325
    (poly, poly_no_nunataks) Shapely polygons
1326
    """
1327

1328
    regions, nregions = label(mask, structure=LABEL_STRUCT)
5✔
1329
    if nregions > 1:
5✔
1330
        rid = ''
2✔
1331
        if gdir is not None:
2✔
1332
            rid = gdir.rgi_id
1✔
1333
        log.debug('(%s) we had to cut a blob from the catchment', rid)
2✔
1334
        # Check the size of those
1335
        region_sizes = [np.sum(regions == r) for r in np.arange(1, nregions+1)]
2✔
1336
        am = np.argmax(region_sizes)
2✔
1337
        # Check not a strange glacier
1338
        sr = region_sizes.pop(am)
2✔
1339
        for ss in region_sizes:
2✔
1340
            if (ss / sr) > 0.2:
2✔
1341
                log.info('(%s) this blob was unusually large', rid)
×
1342
        mask[:] = 0
2✔
1343
        mask[np.where(regions == (am+1))] = 1
2✔
1344

1345
    nlist = measure.find_contours(mask, 0.5)
5✔
1346
    # First is the exterior, the rest are nunataks
1347
    e_line = shpg.LinearRing(nlist[0][:, ::-1])
5✔
1348
    i_lines = [shpg.LinearRing(ipoly[:, ::-1]) for ipoly in nlist[1:]]
5✔
1349

1350
    poly = shpg.Polygon(e_line, i_lines).buffer(0)
5✔
1351
    if not poly.is_valid:
5✔
1352
        raise GeometryError('Mask to polygon conversion error.')
×
1353
    poly_no = shpg.Polygon(e_line).buffer(0)
5✔
1354
    if not poly_no.is_valid:
5✔
1355
        raise GeometryError('Mask to polygon conversion error.')
×
1356
    return poly, poly_no
5✔
1357

1358

1359
def _point_width(normals, point, centerline, poly, poly_no_nunataks):
9✔
1360
    """ Compute the geometrical width on a specific point.
1361

1362
    Called by catchment_width_geom.
1363

1364
    Parameters
1365
    ----------
1366
    normals: normals of the current point, before, and after
1367
    point: the centerline's point
1368
    centerline: Centerline object
1369
    poly, poly_no_nuntaks: subcatchment polygons
1370

1371
    Returns
1372
    -------
1373
    (width, MultiLineString)
1374
    """
1375

1376
    # How far should the normal vector reach? (make it large)
1377
    far_factor = 150.
5✔
1378

1379
    normal = shpg.LineString([shpg.Point(point + normals[0] * far_factor),
5✔
1380
                              shpg.Point(point + normals[1] * far_factor)])
1381

1382
    # First use the external boundaries only
1383
    line = normal.intersection(poly_no_nunataks)
5✔
1384
    if line.geom_type == 'LineString':
5✔
1385
        pass  # Nothing to be done
5✔
1386
    elif line.geom_type in ['MultiLineString', 'GeometryCollection']:
5✔
1387
        # Take the one that contains the centerline
1388
        oline = None
5✔
1389
        for l in line.geoms:
5✔
1390
            if l.geom_type != 'LineString':
5✔
1391
                continue
×
1392
            if l.intersects(centerline.line):
5✔
1393
                oline = l
5✔
1394
                break
5✔
1395
        if oline is None:
5✔
1396
            return np.NaN, shpg.MultiLineString()
4✔
1397
        line = oline
5✔
1398
    else:
1399
        extext = 'Geometry collection not expected: {}'.format(line.geom_type)
×
1400
        raise InvalidGeometryError(extext)
×
1401

1402
    # Then take the nunataks into account
1403
    # Make sure we are always returning a MultiLineString for later
1404
    line = line.intersection(poly)
5✔
1405
    if line.geom_type == 'LineString':
5✔
1406
        try:
5✔
1407
            line = shpg.MultiLineString([line])
5✔
1408
        except shapely.errors.EmptyPartError:
×
1409
            return np.NaN, shpg.MultiLineString()
×
1410
    elif line.geom_type == 'MultiLineString':
5✔
1411
        pass  # nothing to be done
5✔
1412
    elif line.geom_type == 'GeometryCollection':
×
1413
        oline = []
×
1414
        for l in line:
×
1415
            if l.geom_type != 'LineString':
×
1416
                continue
×
1417
            oline.append(l)
×
1418
        if len(oline) == 0:
×
1419
            return np.NaN, shpg.MultiLineString()
×
1420
        line = shpg.MultiLineString(oline)
×
1421
    else:
1422
        extext = 'Geometry collection not expected: {}'.format(line.geom_type)
×
1423
        raise InvalidGeometryError(extext)
×
1424

1425
    assert line.geom_type == 'MultiLineString', 'Should be MultiLineString'
5✔
1426
    width = np.sum([l.length for l in line.geoms])
5✔
1427

1428
    return width, line
5✔
1429

1430

1431
def _filter_small_slopes(hgt, dx, min_slope):
9✔
1432
    """Masks out slopes with NaN until the slope if all valid points is at
1433
    least min_slope (in radians).
1434
    """
1435

1436
    slope = np.arctan(-np.gradient(hgt, dx))  # beware the minus sign
5✔
1437
    # slope at the end always OK
1438
    slope[-1] = min_slope
5✔
1439

1440
    # Find the locs where it doesn't work and expand till we got everything
1441
    slope_mask = np.where(slope >= min_slope, slope, np.NaN)
5✔
1442
    r, nr = label(~np.isfinite(slope_mask))
5✔
1443
    for objs in find_objects(r):
5✔
1444
        obj = objs[0]
4✔
1445
        i = 0
4✔
1446
        while True:
4✔
1447
            i += 1
4✔
1448
            i0 = objs[0].start-i
4✔
1449
            if i0 < 0:
4✔
1450
                break
×
1451
            ngap = obj.stop - i0 - 1
4✔
1452
            nhgt = hgt[[i0, obj.stop]]
4✔
1453
            current_slope = np.arctan(-np.gradient(nhgt, ngap * dx))
4✔
1454
            if i0 <= 0 or current_slope[0] >= min_slope:
4✔
1455
                break
4✔
1456
        slope_mask[i0:obj.stop] = np.NaN
4✔
1457
    out = hgt.copy()
5✔
1458
    out[~np.isfinite(slope_mask)] = np.NaN
5✔
1459
    return out
5✔
1460

1461

1462
def _filter_for_altitude_range(widths, wlines, topo):
9✔
1463
    """Some width lines have unrealistic length and go over the whole
1464
    glacier. Filter them out."""
1465

1466
    # altitude range threshold (if range over the line > threshold, filter it)
1467
    alt_range_th = cfg.PARAMS['width_alt_range_thres']
5✔
1468

1469
    while True:
5✔
1470
        out_width = widths.copy()
5✔
1471
        for i, (wi, wl) in enumerate(zip(widths, wlines)):
5✔
1472
            if np.isnan(wi):
5✔
1473
                continue
4✔
1474
            xc = []
5✔
1475
            yc = []
5✔
1476
            for dwl in wl.geoms:
5✔
1477
                # we interpolate at high res and take the int coords
1478
                dwl = shpg.LineString([dwl.interpolate(x, normalized=True)
5✔
1479
                                       for x in np.linspace(0., 1., num=100)])
1480
                grouped = groupby(map(tuple, np.rint(dwl.coords)))
5✔
1481
                dwl = np.array([x[0] for x in grouped], dtype=int)
5✔
1482
                xc.extend(dwl[:, 0])
5✔
1483
                yc.extend(dwl[:, 1])
5✔
1484

1485
            altrange = topo[yc, xc]
5✔
1486
            if len(np.where(np.isfinite(altrange))[0]) != 0:
5✔
1487
                if (np.nanmax(altrange) - np.nanmin(altrange)) > alt_range_th:
5✔
1488
                    out_width[i] = np.NaN
5✔
1489

1490
        valid = np.where(np.isfinite(out_width))
5✔
1491
        if len(valid[0]) > 0:
5✔
1492
            break
5✔
1493
        else:
1494
            alt_range_th += 20
×
1495
            log.debug('Set altitude threshold to {}'.format(alt_range_th))
×
1496
        if alt_range_th > 2000:
×
1497
            raise GeometryError('Problem by altitude filter.')
×
1498

1499
    return out_width
5✔
1500

1501

1502
def _filter_grouplen(arr, minsize=3):
9✔
1503
    """Filter out the groups of grid points smaller than minsize
1504

1505
    Parameters
1506
    ----------
1507
    arr : the array to filter (should be False and Trues)
1508
    minsize : the minimum size of the group
1509

1510
    Returns
1511
    -------
1512
    the array, with small groups removed
1513
    """
1514

1515
    # Do it with trues
1516
    r, nr = label(arr)
5✔
1517
    nr = [i+1 for i, o in enumerate(find_objects(r)) if (len(r[o]) >= minsize)]
5✔
1518
    arr = np.asarray([ri in nr for ri in r])
5✔
1519

1520
    # and with Falses
1521
    r, nr = label(~ arr)
5✔
1522
    nr = [i+1 for i, o in enumerate(find_objects(r)) if (len(r[o]) >= minsize)]
5✔
1523
    arr = ~ np.asarray([ri in nr for ri in r])
5✔
1524

1525
    return arr
5✔
1526

1527

1528
def _width_change_factor(widths):
9✔
1529
    fac = widths[:-1] / widths[1:]
×
1530
    return fac
×
1531

1532

1533
@entity_task(log, writes=['geometries'])
9✔
1534
def catchment_area(gdir):
9✔
1535
    """Compute the catchment areas of each tributary line.
1536

1537
    The idea is to compute the route of lowest cost for any point on the
1538
    glacier to rejoin a centerline. These routes are then put together if
1539
    they belong to the same centerline, thus creating "catchment areas" for
1540
    each centerline.
1541

1542
    Parameters
1543
    ----------
1544
    gdir : :py:class:`oggm.GlacierDirectory`
1545
        where to write the data
1546
    """
1547

1548
    # Variables
1549
    cls = gdir.read_pickle('centerlines')
5✔
1550
    geom = gdir.read_pickle('geometries')
5✔
1551
    glacier_pix = geom['polygon_pix']
5✔
1552
    fpath = gdir.get_filepath('gridded_data')
5✔
1553
    with utils.ncDataset(fpath) as nc:
5✔
1554
        glacier_mask = nc.variables['glacier_mask'][:]
5✔
1555
        glacier_ext = nc.variables['glacier_ext'][:]
5✔
1556
        topo = nc.variables['topo_smoothed'][:]
5✔
1557

1558
    # If we have only one centerline this is going to be easy: take the
1559
    # mask and return
1560
    if len(cls) == 1:
5✔
1561
        cl_catchments = [np.array(np.nonzero(glacier_mask == 1)).T]
2✔
1562
        geom['catchment_indices'] = cl_catchments
2✔
1563
        gdir.write_pickle(geom, 'geometries')
2✔
1564
        return
2✔
1565

1566
    # Cost array
1567
    costgrid = _make_costgrid(glacier_mask, glacier_ext, topo)
5✔
1568

1569
    # Initialise costgrid and the "catching" dict
1570
    cost_factor = 0.  # Make it cheap
5✔
1571
    dic_catch = dict()
5✔
1572
    for i, cl in enumerate(cls):
5✔
1573
        x, y = tuple2int(cl.line.xy)
5✔
1574
        costgrid[y, x] = cost_factor
5✔
1575
        for x, y in [(int(x), int(y)) for x, y in cl.line.coords]:
5✔
1576
            assert (y, x) not in dic_catch
5✔
1577
            dic_catch[(y, x)] = set([(y, x)])
5✔
1578

1579
    # It is much faster to make the array as small as possible. We trick:
1580
    pm = np.nonzero(glacier_mask == 1)
5✔
1581
    ymi, yma = np.min(pm[0])-1, np.max(pm[0])+2
5✔
1582
    xmi, xma = np.min(pm[1])-1, np.max(pm[1])+2
5✔
1583
    costgrid = costgrid[ymi:yma, xmi:xma]
5✔
1584
    mask = glacier_mask[ymi:yma, xmi:xma]
5✔
1585

1586
    # Where did we compute the path already?
1587
    computed = np.where(mask == 1, 0, np.nan)
5✔
1588

1589
    # Coords of Terminus (converted)
1590
    endcoords = np.array(cls[0].tail.coords[0])[::-1].astype(np.int64)
5✔
1591
    endcoords -= [ymi, xmi]
5✔
1592

1593
    # Start with all the paths at the boundaries, they are more likely
1594
    # to cover much of the glacier
1595
    for headx, heady in tuple2int(glacier_pix.exterior.coords):
5✔
1596
        headcoords = np.array([heady-ymi, headx-xmi])  # convert
5✔
1597
        indices, _ = route_through_array(costgrid, headcoords, endcoords)
5✔
1598
        inds = np.array(indices).T
5✔
1599
        computed[inds[0], inds[1]] = 1
5✔
1600
        set_dump = set([])
5✔
1601
        for y, x in indices:
5✔
1602
            y, x = y+ymi, x+xmi  # back to original
5✔
1603
            set_dump.add((y, x))
5✔
1604
            if (y, x) in dic_catch:
5✔
1605
                dic_catch[(y, x)] = dic_catch[(y, x)].union(set_dump)
5✔
1606
                break
5✔
1607

1608
    # Repeat for the not yet computed pixels
1609
    while True:
5✔
1610
        not_computed = np.where(computed == 0)
5✔
1611
        if len(not_computed[0]) == 0:  # All points computed !!
5✔
1612
            break
5✔
1613
        headcoords = np.array([not_computed[0][0], not_computed[1][0]],
5✔
1614
                              dtype=np.int64)
1615
        indices, _ = route_through_array(costgrid, headcoords, endcoords)
5✔
1616
        inds = np.array(indices).T
5✔
1617
        computed[inds[0], inds[1]] = 1
5✔
1618
        set_dump = set([])
5✔
1619
        for y, x in indices:
5✔
1620
            y, x = y+ymi, x+xmi  # back to original
5✔
1621
            set_dump.add((y, x))
5✔
1622
            if (y, x) in dic_catch:
5✔
1623
                dic_catch[(y, x)] = dic_catch[(y, x)].union(set_dump)
5✔
1624
                break
5✔
1625

1626
    # For each centerline, make a set of all points flowing into it
1627
    cl_catchments = []
5✔
1628
    for cl in cls:
5✔
1629
        # Union of all
1630
        cl_catch = set()
5✔
1631
        for x, y in [(int(x), int(y)) for x, y in cl.line.coords]:
5✔
1632
            cl_catch = cl_catch.union(dic_catch[(y, x)])
5✔
1633
        cl_catchments.append(cl_catch)
5✔
1634

1635
    # The higher order centerlines will get the points from the upstream
1636
    # ones too. The idea is to store the points which are unique to each
1637
    # centerline: now, in decreasing line order we remove the indices from
1638
    # the tributaries
1639
    cl_catchments = cl_catchments[::-1]
5✔
1640
    for i, cl in enumerate(cl_catchments):
5✔
1641
        cl_catchments[i] = np.array(list(cl.difference(*cl_catchments[i+1:])))
5✔
1642
    cl_catchments = cl_catchments[::-1]  # put it back in order
5✔
1643

1644
    # Write the data
1645
    geom['catchment_indices'] = cl_catchments
5✔
1646
    gdir.write_pickle(geom, 'geometries')
5✔
1647

1648

1649
@entity_task(log, writes=['flowline_catchments', 'catchments_intersects'])
9✔
1650
def catchment_intersections(gdir):
9✔
1651
    """Computes the intersections between the catchments.
1652

1653
    A glacier usually consists of several flowlines and each flowline has a
1654
    distinct catchment area. This function calculates the intersections between
1655
    these areas.
1656

1657
    Parameters
1658
    ----------
1659
    gdir : :py:class:`oggm.GlacierDirectory`
1660
        where to write the data
1661
    """
1662

1663
    catchment_indices = gdir.read_pickle('geometries')['catchment_indices']
5✔
1664

1665
    # Loop over the lines
1666
    mask = np.zeros((gdir.grid.ny, gdir.grid.nx))
5✔
1667

1668
    gdfc = gpd.GeoDataFrame()
5✔
1669
    for i, ci in enumerate(catchment_indices):
5✔
1670
        # Catchment polygon
1671
        mask[:] = 0
5✔
1672
        mask[tuple(ci.T)] = 1
5✔
1673
        _, poly_no = _mask_to_polygon(mask, gdir=gdir)
5✔
1674
        gdfc.loc[i, 'geometry'] = poly_no
5✔
1675

1676
    gdfi = utils.polygon_intersections(gdfc)
5✔
1677

1678
    # We project them onto the mercator proj before writing. This is a bit
1679
    # inefficient (they'll be projected back later), but it's more sustainable
1680
    try:
5✔
1681
        # salem for geopandas > 0.7
1682
        salem.transform_geopandas(gdfc, from_crs=gdir.grid,
5✔
1683
                                  to_crs=gdir.grid.proj, inplace=True)
1684
        salem.transform_geopandas(gdfi, from_crs=gdir.grid,
5✔
1685
                                  to_crs=gdir.grid.proj, inplace=True)
1686
    except TypeError:
×
1687
        # from_crs not available yet
1688
        if Version(gpd.__version__) >= Version('0.7.0'):
×
1689
            raise ImportError('You have installed geopandas v0.7 or higher. '
1690
                              'Please also update salem for compatibility.')
1691
        gdfc.crs = gdir.grid
×
1692
        gdfi.crs = gdir.grid
×
1693
        salem.transform_geopandas(gdfc, to_crs=gdir.grid.proj, inplace=True)
×
1694
        salem.transform_geopandas(gdfi, to_crs=gdir.grid.proj, inplace=True)
×
1695
    if hasattr(gdfc.crs, 'srs'):
5✔
1696
        # salem uses pyproj
1697
        gdfc.crs = gdfc.crs.srs
5✔
1698
        gdfi.crs = gdfi.crs.srs
5✔
1699
    gdir.write_shapefile(gdfc, 'flowline_catchments')
5✔
1700
    if len(gdfi) > 0:
5✔
1701
        gdir.write_shapefile(gdfi, 'catchments_intersects')
5✔
1702

1703

1704
@entity_task(log, writes=['inversion_flowlines'])
9✔
1705
def initialize_flowlines(gdir):
9✔
1706
    """ Computes more physical Inversion Flowlines from geometrical Centerlines
1707

1708
    This interpolates the centerlines on a regular spacing (i.e. not the
1709
    grid's (i, j) indices. Cuts out the tail of the tributaries to make more
1710
    realistic junctions. Also checks for low and negative slopes and corrects
1711
    them by interpolation.
1712

1713
    Parameters
1714
    ----------
1715
    gdir : :py:class:`oggm.GlacierDirectory`
1716
        where to write the data
1717
    """
1718

1719
    # variables
1720
    cls = gdir.read_pickle('centerlines')
5✔
1721

1722
    # Initialise the flowlines
1723
    dx = cfg.PARAMS['flowline_dx']
5✔
1724
    do_filter = cfg.PARAMS['filter_min_slope']
5✔
1725
    min_slope = np.deg2rad(cfg.PARAMS['min_slope_flowline_filter'])
5✔
1726

1727
    lid = int(cfg.PARAMS['flowline_junction_pix'])
5✔
1728
    fls = []
5✔
1729

1730
    # Topo for heights
1731
    fpath = gdir.get_filepath('gridded_data')
5✔
1732
    with utils.ncDataset(fpath) as nc:
5✔
1733
        topo = nc.variables['topo_smoothed'][:]
5✔
1734

1735
    # Bilinear interpolation
1736
    # Geometries coordinates are in "pixel centered" convention, i.e
1737
    # (0, 0) is also located in the center of the pixel
1738
    xy = (np.arange(0, gdir.grid.ny-0.1, 1),
5✔
1739
          np.arange(0, gdir.grid.nx-0.1, 1))
1740
    interpolator = RegularGridInterpolator(xy, topo.astype(np.float64))
5✔
1741

1742
    # Smooth window
1743
    sw = cfg.PARAMS['flowline_height_smooth']
5✔
1744
    diag_n_bad_slopes = 0
5✔
1745
    diag_n_pix = 0
5✔
1746
    for ic, cl in enumerate(cls):
5✔
1747
        points = line_interpol(cl.line, dx)
5✔
1748

1749
        # For tributaries, remove the tail
1750
        if ic < (len(cls)-1):
5✔
1751
            points = points[0:-lid]
5✔
1752

1753
        new_line = shpg.LineString(points)
5✔
1754

1755
        # Interpolate heights
1756
        xx, yy = new_line.xy
5✔
1757
        hgts = interpolator((yy, xx))
5✔
1758
        if len(hgts) < 5:
5✔
1759
            raise GeometryError('This centerline is too short')
×
1760

1761
        # If smoothing, this is the moment
1762
        hgts = gaussian_filter1d(hgts, sw)
5✔
1763

1764
        # Clip topography to 0 m a.s.l.
1765
        utils.clip_min(hgts, 0, out=hgts)
5✔
1766

1767
        # Last safeguard here
1768
        if ic == (len(cls)-1) and ((np.max(hgts) - np.min(hgts)) < 10):
5✔
1769
            raise RuntimeError('Altitude range of main flowline too small: '
×
1770
                               '{}'.format(np.max(hgts) - np.min(hgts)))
1771

1772
        # Check for min slope issues and correct if needed
1773
        if do_filter:
5✔
1774
            # Correct only where glacier
1775
            hgts = _filter_small_slopes(hgts, dx*gdir.grid.dx, min_slope)
5✔
1776
            isfin = np.isfinite(hgts)
5✔
1777
            if not np.any(isfin):
5✔
1778
                raise GeometryError('This centerline has no positive slopes')
×
1779
            diag_n_bad_slopes += np.sum(~isfin)
5✔
1780
            diag_n_pix += len(isfin)
5✔
1781
            perc_bad = np.sum(~isfin) / len(isfin)
5✔
1782
            if perc_bad > 0.8:
5✔
1783
                log.info('({}) more than {:.0%} of the flowline is cropped '
×
1784
                         'due to negative slopes.'.format(gdir.rgi_id,
1785
                                                          perc_bad))
1786

1787
            sp = np.min(np.where(np.isfinite(hgts))[0])
5✔
1788
            while len(hgts[sp:]) < 5:
5✔
1789
                sp -= 1
×
1790
            hgts = utils.interp_nans(hgts[sp:])
5✔
1791
            if not (np.all(np.isfinite(hgts)) and len(hgts) >= 5):
5✔
1792
                raise GeometryError('Something went wrong in flowline init')
×
1793
            new_line = shpg.LineString(points[sp:])
5✔
1794

1795
        sl = Centerline(new_line, dx=dx, surface_h=hgts,
5✔
1796
                        orig_head=cl.orig_head, rgi_id=gdir.rgi_id,
1797
                        map_dx=gdir.grid.dx)
1798
        sl.order = cl.order
5✔
1799
        fls.append(sl)
5✔
1800

1801
    # All objects are initialized, now we can link them.
1802
    for cl, fl in zip(cls, fls):
5✔
1803
        fl.orig_centerline_id = id(cl)
5✔
1804
        if cl.flows_to is None:
5✔
1805
            continue
5✔
1806
        fl.set_flows_to(fls[cls.index(cl.flows_to)])
5✔
1807

1808
    # Write the data
1809
    gdir.write_pickle(fls, 'inversion_flowlines')
5✔
1810
    gdir.add_to_diagnostics('flowline_type', 'centerlines')
5✔
1811
    if do_filter:
5✔
1812
        out = diag_n_bad_slopes/diag_n_pix
5✔
1813
        gdir.add_to_diagnostics('perc_invalid_flowline', out)
5✔
1814

1815

1816
@entity_task(log, writes=['inversion_flowlines'])
9✔
1817
def catchment_width_geom(gdir):
9✔
1818
    """Compute geometrical catchment widths for each point of the flowlines.
1819

1820
    Updates the 'inversion_flowlines' save file.
1821

1822
    Parameters
1823
    ----------
1824
    gdir : :py:class:`oggm.GlacierDirectory`
1825
        where to write the data
1826
    """
1827

1828
    # variables
1829
    flowlines = gdir.read_pickle('inversion_flowlines')
5✔
1830
    catchment_indices = gdir.read_pickle('geometries')['catchment_indices']
5✔
1831

1832
    # Topography is to filter the unrealistic lines afterwards.
1833
    # I take the non-smoothed topography
1834
    # I remove the boundary pixs because they are likely to be higher
1835
    fpath = gdir.get_filepath('gridded_data')
5✔
1836
    with utils.ncDataset(fpath) as nc:
5✔
1837
        topo = nc.variables['topo'][:]
5✔
1838
        mask_ext = nc.variables['glacier_ext'][:]
5✔
1839
        mask_glacier = nc.variables['glacier_mask'][:]
5✔
1840
    topo[np.where(mask_glacier == 0)] = np.NaN
5✔
1841
    topo[np.where(mask_ext == 1)] = np.NaN
5✔
1842

1843
    # Intersects between catchments/glaciers
1844
    gdfi = gpd.GeoDataFrame(columns=['geometry'])
5✔
1845
    if gdir.has_file('catchments_intersects'):
5✔
1846
        # read and transform to grid
1847
        gdf = gdir.read_shapefile('catchments_intersects')
5✔
1848
        salem.transform_geopandas(gdf, to_crs=gdir.grid, inplace=True)
5✔
1849
        gdfi = pd.concat([gdfi, gdf[['geometry']]])
5✔
1850
    if gdir.has_file('intersects'):
5✔
1851
        # read and transform to grid
1852
        gdf = gdir.read_shapefile('intersects')
4✔
1853
        salem.transform_geopandas(gdf, to_crs=gdir.grid, inplace=True)
4✔
1854
        gdfi = pd.concat([gdfi, gdf[['geometry']]])
4✔
1855

1856
    # apply a buffer to be sure we get the intersects right. Be generous
1857
    gdfi = gdfi.buffer(1.5)
5✔
1858

1859
    # Filter parameters
1860
    # Number of pixels to arbitrarily remove at junctions
1861
    jpix = int(cfg.PARAMS['flowline_junction_pix'])
5✔
1862

1863
    # Loop over the lines
1864
    mask = np.zeros((gdir.grid.ny, gdir.grid.nx))
5✔
1865
    for fl, ci in zip(flowlines, catchment_indices):
5✔
1866

1867
        n = len(fl.dis_on_line)
5✔
1868

1869
        widths = np.zeros(n)
5✔
1870
        wlines = []
5✔
1871

1872
        # Catchment polygon
1873
        mask[:] = 0
5✔
1874
        mask[tuple(ci.T)] = 1
5✔
1875
        poly, poly_no = _mask_to_polygon(mask, gdir=gdir)
5✔
1876

1877
        # First guess widths
1878
        for i, (normal, pcoord) in enumerate(zip(fl.normals, fl.line.coords)):
5✔
1879
            width, wline = _point_width(normal, pcoord, fl, poly, poly_no)
5✔
1880
            widths[i] = width
5✔
1881
            wlines.append(wline)
5✔
1882

1883
        valid = np.where(np.isfinite(widths))
5✔
1884
        if len(valid[0]) == 0:
5✔
1885
            errmsg = '({}) first guess widths went wrong.'.format(gdir.rgi_id)
×
1886
            raise GeometryError(errmsg)
×
1887

1888
        # Ok now the entire centerline is computed.
1889
        # I take all these widths for geometrically valid, and see if they
1890
        # intersect with our buffered catchment/glacier intersections
1891
        is_rectangular = []
5✔
1892
        for wg in wlines:
5✔
1893
            with warnings.catch_warnings():
5✔
1894
                warnings.simplefilter('ignore', category=RuntimeWarning)
5✔
1895
                inter = gdfi.intersects(wg)
5✔
1896
            is_rectangular.append(np.any(inter))
5✔
1897
        is_rectangular = _filter_grouplen(is_rectangular, minsize=5)
5✔
1898

1899
        # we filter the lines which have a large altitude range
1900
        fil_widths = _filter_for_altitude_range(widths, wlines, topo)
5✔
1901

1902
        # Filter +- widths at junction points
1903
        for fid in fl.inflow_indices:
5✔
1904
            i0 = int(utils.clip_scalar(fid-jpix, jpix/2, n-jpix/2))
5✔
1905
            i1 = int(utils.clip_scalar(fid+jpix+1, jpix/2, n-jpix/2))
5✔
1906
            fil_widths[i0:i1] = np.NaN
5✔
1907

1908
        valid = np.where(np.isfinite(fil_widths))
5✔
1909
        if len(valid[0]) == 0:
5✔
1910
            # This happens very rarely. Just pick the middle and
1911
            # the correction task should do the rest
1912
            log.info('({}) width filtering too strong.'.format(gdir.rgi_id))
×
1913
            fil_widths = np.ones(n) * widths[int(len(widths) / 2.)]
×
1914

1915
        # Special treatment for tidewater glaciers
1916
        if gdir.is_tidewater and fl.flows_to is None:
5✔
1917
            is_rectangular[-5:] = True
3✔
1918

1919
        # Write it in the objects attributes
1920
        if len(fil_widths) != n:
5✔
1921
            raise GeometryError('Something went wrong')
×
1922
        fl.widths = fil_widths
5✔
1923
        fl.geometrical_widths = wlines
5✔
1924
        fl.is_rectangular = is_rectangular
5✔
1925

1926
    # Overwrite pickle
1927
    gdir.write_pickle(flowlines, 'inversion_flowlines')
5✔
1928

1929

1930
@entity_task(log, writes=['inversion_flowlines'])
9✔
1931
def catchment_width_correction(gdir):
9✔
1932
    """Corrects for NaNs and inconsistencies in the geometrical widths.
1933

1934
    Interpolates missing values, ensures consistency of the
1935
    surface-area distribution AND with the geometrical area of the glacier
1936
    polygon, avoiding errors due to gridded representation.
1937

1938
    Updates the 'inversion_flowlines' save file.
1939

1940
    Parameters
1941
    ----------
1942
    gdir : :py:class:`oggm.GlacierDirectory`
1943
        where to write the data
1944
    """
1945

1946
    # variables
1947
    fls = gdir.read_pickle('inversion_flowlines')
7✔
1948
    catchment_indices = gdir.read_pickle('geometries')['catchment_indices']
7✔
1949

1950
    # Topography for altitude-area distribution
1951
    # I take the non-smoothed topography and remove the borders
1952
    fpath = gdir.get_filepath('gridded_data')
7✔
1953
    with utils.ncDataset(fpath) as nc:
7✔
1954
        topo = nc.variables['topo'][:]
7✔
1955
        ext = nc.variables['glacier_ext'][:]
7✔
1956
    topo[np.where(ext == 1)] = np.NaN
7✔
1957

1958
    # Param
1959
    nmin = int(cfg.PARAMS['min_n_per_bin'])
7✔
1960
    smooth_ws = int(cfg.PARAMS['smooth_widths_window_size'])
7✔
1961

1962
    # Per flowline (important so that later, the indices can be moved)
1963
    catchment_heights = []
7✔
1964
    for ci in catchment_indices:
7✔
1965
        _t = topo[tuple(ci.T)][:]
7✔
1966
        catchment_heights.append(list(_t[np.isfinite(_t)]))
7✔
1967

1968
    # Loop over lines in a reverse order
1969
    for fl, catch_h in zip(fls, catchment_heights):
7✔
1970

1971
        # Interpolate widths
1972
        widths = utils.interp_nans(fl.widths)
7✔
1973
        widths = utils.clip_min(widths, 0.1)
7✔
1974

1975
        # Get topo per catchment and per flowline point
1976
        fhgt = fl.surface_h
7✔
1977

1978
        # Max and mins for the histogram
1979
        maxh = np.max(fhgt)
7✔
1980
        minh = np.min(fhgt)
7✔
1981

1982
        # Sometimes, the centerline does not reach as high as each pix on the
1983
        # glacier. (e.g. RGI40-11.00006)
1984
        catch_h = utils.clip_max(catch_h, maxh)
7✔
1985
        # Same for min
1986
        if fl.flows_to is None:
7✔
1987
            # We clip only for main flowline (this has reasons)
1988
            catch_h = utils.clip_min(catch_h, minh)
7✔
1989

1990
        # Now decide on a binsize which ensures at least N element per bin
1991
        bsize = cfg.PARAMS['base_binsize']
7✔
1992
        while True:
7✔
1993
            maxb = utils.nicenumber(maxh, 1)
7✔
1994
            minb = utils.nicenumber(minh, 1, lower=True)
7✔
1995
            bins = np.arange(minb, maxb+bsize+0.01, bsize)
7✔
1996
            minb = np.min(bins)
7✔
1997

1998
            # Ignore the topo pixels below the last bin
1999
            tmp_ght = catch_h[np.where(catch_h >= minb)]
7✔
2000

2001
            topo_digi = np.digitize(tmp_ght, bins) - 1  # I prefer the left
7✔
2002
            fl_digi = np.digitize(fhgt, bins) - 1  # I prefer the left
7✔
2003
            if nmin == 1:
7✔
2004
                # No need for complicated count
2005
                _c = set(topo_digi)
×
2006
                _fl = set(fl_digi)
×
2007
            else:
2008
                # Keep indexes with at least n counts
2009
                _c = Counter(topo_digi.tolist())
7✔
2010
                _c = set([k for (k, v) in _c.items() if v >= nmin])
7✔
2011
                _fl = Counter(fl_digi.tolist())
7✔
2012
                _fl = set([k for (k, v) in _fl.items() if v >= nmin])
7✔
2013

2014
            ref_set = set(range(len(bins)-1))
7✔
2015
            if (_c == ref_set) and (_fl == ref_set):
7✔
2016
                # For each bin, the width(s) have to represent the "real" area
2017
                new_widths = widths.copy()
7✔
2018
                for bi in range(len(bins) - 1):
7✔
2019
                    bintopoarea = len(np.where(topo_digi == bi)[0])
7✔
2020
                    wherewiths = np.where(fl_digi == bi)
7✔
2021
                    binflarea = np.sum(new_widths[wherewiths]) * fl.dx
7✔
2022
                    new_widths[wherewiths] = (bintopoarea / binflarea *
7✔
2023
                                              new_widths[wherewiths])
2024
                break
7✔
2025
            bsize += 5
7✔
2026

2027
            # Add a security for infinite loops
2028
            if bsize > 600:
7✔
2029
                nmin -= 1
×
2030
                bsize = cfg.PARAMS['base_binsize']
×
2031
                log.info('(%s) reduced min n per bin to %d', gdir.rgi_id,
×
2032
                         nmin)
2033
                if nmin == 0:
×
2034
                    raise GeometryError('({}) no binsize could be chosen '
×
2035
                                        .format(gdir.rgi_id))
2036
        if bsize > 300:
7✔
2037
            log.info('(%s) chosen binsize %d', gdir.rgi_id, bsize)
×
2038
        else:
2039
            log.debug('(%s) chosen binsize %d', gdir.rgi_id, bsize)
7✔
2040

2041
        # Now keep the good topo pixels and send the unattributed ones to the
2042
        # next flowline
2043
        tosend = list(catch_h[np.where(catch_h < minb)])
7✔
2044
        if (len(tosend) > 0) and (fl.flows_to is not None):
7✔
2045
            ide = fls.index(fl.flows_to)
5✔
2046
            catchment_heights[ide] = np.append(catchment_heights[ide], tosend)
5✔
2047
        if (len(tosend) > 0) and (fl.flows_to is None):
7✔
2048
            raise RuntimeError('This should not happen')
×
2049

2050
        # Now we have a width which is the "best" representation of our
2051
        # tributary according to the altitude area distribution.
2052
        # This sometimes leads to abrupt changes in the widths from one
2053
        # grid point to another. I think it's not too harmful to smooth them
2054
        # here, at the cost of a less good altitude area distribution
2055
        if smooth_ws != 0:
7✔
2056
            if smooth_ws == 1:
7✔
2057
                new_widths = utils.smooth1d(new_widths)
7✔
2058
            else:
2059
                new_widths = utils.smooth1d(new_widths, window_size=smooth_ws)
×
2060

2061
        # Write it
2062
        fl.widths = new_widths
7✔
2063

2064
    # Final correction - because of the raster, the gridded area of the
2065
    # glacier is not that of the actual geometry. correct for that
2066
    area = 0.
7✔
2067
    for fl in fls:
7✔
2068
        area += np.sum(fl.widths) * fl.dx
7✔
2069

2070
    fac = gdir.rgi_area_m2 / (area * gdir.grid.dx**2)
7✔
2071
    log.debug('(%s) corrected widths with a factor %.2f', gdir.rgi_id, fac)
7✔
2072
    for fl in fls:
7✔
2073
        fl.widths *= fac
7✔
2074

2075
    # Overwrite centerlines
2076
    gdir.write_pickle(fls, 'inversion_flowlines')
7✔
2077

2078

2079
@entity_task(log, writes=['inversion_flowlines'])
9✔
2080
def terminus_width_correction(gdir, new_width=None):
9✔
2081
    """Sets a new value for the terminus width.
2082

2083
    This can be useful for e.g. tidewater glaciers where we know the width
2084
    and don't like the OGGM one.
2085

2086
    This task preserves the glacier area but will change the fit of the
2087
    altitude-area distribution slightly.
2088

2089
    Parameters
2090
    ----------
2091
    gdir : oggm.GlacierDirectory
2092
    new_width : float
2093
       the new width of the terminus (in meters)
2094
    """
2095

2096
    # variables
2097
    fls = gdir.read_pickle('inversion_flowlines')
1✔
2098
    fl = fls[-1]
1✔
2099
    mapdx = gdir.grid.dx
1✔
2100

2101
    # Change the value and interpolate
2102
    width = copy.deepcopy(fl.widths)
1✔
2103
    width[-5:] = np.NaN
1✔
2104
    width[-1] = new_width / mapdx
1✔
2105
    width = utils.interp_nans(width)
1✔
2106

2107
    # Correct for RGI area
2108
    area_to_match = gdir.rgi_area_m2 - np.sum(width[-5:] * mapdx**2 * fl.dx)
1✔
2109
    area_before = np.sum(width[:-5] * mapdx**2 * fl.dx)
1✔
2110
    for tfl in fls[:-1]:
1✔
2111
        area_before += np.sum(tfl.widths * mapdx**2 * fl.dx)
1✔
2112
    cor_factor = area_to_match / area_before
1✔
2113
    for tfl in fls:
1✔
2114
        tfl.widths = tfl.widths * cor_factor
1✔
2115
    width[:-5] = width[:-5] * cor_factor
1✔
2116
    fl.widths = width
1✔
2117

2118
    # Overwrite centerlines
2119
    gdir.write_pickle(fls, 'inversion_flowlines')
1✔
2120

2121

2122
def intersect_downstream_lines(gdir, candidates=None):
9✔
2123
    """Find tributaries to a main glacier by intersecting downstream lines
2124

2125
    The GlacierDirectories must at least contain a `downstream_line`.
2126
    If you have a lot of candidates, only execute the necessary tasks for that
2127
    and do the rest of the preprocessing after this function identified the
2128
    true tributary glaciers.
2129

2130
    Parameters
2131
    ----------
2132
    gdir : oggm.GlacierDirectory
2133
        The main glacier of interest
2134
    candidates: list of oggm.GlacierDirectory
2135
        Possible tributary glaciers to the main glacier
2136

2137
    Returns
2138
    -------
2139
    tributaries: list
2140
        list of tributary rgi_ids
2141
    """
2142

2143
    # make sure tributaries are iterable
2144
    candidates = utils.tolist(candidates)
×
2145

2146
    # Buffer in pixels around the flowline
2147
    buffer = cfg.PARAMS['kbuffer']
×
2148

2149
    # get main glacier downstream line and CRS
2150
    dline = gdir.read_pickle('downstream_line')['full_line']
×
2151
    crs = gdir.grid
×
2152

2153
    # return list
2154
    tributaries = []
×
2155

2156
    # loop over tributaries
2157
    for trib in candidates:
×
2158
        # skip self
2159
        if gdir.rgi_id == trib.rgi_id:
×
2160
            continue
×
2161

2162
        # get tributary glacier downstream line and CRS
2163
        _dline = trib.read_pickle('downstream_line')['full_line']
×
2164
        _crs = trib.grid
×
2165

2166
        # use salem to transform the grids
2167
        _trans_dline = salem.transform_geometry(_dline, crs=_crs, to_crs=crs)
×
2168

2169
        # check for intersection, with a small buffer and add to list
2170
        if dline.intersects(_trans_dline.buffer(buffer)):
×
2171
            tributaries.append(trib)
×
2172

2173
    return tributaries
×
2174

2175

2176
@entity_task(log, writes=['elevation_band_flowline'])
9✔
2177
def elevation_band_flowline(gdir, bin_variables=None, preserve_totals=True):
9✔
2178
    """Compute "squeezed" or "collapsed" glacier flowlines from Huss 2012.
2179

2180
    This writes out a table of along glacier bins, strictly following the
2181
    method described in Werder, M. A., Huss, M., Paul, F., Dehecq, A. and
2182
    Farinotti, D.: A Bayesian ice thickness estimation model for large-scale
2183
    applications, J. Glaciol., 1–16, doi:10.1017/jog.2019.93, 2019.
2184

2185
    The only parameter is cfg.PARAMS['elevation_band_flowline_binsize'],
2186
    which is 30m in Werder et al and 10m in Huss&Farinotti2012.
2187

2188
    Currently the bands are assumed to have a rectangular bed.
2189

2190
    Before calling this task you should run `tasks.define_glacier_region`
2191
    and `gis.simple_glacier_masks`. The logical following task is
2192
    `fixed_dx_elevation_band_flowline` to convert this to an OGGM flowline.
2193

2194
    Parameters
2195
    ----------
2196
    gdir : :py:class:`oggm.GlacierDirectory`
2197
        where to write the data
2198
    bin_variables : str or list of str
2199
        variables to add to the binned flowline
2200
    preserve_totals : bool or list of bool
2201
        whether or not to preserve the variables totals (e.g. volume)
2202
    """
2203

2204
    # Variables
2205
    bin_variables = [] if bin_variables is None else utils.tolist(bin_variables)
4✔
2206
    out_vars = []
4✔
2207
    out_totals = []
4✔
2208
    grids_file = gdir.get_filepath('gridded_data')
4✔
2209
    with utils.ncDataset(grids_file) as nc:
4✔
2210
        glacier_mask = nc.variables['glacier_mask'][:] == 1
4✔
2211
        topo = nc.variables['topo_smoothed'][:]
4✔
2212

2213
        # Check if there and do not raise when not available
2214
        keep = []
4✔
2215
        for var in bin_variables:
4✔
2216
            if var in nc.variables:
1✔
2217
                keep.append(var)
1✔
2218
            else:
2219
                log.warning('{}: var `{}` not found in gridded_data.'
1✔
2220
                            ''.format(gdir.rgi_id, var))
2221
        bin_variables = keep
4✔
2222
        for var in bin_variables:
4✔
2223
            data = nc.variables[var][:]
1✔
2224
            out_totals.append(np.nansum(data) * gdir.grid.dx ** 2)
1✔
2225
            out_vars.append(data[glacier_mask])
1✔
2226

2227
    preserve_totals = utils.tolist(preserve_totals, length=len(bin_variables))
4✔
2228

2229
    # Slope
2230
    sy, sx = np.gradient(topo, gdir.grid.dx)
4✔
2231
    slope = np.arctan(np.sqrt(sx ** 2 + sy ** 2))
4✔
2232

2233
    # Clip following Werder et al 2019
2234
    slope = utils.clip_array(slope, np.deg2rad(0.4), np.deg2rad(60))
4✔
2235

2236
    topo = topo[glacier_mask]
4✔
2237
    slope = slope[glacier_mask]
4✔
2238

2239
    bsize = cfg.PARAMS['elevation_band_flowline_binsize']
4✔
2240

2241
    # Make nice bins ensuring to cover the full range with the given bin size
2242
    maxb = utils.nicenumber(np.max(topo), bsize)
4✔
2243
    minb = utils.nicenumber(np.min(topo), bsize, lower=True)
4✔
2244
    bins = np.arange(minb, maxb + 0.01, bsize)
4✔
2245

2246
    # Some useful constants
2247
    min_alpha = np.deg2rad(0.4)
4✔
2248
    max_alpha = np.deg2rad(60)
4✔
2249

2250
    if len(bins) < 3:
4✔
2251
        # Very low elevation range
2252
        bsize = cfg.PARAMS['elevation_band_flowline_binsize'] / 3
×
2253
        maxb = utils.nicenumber(np.max(topo), bsize)
×
2254
        minb = utils.nicenumber(np.min(topo), bsize, lower=True)
×
2255
        bins = np.arange(minb, maxb + 0.01, bsize)
×
2256
        if len(bins) < 3:
×
2257
            # Ok this just not gonna work
2258
            raise InvalidDEMError('({}) DEM altidude range too small.'
×
2259
                                  .format(gdir.rgi_id))
2260

2261
    # Go - binning
2262
    df = pd.DataFrame()
4✔
2263
    topo_digi = np.digitize(topo, bins) - 1  # I prefer the left
4✔
2264
    for bi in range(len(bins) - 1):
4✔
2265
        # The coordinates of the current bin
2266
        bin_coords = topo_digi == bi
4✔
2267

2268
        # Bin area
2269
        bin_area = np.sum(bin_coords) * gdir.grid.dx ** 2
4✔
2270
        if bin_area == 0:
4✔
2271
            # Ignored in this case - which I believe is strange because deltaH
2272
            # should be larger for the previous bin, but this is what they do
2273
            # according to Zekollari 2019 review
2274
            df.loc[bi, 'area'] = np.NaN
1✔
2275
            continue
1✔
2276
        df.loc[bi, 'area'] = bin_area
4✔
2277

2278
        # Bin average elevation
2279
        df.loc[bi, 'mean_elevation'] = np.mean(topo[bin_coords])
4✔
2280

2281
        # Bin averge slope
2282
        # there are a few more shenanigans here described in Werder et al 2019
2283
        s_bin = slope[bin_coords]
4✔
2284
        # between the 5% percentile and the x% percentile where x is some magic
2285
        qmin = np.quantile(s_bin, 0.05)
4✔
2286
        x = max(2 * np.quantile(s_bin, 0.2) / np.quantile(s_bin, 0.8), 0.55)
4✔
2287
        x = min(x, 0.95)
4✔
2288
        qmax = np.quantile(s_bin, x)
4✔
2289
        sel_s_bin = s_bin[(s_bin >= qmin) & (s_bin <= qmax)]
4✔
2290
        if len(sel_s_bin) == 0:
4✔
2291
            # This can happen when n pix is small. In this case we just avg
2292
            avg_s = np.mean(s_bin)
2✔
2293
        else:
2294
            avg_s = np.mean(sel_s_bin)
4✔
2295

2296
        # Final clip as in Werder et al 2019
2297
        df.loc[bi, 'slope'] = utils.clip_scalar(avg_s, min_alpha, max_alpha)
4✔
2298

2299
        # Binned variables
2300
        for var, data in zip(bin_variables, out_vars):
4✔
2301
            df.loc[bi, var] = np.nanmean(data[bin_coords])
1✔
2302

2303
    # The grid point's grid spacing and widths
2304
    df['bin_elevation'] = (bins[1:] + bins[:-1]) / 2
4✔
2305
    df['dx'] = bsize / np.tan(df['slope'])
4✔
2306
    df['width'] = df['area'] / df['dx']
4✔
2307

2308
    # Remove possible NaNs from above
2309
    df = df.dropna()
4✔
2310

2311
    # Check for binned vars
2312
    for var, data, in_total, do_p in zip(bin_variables, out_vars, out_totals,
4✔
2313
                                         preserve_totals):
2314
        if do_p:
1✔
2315
            out_total = np.nansum(df[var] * df['area'])
1✔
2316
            if out_total > 0:
1✔
2317
                df[var] *= in_total / out_total
1✔
2318

2319
    # In OGGM we go from top to bottom
2320
    df = df[::-1]
4✔
2321

2322
    # The x coordinate in meter - this is a bit arbitrary but we put it at the
2323
    # center of the irregular grid (better for interpolation later
2324
    dx = df['dx'].values
4✔
2325
    dx_points = np.append(dx[0]/2, (dx[:-1] + dx[1:]) / 2)
4✔
2326
    df.index = np.cumsum(dx_points)
4✔
2327
    df.index.name = 'dis_along_flowline'
4✔
2328

2329
    # Store and return
2330
    df.to_csv(gdir.get_filepath('elevation_band_flowline'))
4✔
2331

2332

2333
@entity_task(log, writes=['inversion_flowlines'])
9✔
2334
def fixed_dx_elevation_band_flowline(gdir, bin_variables=None,
9✔
2335
                                     preserve_totals=True):
2336
    """Converts the "collapsed" flowline into a regular "inversion flowline".
2337

2338
    You need to run `tasks.elevation_band_flowline` first. It then interpolates
2339
    onto a regular grid with the same dx as the one that OGGM would choose
2340
    (cfg.PARAMS['flowline_dx'] * map_dx).
2341

2342
    Parameters
2343
    ----------
2344
    gdir : :py:class:`oggm.GlacierDirectory`
2345
        where to write the data
2346
    bin_variables : str or list of str
2347
        variables to add to the interpolated flowline (will be stored in a new
2348
        csv file: gdir.get_filepath('elevation_band_flowline',
2349
        filesuffix='_fixed_dx').
2350
    preserve_totals : bool or list of bool
2351
        whether or not to preserve the variables totals (e.g. volume)
2352
    """
2353

2354
    df = pd.read_csv(gdir.get_filepath('elevation_band_flowline'), index_col=0)
4✔
2355

2356
    map_dx = gdir.grid.dx
4✔
2357
    dx = cfg.PARAMS['flowline_dx']
4✔
2358
    dx_meter = dx * map_dx
4✔
2359
    nx = int(df.dx.sum() / dx_meter)
4✔
2360
    dis_along_flowline = dx_meter / 2 + np.arange(nx) * dx_meter
4✔
2361

2362
    while dis_along_flowline[-1] > df.index[-1]:
4✔
2363
        # do not extrapolate
2364
        dis_along_flowline = dis_along_flowline[:-1]
2✔
2365

2366
    while dis_along_flowline[0] < df.index[0]:
4✔
2367
        # do not extrapolate
2368
        dis_along_flowline = dis_along_flowline[1:]
3✔
2369

2370
    nx = len(dis_along_flowline)
4✔
2371

2372
    # Interpolate the data we need
2373
    hgts = np.interp(dis_along_flowline, df.index, df['mean_elevation'])
4✔
2374
    widths_m = np.interp(dis_along_flowline, df.index, df['width'])
4✔
2375

2376
    # Correct the widths - area preserving
2377
    area = np.sum(widths_m * dx_meter)
4✔
2378
    fac = gdir.rgi_area_m2 / area
4✔
2379
    log.debug('(%s) corrected widths with a factor %.2f', gdir.rgi_id, fac)
4✔
2380
    widths_m *= fac
4✔
2381

2382
    # Additional vars
2383
    if bin_variables is not None:
4✔
2384
        bin_variables = utils.tolist(bin_variables)
1✔
2385

2386
        # Check if there and do not raise when not available
2387
        keep = []
1✔
2388
        for var in bin_variables:
1✔
2389
            if var in df:
1✔
2390
                keep.append(var)
1✔
2391
            else:
2392
                log.warning('{}: var `{}` not found in gridded_data.'
1✔
2393
                            ''.format(gdir.rgi_id, var))
2394
        bin_variables = keep
1✔
2395

2396
        preserve_totals = utils.tolist(preserve_totals,
1✔
2397
                                       length=len(bin_variables))
2398
        odf = pd.DataFrame(index=dis_along_flowline)
1✔
2399
        odf.index.name = 'dis_along_flowline'
1✔
2400
        odf['widths_m'] = widths_m
1✔
2401
        odf['area_m2'] = widths_m * dx_meter
1✔
2402
        for var, do_p in zip(bin_variables, preserve_totals):
1✔
2403
            interp = np.interp(dis_along_flowline, df.index, df[var])
1✔
2404
            if do_p:
1✔
2405
                in_total = np.nansum(df[var] * df['area'])
1✔
2406
                out_total = np.nansum(interp * widths_m * dx_meter)
1✔
2407
                if out_total > 0:
1✔
2408
                    interp *= in_total / out_total
1✔
2409
            odf[var] = interp
1✔
2410
        odf.to_csv(gdir.get_filepath('elevation_band_flowline',
1✔
2411
                                     filesuffix='_fixed_dx'))
2412

2413
    # Write as a Centerline object
2414
    fl = Centerline(None, dx=dx, surface_h=hgts, rgi_id=gdir.rgi_id,
4✔
2415
                    map_dx=map_dx)
2416
    fl.order = 0
4✔
2417
    fl.widths = widths_m / map_dx
4✔
2418
    fl.is_rectangular = np.zeros(nx, dtype=bool)
4✔
2419
    fl.is_trapezoid = np.ones(nx, dtype=bool)
4✔
2420

2421
    if gdir.is_tidewater:
4✔
2422
        fl.is_rectangular[-5:] = True
1✔
2423
        fl.is_trapezoid[-5:] = False
1✔
2424

2425
    gdir.write_pickle([fl], 'inversion_flowlines')
4✔
2426
    gdir.add_to_diagnostics('flowline_type', 'elevation_band')
4✔
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