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

OGGM / oggm / 5975837719

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

push

github

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

* Update index.rst

* Update index.rst

11441 of 13374 relevant lines covered (85.55%)

3.76 hits per line

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

81.15
/oggm/core/gis.py
1
""" Handling of the local glacier map and masks. Defines the first tasks
2
to be realized by any OGGM pre-processing workflow.
3

4
"""
5
# Built ins
6
import os
9✔
7
import logging
9✔
8
import warnings
9✔
9
from packaging.version import Version
9✔
10
from functools import partial
9✔
11

12
# External libs
13
import numpy as np
9✔
14
import shapely.ops
9✔
15
import pandas as pd
9✔
16
import xarray as xr
9✔
17
import shapely.geometry as shpg
9✔
18
import scipy.signal
9✔
19
from scipy.ndimage import label, distance_transform_edt, binary_erosion
9✔
20
from scipy.interpolate import griddata
9✔
21
from scipy import optimize as optimization
9✔
22

23
# Optional libs
24
try:
9✔
25
    import salem
9✔
26
    from salem.gis import transform_proj
9✔
27
except ImportError:
28
    pass
29
try:
9✔
30
    import pyproj
9✔
31
except ImportError:
32
    pass
33
try:
9✔
34
    import geopandas as gpd
9✔
35
except ImportError:
36
    pass
37
try:
9✔
38
    import skimage.draw as skdraw
9✔
39
except ImportError:
40
    pass
41
try:
9✔
42
    import rasterio
9✔
43
    from rasterio.warp import reproject, Resampling
8✔
44
    from rasterio.mask import mask as riomask
8✔
45
    try:
8✔
46
        # rasterio V > 1.0
47
        from rasterio.merge import merge as merge_tool
8✔
48
    except ImportError:
49
        from rasterio.tools.merge import merge as merge_tool
50
except ImportError:
51
    pass
52

53
# Locals
54
from oggm import entity_task, utils
9✔
55
import oggm.cfg as cfg
9✔
56
from oggm.exceptions import (InvalidParamsError, InvalidGeometryError,
9✔
57
                             InvalidDEMError, GeometryError,
58
                             InvalidWorkflowError)
59
from oggm.utils import (tuple2int, get_topo_file, is_dem_source_available,
9✔
60
                        nicenumber, ncDataset, tolist)
61

62

63
# Module logger
64
log = logging.getLogger(__name__)
9✔
65

66
# Needed later
67
label_struct = np.ones((3, 3))
9✔
68

69

70
def _parse_source_text():
9✔
71
    fp = os.path.join(os.path.abspath(os.path.dirname(cfg.__file__)),
9✔
72
                      'data', 'dem_sources.txt')
73

74
    out = dict()
9✔
75
    cur_key = None
9✔
76
    with open(fp, 'r', encoding='utf-8') as fr:
9✔
77
        this_text = []
9✔
78
        for l in fr.readlines():
9✔
79
            l = l.strip()
9✔
80
            if l and (l[0] == '[' and l[-1] == ']'):
9✔
81
                if cur_key:
9✔
82
                    out[cur_key] = '\n'.join(this_text)
9✔
83
                this_text = []
9✔
84
                cur_key = l.strip('[]')
9✔
85
                continue
9✔
86
            this_text.append(l)
9✔
87
    out[cur_key] = '\n'.join(this_text)
9✔
88
    return out
9✔
89

90

91
DEM_SOURCE_INFO = _parse_source_text()
9✔
92

93

94
def gaussian_blur(in_array, size):
9✔
95
    """Applies a Gaussian filter to a 2d array.
96

97
    Parameters
98
    ----------
99
    in_array : numpy.array
100
        The array to smooth.
101
    size : int
102
        The half size of the smoothing window.
103

104
    Returns
105
    -------
106
    a smoothed numpy.array
107
    """
108

109
    # expand in_array to fit edge of kernel
110
    padded_array = np.pad(in_array, size, 'symmetric')
7✔
111

112
    # build kernel
113
    x, y = np.mgrid[-size:size + 1, -size:size + 1]
7✔
114
    g = np.exp(-(x**2 / float(size) + y**2 / float(size)))
7✔
115
    g = (g / g.sum()).astype(in_array.dtype)
7✔
116

117
    # do the Gaussian blur
118
    return scipy.signal.fftconvolve(padded_array, g, mode='valid')
7✔
119

120

121
def _interp_polygon(polygon, dx):
9✔
122
    """Interpolates an irregular polygon to a regular step dx.
123

124
    Interior geometries are also interpolated if they are longer then 3*dx,
125
    otherwise they are ignored.
126

127
    Parameters
128
    ----------
129
    polygon: The shapely.geometry.Polygon instance to interpolate
130
    dx : the step (float)
131

132
    Returns
133
    -------
134
    an interpolated shapely.geometry.Polygon class instance.
135
    """
136

137
    # remove last (duplex) point to build a LineString from the LinearRing
138
    line = shpg.LineString(np.asarray(polygon.exterior.xy).T)
7✔
139

140
    e_line = []
7✔
141
    for distance in np.arange(0.0, line.length, dx):
7✔
142
        e_line.append(*line.interpolate(distance).coords)
7✔
143
    e_line = shpg.LinearRing(e_line)
7✔
144

145
    i_lines = []
7✔
146
    for ipoly in polygon.interiors:
7✔
147
        line = shpg.LineString(np.asarray(ipoly.xy).T)
7✔
148
        if line.length < 3*dx:
7✔
149
            continue
7✔
150
        i_points = []
7✔
151
        for distance in np.arange(0.0, line.length, dx):
7✔
152
            i_points.append(*line.interpolate(distance).coords)
7✔
153
        i_lines.append(shpg.LinearRing(i_points))
7✔
154

155
    return shpg.Polygon(e_line, i_lines)
7✔
156

157

158
def _polygon_to_pix(polygon):
9✔
159
    """Transforms polygon coordinates to integer pixel coordinates. It makes
160
    the geometry easier to handle and reduces the number of points.
161

162
    Parameters
163
    ----------
164
    polygon: the shapely.geometry.Polygon instance to transform.
165

166
    Returns
167
    -------
168
    a shapely.geometry.Polygon class instance.
169
    """
170

171
    def project(x, y):
7✔
172
        return np.rint(x).astype(np.int64), np.rint(y).astype(np.int64)
7✔
173

174
    def project_coarse(x, y, c=2):
7✔
175
        return ((np.rint(x/c)*c).astype(np.int64),
×
176
                (np.rint(y/c)*c).astype(np.int64))
177

178
    poly_pix = shapely.ops.transform(project, polygon)
7✔
179

180
    # simple trick to correct invalid polys:
181
    tmp = poly_pix.buffer(0)
7✔
182

183
    # try to deal with a bug in buffer where the corrected poly would be null
184
    c = 2
7✔
185
    while tmp.length == 0 and c < 7:
7✔
186
        project = partial(project_coarse, c=c)
×
187
        poly_pix = shapely.ops.transform(project_coarse, polygon)
×
188
        tmp = poly_pix.buffer(0)
×
189
        c += 1
×
190

191
    # We tried all we could
192
    if tmp.length == 0:
7✔
193
        raise InvalidGeometryError('This glacier geometry is not valid for '
×
194
                                   'OGGM.')
195

196
    # sometimes the glacier gets cut out in parts
197
    if tmp.geom_type == 'MultiPolygon':
7✔
198
        # If only small arms are cut out, remove them
199
        area = np.array([_tmp.area for _tmp in tmp.geoms])
7✔
200
        _tokeep = np.argmax(area).item()
7✔
201
        tmp = tmp.geoms[_tokeep]
7✔
202

203
        # check that the other parts really are small,
204
        # otherwise replace tmp with something better
205
        area = area / area[_tokeep]
7✔
206
        for _a in area:
7✔
207
            if _a != 1 and _a > 0.05:
7✔
208
                # these are extremely thin glaciers
209
                # eg. RGI40-11.01381 RGI40-11.01697 params.d1 = 5. and d2 = 8.
210
                # make them bigger until its ok
211
                for b in np.arange(0., 1., 0.01):
×
212
                    tmp = shapely.ops.transform(project, polygon.buffer(b))
×
213
                    tmp = tmp.buffer(0)
×
214
                    if tmp.geom_type == 'MultiPolygon':
×
215
                        continue
×
216
                    if tmp.is_valid:
×
217
                        break
×
218
                if b == 0.99:
×
219
                    raise InvalidGeometryError('This glacier geometry is not '
×
220
                                               'valid for OGGM.')
221

222
    if not tmp.is_valid:
7✔
223
        raise InvalidGeometryError('This glacier geometry is not valid '
×
224
                                   'for OGGM.')
225

226
    return tmp
7✔
227

228

229
def glacier_grid_params(gdir):
9✔
230
    """Define the glacier grid map based on the user params."""
231

232
    # Get the local map proj params and glacier extent
233
    gdf = gdir.read_shapefile('outlines')
7✔
234

235
    # Get the map proj
236
    utm_proj = salem.check_crs(gdf.crs)
7✔
237

238
    # Get glacier extent
239
    try:
7✔
240
        xx, yy = gdf.iloc[0]['geometry'].exterior.xy
7✔
241
    # special treatment for Multipolygons
242
    except AttributeError:
×
243
        if not cfg.PARAMS['keep_multipolygon_outlines']:
×
244
            raise
×
245
        parts = []
×
246
        for p in gdf.iloc[0]['geometry'].geoms:
×
247
            parts.append(p)
×
248
        parts = np.array(parts)
×
249

250
        xx = []
×
251
        yy = []
×
252
        for part in parts:
×
253
            xx_tmp, yy_tmp = part.exterior.xy
×
254
            xx = np.append(xx, xx_tmp)
×
255
            yy = np.append(yy, yy_tmp)
×
256

257
    # Define glacier area to use
258
    area = gdir.rgi_area_km2
7✔
259

260
    # Choose a spatial resolution with respect to the glacier area
261
    dxmethod = cfg.PARAMS['grid_dx_method']
7✔
262
    if dxmethod == 'linear':
7✔
263
        dx = np.rint(cfg.PARAMS['d1'] * area + cfg.PARAMS['d2'])
1✔
264
    elif dxmethod == 'square':
7✔
265
        dx = np.rint(cfg.PARAMS['d1'] * np.sqrt(area) + cfg.PARAMS['d2'])
7✔
266
    elif dxmethod == 'fixed':
1✔
267
        dx = np.rint(cfg.PARAMS['fixed_dx'])
1✔
268
    else:
269
        raise InvalidParamsError('grid_dx_method not supported: {}'
×
270
                                 .format(dxmethod))
271
    # Additional trick for varying dx
272
    if dxmethod in ['linear', 'square']:
7✔
273
        dx = utils.clip_scalar(dx, cfg.PARAMS['d2'], cfg.PARAMS['dmax'])
7✔
274

275
    log.debug('(%s) area %.2f km, dx=%.1f', gdir.rgi_id, area, dx)
7✔
276

277
    # Safety check
278
    border = cfg.PARAMS['border']
7✔
279
    if border > 1000:
7✔
280
        raise InvalidParamsError("You have set a cfg.PARAMS['border'] value "
×
281
                                 "of {}. ".format(cfg.PARAMS['border']) +
282
                                 'This a very large value, which is '
283
                                 'currently not supported in OGGM.')
284

285
    # For tidewater glaciers we force border to 10
286
    if gdir.is_tidewater and cfg.PARAMS['clip_tidewater_border']:
7✔
287
        border = 10
6✔
288

289
    # Corners, incl. a buffer of N pix
290
    ulx = np.min(xx) - border * dx
7✔
291
    lrx = np.max(xx) + border * dx
7✔
292
    uly = np.max(yy) + border * dx
7✔
293
    lry = np.min(yy) - border * dx
7✔
294
    # n pixels
295
    nx = int((lrx - ulx) / dx)
7✔
296
    ny = int((uly - lry) / dx)
7✔
297

298
    return utm_proj, nx, ny, ulx, uly, dx
7✔
299

300

301
@entity_task(log, writes=['glacier_grid', 'dem', 'outlines'])
9✔
302
def define_glacier_region(gdir, entity=None, source=None):
9✔
303
    """Very first task after initialization: define the glacier's local grid.
304

305
    Defines the local projection (Transverse Mercator or UTM depending on
306
    user choice), centered on the glacier.
307
    There is some options to set the resolution of the local grid.
308
    It can be adapted depending on the size of the glacier.
309
    See ``params.cfg`` for setting these options.
310

311
    Default values of the adapted mode lead to a resolution of 50 m for
312
    Hintereisferner, which is approx. 8 km2 large.
313

314
    After defining the grid, the topography and the outlines of the glacier
315
    are transformed into the local projection.
316

317
    Parameters
318
    ----------
319
    gdir : :py:class:`oggm.GlacierDirectory`
320
        where to write the data
321
    entity : geopandas.GeoSeries
322
        the glacier geometry to process - DEPRECATED. It is now ignored
323
    source : str or list of str, optional
324
        If you want to force the use of a certain DEM source. Available are:
325
          - 'USER' : file set in cfg.PATHS['dem_file']
326
          - 'SRTM' : http://srtm.csi.cgiar.org/
327
          - 'GIMP' : https://bpcrc.osu.edu/gdg/data/gimpdem
328
          - 'RAMP' : https://nsidc.org/data/nsidc-0082/versions/2/documentation
329
          - 'REMA' : https://www.pgc.umn.edu/data/rema/
330
          - 'DEM3' : http://viewfinderpanoramas.org/
331
          - 'ASTER' : https://asterweb.jpl.nasa.gov/gdem.asp
332
          - 'TANDEM' : https://geoservice.dlr.de/web/dataguide/tdm90/
333
          - 'ARCTICDEM' : https://www.pgc.umn.edu/data/arcticdem/
334
          - 'AW3D30' : https://www.eorc.jaxa.jp
335
          - 'MAPZEN' : https://registry.opendata.aws/terrain-tiles/
336
          - 'ALASKA' : https://www.the-cryosphere.net/8/503/2014/
337
          - 'COPDEM30' : Copernicus DEM GLO30 https://spacedata.copernicus.eu/web/cscda/cop-dem-faq
338
          - 'COPDEM90' : Copernicus DEM GLO90 https://spacedata.copernicus.eu/web/cscda/cop-dem-faq
339
          - 'NASADEM':  https://doi.org/10.5069/G93T9FD9
340
    """
341

342
    utm_proj, nx, ny, ulx, uly, dx = glacier_grid_params(gdir)
7✔
343

344
    # Back to lon, lat for DEM download/preparation
345
    tmp_grid = salem.Grid(proj=utm_proj, nxny=(nx, ny), x0y0=(ulx, uly),
7✔
346
                          dxdy=(dx, -dx), pixel_ref='corner')
347
    minlon, maxlon, minlat, maxlat = tmp_grid.extent_in_crs(crs=salem.wgs84)
7✔
348

349
    # Open DEM
350
    # We test DEM availability for glacier only (maps can grow big)
351
    if isinstance(source, list):  # when multiple sources are provided, try them sequentially
7✔
352
        for src in source:
×
353
            source_exists = is_dem_source_available(src, *gdir.extent_ll)
×
354
            if source_exists:
×
355
                source = src  # pick the first source which exists
×
356
                break
×
357
    else:
358
        source_exists = is_dem_source_available(source, *gdir.extent_ll)
7✔
359
        
360
    if not source_exists:
7✔
361
        raise InvalidWorkflowError(f'Source: {source} is not available for '
×
362
                                   f'glacier {gdir.rgi_id} with border '
363
                                   f"{cfg.PARAMS['border']}")
364
    dem_list, dem_source = get_topo_file((minlon, maxlon), (minlat, maxlat),
7✔
365
                                         rgi_id=gdir.rgi_id,
366
                                         dx_meter=dx,
367
                                         source=source)
368
    log.debug('(%s) DEM source: %s', gdir.rgi_id, dem_source)
7✔
369
    log.debug('(%s) N DEM Files: %s', gdir.rgi_id, len(dem_list))
7✔
370

371
    # Decide how to tag nodata
372
    def _get_nodata(rio_ds):
7✔
373
        nodata = rio_ds[0].meta.get('nodata', None)
7✔
374
        if nodata is None:
7✔
375
            # badly tagged geotiffs, let's do it ourselves
376
            nodata = -32767 if source == 'TANDEM' else -9999
6✔
377
        return nodata
7✔
378

379
    # A glacier area can cover more than one tile:
380
    if len(dem_list) == 1:
7✔
381
        dem_dss = [rasterio.open(dem_list[0])]  # if one tile, just open it
7✔
382
        dem_data = rasterio.band(dem_dss[0], 1)
7✔
383
        if Version(rasterio.__version__) >= Version('1.0'):
7✔
384
            src_transform = dem_dss[0].transform
7✔
385
        else:
386
            src_transform = dem_dss[0].affine
×
387
        nodata = _get_nodata(dem_dss)
7✔
388
    else:
389
        dem_dss = [rasterio.open(s) for s in dem_list]  # list of rasters
×
390
        nodata = _get_nodata(dem_dss)
×
391
        dem_data, src_transform = merge_tool(dem_dss, nodata=nodata)  # merge
×
392

393
    # Use Grid properties to create a transform (see rasterio cookbook)
394
    dst_transform = rasterio.transform.from_origin(
7✔
395
        ulx, uly, dx, dx  # sign change (2nd dx) is done by rasterio.transform
396
    )
397

398
    # Set up profile for writing output
399
    profile = dem_dss[0].profile
7✔
400
    profile.update({
7✔
401
        'crs': utm_proj.srs,
402
        'transform': dst_transform,
403
        'nodata': nodata,
404
        'width': nx,
405
        'height': ny,
406
        'driver': 'GTiff'
407
    })
408

409
    # Could be extended so that the cfg file takes all Resampling.* methods
410
    if cfg.PARAMS['topo_interp'] == 'bilinear':
7✔
411
        resampling = Resampling.bilinear
×
412
    elif cfg.PARAMS['topo_interp'] == 'cubic':
7✔
413
        resampling = Resampling.cubic
7✔
414
    else:
415
        raise InvalidParamsError('{} interpolation not understood'
×
416
                                 .format(cfg.PARAMS['topo_interp']))
417

418
    dem_reproj = gdir.get_filepath('dem')
7✔
419
    profile.pop('blockxsize', None)
7✔
420
    profile.pop('blockysize', None)
7✔
421
    profile.pop('compress', None)
7✔
422
    with rasterio.open(dem_reproj, 'w', **profile) as dest:
7✔
423
        dst_array = np.empty((ny, nx), dtype=dem_dss[0].dtypes[0])
7✔
424
        reproject(
7✔
425
            # Source parameters
426
            source=dem_data,
427
            src_crs=dem_dss[0].crs,
428
            src_transform=src_transform,
429
            src_nodata=nodata,
430
            # Destination parameters
431
            destination=dst_array,
432
            dst_transform=dst_transform,
433
            dst_crs=utm_proj.srs,
434
            dst_nodata=nodata,
435
            # Configuration
436
            resampling=resampling)
437
        dest.write(dst_array, 1)
7✔
438

439
    for dem_ds in dem_dss:
7✔
440
        dem_ds.close()
7✔
441

442
    # Glacier grid
443
    x0y0 = (ulx+dx/2, uly-dx/2)  # To pixel center coordinates
7✔
444
    glacier_grid = salem.Grid(proj=utm_proj, nxny=(nx, ny), dxdy=(dx, -dx),
7✔
445
                              x0y0=x0y0)
446
    glacier_grid.to_json(gdir.get_filepath('glacier_grid'))
7✔
447

448
    # Write DEM source info
449
    gdir.add_to_diagnostics('dem_source', dem_source)
7✔
450
    source_txt = DEM_SOURCE_INFO.get(dem_source, dem_source)
7✔
451
    with open(gdir.get_filepath('dem_source'), 'w') as fw:
7✔
452
        fw.write(source_txt)
7✔
453
        fw.write('\n\n')
7✔
454
        fw.write('# Data files\n\n')
7✔
455
        for fname in dem_list:
7✔
456
            fw.write('{}\n'.format(os.path.basename(fname)))
7✔
457

458

459
def rasterio_to_gdir(gdir, input_file, output_file_name,
9✔
460
                     resampling='cubic'):
461
    """Reprojects a file that rasterio can read into the glacier directory.
462

463
    Parameters
464
    ----------
465
    gdir : :py:class:`oggm.GlacierDirectory`
466
        the glacier directory
467
    input_file : str
468
        path to the file to reproject
469
    output_file_name : str
470
        name of the output file (must be in cfg.BASENAMES)
471
    resampling : str
472
        nearest', 'bilinear', 'cubic', 'cubic_spline', or one of
473
        https://rasterio.readthedocs.io/en/latest/topics/resampling.html
474
    """
475

476
    output_file = gdir.get_filepath(output_file_name)
2✔
477
    assert '.tif' in output_file, 'output_file should end with .tif'
2✔
478

479
    if not gdir.has_file('dem'):
2✔
480
        raise InvalidWorkflowError('Need a dem.tif file to reproject to')
×
481

482
    with rasterio.open(input_file) as src:
2✔
483

484
        kwargs = src.meta.copy()
2✔
485
        data = src.read(1)
2✔
486

487
        with rasterio.open(gdir.get_filepath('dem')) as tpl:
2✔
488

489
            kwargs.update({
2✔
490
                'crs': tpl.crs,
491
                'transform': tpl.transform,
492
                'width': tpl.width,
493
                'height': tpl.height
494
            })
495

496
            with rasterio.open(output_file, 'w', **kwargs) as dst:
2✔
497
                for i in range(1, src.count + 1):
2✔
498

499
                    dest = np.zeros(shape=(tpl.height, tpl.width),
2✔
500
                                    dtype=data.dtype)
501

502
                    reproject(
2✔
503
                        source=rasterio.band(src, i),
504
                        destination=dest,
505
                        src_transform=src.transform,
506
                        src_crs=src.crs,
507
                        dst_transform=tpl.transform,
508
                        dst_crs=tpl.crs,
509
                        resampling=getattr(Resampling, resampling)
510
                    )
511

512
                    dst.write(dest, indexes=i)
2✔
513

514

515
def read_geotiff_dem(gdir):
9✔
516
    """Reads (and masks out) the DEM out of the gdir's geotiff file.
517

518
    Parameters
519
    ----------
520
    gdir : :py:class:`oggm.GlacierDirectory`
521
        the glacier directory
522

523
    Returns
524
    -------
525
    2D np.float32 array
526
    """
527
    with rasterio.open(gdir.get_filepath('dem'), 'r', driver='GTiff') as ds:
7✔
528
        topo = ds.read(1).astype(rasterio.float32)
7✔
529
        topo[topo <= -999.] = np.NaN
7✔
530
        topo[ds.read_masks(1) == 0] = np.NaN
7✔
531
    return topo
7✔
532

533

534
class GriddedNcdfFile(object):
9✔
535
    """Creates or opens a gridded netcdf file template.
536

537
    The other variables have to be created and filled by the calling
538
    routine.
539
    """
540
    def __init__(self, gdir, basename='gridded_data', reset=False):
9✔
541
        self.fpath = gdir.get_filepath(basename)
7✔
542
        self.grid = gdir.grid
7✔
543
        if reset and os.path.exists(self.fpath):
7✔
544
            os.remove(self.fpath)
×
545

546
    def __enter__(self):
9✔
547

548
        if os.path.exists(self.fpath):
7✔
549
            # Already there - just append
550
            self.nc = ncDataset(self.fpath, 'a', format='NETCDF4')
7✔
551
            return self.nc
7✔
552

553
        # Create and fill
554
        nc = ncDataset(self.fpath, 'w', format='NETCDF4')
7✔
555

556
        nc.createDimension('x', self.grid.nx)
7✔
557
        nc.createDimension('y', self.grid.ny)
7✔
558

559
        nc.author = 'OGGM'
7✔
560
        nc.author_info = 'Open Global Glacier Model'
7✔
561
        nc.pyproj_srs = self.grid.proj.srs
7✔
562

563
        x = self.grid.x0 + np.arange(self.grid.nx) * self.grid.dx
7✔
564
        y = self.grid.y0 + np.arange(self.grid.ny) * self.grid.dy
7✔
565

566
        v = nc.createVariable('x', 'f4', ('x',), zlib=True)
7✔
567
        v.units = 'm'
7✔
568
        v.long_name = 'x coordinate of projection'
7✔
569
        v.standard_name = 'projection_x_coordinate'
7✔
570
        v[:] = x
7✔
571

572
        v = nc.createVariable('y', 'f4', ('y',), zlib=True)
7✔
573
        v.units = 'm'
7✔
574
        v.long_name = 'y coordinate of projection'
7✔
575
        v.standard_name = 'projection_y_coordinate'
7✔
576
        v[:] = y
7✔
577

578
        self.nc = nc
7✔
579
        return nc
7✔
580

581
    def __exit__(self, exc_type, exc_value, exc_traceback):
9✔
582
        self.nc.close()
7✔
583

584

585
@entity_task(log, writes=['gridded_data'])
9✔
586
def process_dem(gdir):
9✔
587
    """Reads the DEM from the tiff, attempts to fill voids and apply smooth.
588

589
    The data is then written to `gridded_data.nc`.
590

591
    Parameters
592
    ----------
593
    gdir : :py:class:`oggm.GlacierDirectory`
594
        where to write the data
595
    """
596

597
    # open srtm tif-file:
598
    dem = read_geotiff_dem(gdir)
7✔
599

600
    # Grid
601
    nx = gdir.grid.nx
7✔
602
    ny = gdir.grid.ny
7✔
603

604
    # Correct the DEM
605
    valid_mask = np.isfinite(dem)
7✔
606
    if np.all(~valid_mask):
7✔
607
        raise InvalidDEMError('Not a single valid grid point in DEM')
×
608

609
    if np.any(~valid_mask):
7✔
610
        # We interpolate
611
        if np.sum(~valid_mask) > (0.25 * nx * ny):
1✔
612
            log.info('({}) more than 25% NaNs in DEM'.format(gdir.rgi_id))
×
613
        xx, yy = gdir.grid.ij_coordinates
1✔
614
        pnan = np.nonzero(~valid_mask)
1✔
615
        pok = np.nonzero(valid_mask)
1✔
616
        points = np.array((np.ravel(yy[pok]), np.ravel(xx[pok]))).T
1✔
617
        inter = np.array((np.ravel(yy[pnan]), np.ravel(xx[pnan]))).T
1✔
618
        try:
1✔
619
            dem[pnan] = griddata(points, np.ravel(dem[pok]), inter,
1✔
620
                                 method='linear')
621
        except ValueError:
×
622
            raise InvalidDEMError('DEM interpolation not possible.')
×
623
        log.info(gdir.rgi_id + ': DEM needed interpolation.')
1✔
624
        gdir.add_to_diagnostics('dem_needed_interpolation', True)
1✔
625
        gdir.add_to_diagnostics('dem_invalid_perc', len(pnan[0]) / (nx * ny))
1✔
626

627
    isfinite = np.isfinite(dem)
7✔
628
    if np.any(~isfinite):
7✔
629
        # interpolation will still leave NaNs in DEM:
630
        # extrapolate with NN if needed (e.g. coastal areas)
631
        xx, yy = gdir.grid.ij_coordinates
1✔
632
        pnan = np.nonzero(~isfinite)
1✔
633
        pok = np.nonzero(isfinite)
1✔
634
        points = np.array((np.ravel(yy[pok]), np.ravel(xx[pok]))).T
1✔
635
        inter = np.array((np.ravel(yy[pnan]), np.ravel(xx[pnan]))).T
1✔
636
        try:
1✔
637
            dem[pnan] = griddata(points, np.ravel(dem[pok]), inter,
1✔
638
                                 method='nearest')
639
        except ValueError:
×
640
            raise InvalidDEMError('DEM extrapolation not possible.')
×
641
        log.info(gdir.rgi_id + ': DEM needed extrapolation.')
1✔
642
        gdir.add_to_diagnostics('dem_needed_extrapolation', True)
1✔
643
        gdir.add_to_diagnostics('dem_extrapol_perc', len(pnan[0]) / (nx * ny))
1✔
644

645
    if np.min(dem) == np.max(dem):
7✔
646
        raise InvalidDEMError('({}) min equal max in the DEM.'
×
647
                              .format(gdir.rgi_id))
648

649
    # Clip topography to 0 m a.s.l.
650
    if cfg.PARAMS['clip_dem_to_zero']:
7✔
651
        utils.clip_min(dem, 0, out=dem)
7✔
652

653
    # Smooth DEM?
654
    if cfg.PARAMS['smooth_window'] > 0.:
7✔
655
        gsize = np.rint(cfg.PARAMS['smooth_window'] / gdir.grid.dx)
7✔
656
        smoothed_dem = gaussian_blur(dem, int(gsize))
7✔
657
    else:
658
        smoothed_dem = dem.copy()
×
659

660
    # Clip topography to 0 m a.s.l.
661
    if cfg.PARAMS['clip_dem_to_zero']:
7✔
662
        utils.clip_min(smoothed_dem, 0, out=smoothed_dem)
7✔
663

664
    # Write to file
665
    with GriddedNcdfFile(gdir, reset=True) as nc:
7✔
666

667
        v = nc.createVariable('topo', 'f4', ('y', 'x',), zlib=True)
7✔
668
        v.units = 'm'
7✔
669
        v.long_name = 'DEM topography'
7✔
670
        v[:] = dem
7✔
671

672
        v = nc.createVariable('topo_smoothed', 'f4', ('y', 'x',), zlib=True)
7✔
673
        v.units = 'm'
7✔
674
        v.long_name = ('DEM topography smoothed with radius: '
7✔
675
                       '{:.1} m'.format(cfg.PARAMS['smooth_window']))
676
        v[:] = smoothed_dem
7✔
677

678
        # If there was some invalid data store this as well
679
        v = nc.createVariable('topo_valid_mask', 'i1', ('y', 'x',), zlib=True)
7✔
680
        v.units = '-'
7✔
681
        v.long_name = 'DEM validity mask according to geotiff input (1-0)'
7✔
682
        v[:] = valid_mask.astype(int)
7✔
683

684
        # add some meta stats and close
685
        nc.max_h_dem = np.nanmax(dem)
7✔
686
        nc.min_h_dem = np.nanmin(dem)
7✔
687

688

689
@entity_task(log, writes=['gridded_data', 'geometries'])
9✔
690
def glacier_masks(gdir):
9✔
691
    """Makes a gridded mask of the glacier outlines that can be used by OGGM.
692

693
    For a more robust solution (not OGGM compatible) see simple_glacier_masks.
694

695
    Parameters
696
    ----------
697
    gdir : :py:class:`oggm.GlacierDirectory`
698
        where to write the data
699
    """
700

701
    # In case nominal, just raise
702
    if gdir.is_nominal:
7✔
703
        raise GeometryError('{} is a nominal glacier.'.format(gdir.rgi_id))
1✔
704

705
    if not os.path.exists(gdir.get_filepath('gridded_data')):
7✔
706
        # In a possible future, we might actually want to raise a
707
        # deprecation warning here
708
        process_dem(gdir)
7✔
709

710
    # Geometries
711
    geometry = gdir.read_shapefile('outlines').geometry[0]
7✔
712

713
    # Interpolate shape to a regular path
714
    glacier_poly_hr = _interp_polygon(geometry, gdir.grid.dx)
7✔
715

716
    # Transform geometry into grid coordinates
717
    # It has to be in pix center coordinates because of how skimage works
718
    def proj(x, y):
7✔
719
        grid = gdir.grid.center_grid
7✔
720
        return grid.transform(x, y, crs=grid.proj)
7✔
721
    glacier_poly_hr = shapely.ops.transform(proj, glacier_poly_hr)
7✔
722

723
    # simple trick to correct invalid polys:
724
    # http://stackoverflow.com/questions/20833344/
725
    # fix-invalid-polygon-python-shapely
726
    glacier_poly_hr = glacier_poly_hr.buffer(0)
7✔
727
    if not glacier_poly_hr.is_valid:
7✔
728
        raise InvalidGeometryError('This glacier geometry is not valid.')
×
729

730
    # Rounded nearest pix
731
    glacier_poly_pix = _polygon_to_pix(glacier_poly_hr)
7✔
732
    if glacier_poly_pix.exterior is None:
7✔
733
        raise InvalidGeometryError('Problem in converting glacier geometry '
×
734
                                   'to grid resolution.')
735

736
    # Compute the glacier mask (currently: center pixels + touched)
737
    nx, ny = gdir.grid.nx, gdir.grid.ny
7✔
738
    glacier_mask = np.zeros((ny, nx), dtype=np.uint8)
7✔
739
    glacier_ext = np.zeros((ny, nx), dtype=np.uint8)
7✔
740
    (x, y) = glacier_poly_pix.exterior.xy
7✔
741
    glacier_mask[skdraw.polygon(np.array(y), np.array(x))] = 1
7✔
742
    for gint in glacier_poly_pix.interiors:
7✔
743
        x, y = tuple2int(gint.xy)
7✔
744
        glacier_mask[skdraw.polygon(y, x)] = 0
7✔
745
        glacier_mask[y, x] = 0  # on the nunataks, no
7✔
746
    x, y = tuple2int(glacier_poly_pix.exterior.xy)
7✔
747
    glacier_mask[y, x] = 1
7✔
748
    glacier_ext[y, x] = 1
7✔
749

750
    # Because of the 0 values at nunataks boundaries, some "Ice Islands"
751
    # can happen within nunataks (e.g.: RGI40-11.00062)
752
    # See if we can filter them out easily
753
    regions, nregions = label(glacier_mask, structure=label_struct)
7✔
754
    if nregions > 1:
7✔
755
        log.debug('(%s) we had to cut an island in the mask', gdir.rgi_id)
3✔
756
        # Check the size of those
757
        region_sizes = [np.sum(regions == r) for r in np.arange(1, nregions+1)]
3✔
758
        am = np.argmax(region_sizes)
3✔
759
        # Check not a strange glacier
760
        sr = region_sizes.pop(am)
3✔
761
        for ss in region_sizes:
3✔
762
            assert (ss / sr) < 0.1
3✔
763
        glacier_mask[:] = 0
3✔
764
        glacier_mask[np.where(regions == (am+1))] = 1
3✔
765

766
    # Write geometries
767
    geometries = dict()
7✔
768
    geometries['polygon_hr'] = glacier_poly_hr
7✔
769
    geometries['polygon_pix'] = glacier_poly_pix
7✔
770
    geometries['polygon_area'] = geometry.area
7✔
771
    gdir.write_pickle(geometries, 'geometries')
7✔
772

773
    # write out the grids in the netcdf file
774
    with GriddedNcdfFile(gdir) as nc:
7✔
775

776
        if 'glacier_mask' not in nc.variables:
7✔
777
            v = nc.createVariable('glacier_mask', 'i1', ('y', 'x', ),
7✔
778
                                  zlib=True)
779
            v.units = '-'
7✔
780
            v.long_name = 'Glacier mask'
7✔
781
        else:
782
            v = nc.variables['glacier_mask']
2✔
783
        v[:] = glacier_mask
7✔
784

785
        if 'glacier_ext' not in nc.variables:
7✔
786
            v = nc.createVariable('glacier_ext', 'i1', ('y', 'x', ),
7✔
787
                                  zlib=True)
788
            v.units = '-'
7✔
789
            v.long_name = 'Glacier external boundaries'
7✔
790
        else:
791
            v = nc.variables['glacier_ext']
2✔
792
        v[:] = glacier_ext
7✔
793

794
        dem = nc.variables['topo'][:]
7✔
795

796
        if 'topo_valid_mask' not in nc.variables:
7✔
797
            msg = ('You seem to be running from old preprocessed directories. '
×
798
                   'See https://github.com/OGGM/oggm/issues/1095 for a fix.')
799
            raise InvalidWorkflowError(msg)
×
800
        valid_mask = nc.variables['topo_valid_mask'][:]
7✔
801

802
        # Last sanity check based on the masked dem
803
        tmp_max = np.max(dem[np.where(glacier_mask == 1)])
7✔
804
        tmp_min = np.min(dem[np.where(glacier_mask == 1)])
7✔
805
        if tmp_max < (tmp_min + 0.1):
7✔
806
            raise InvalidDEMError('({}) min equal max in the masked DEM.'
×
807
                                  .format(gdir.rgi_id))
808

809
        # Log DEM that needed processing within the glacier mask
810
        if gdir.get_diagnostics().get('dem_needed_interpolation', False):
7✔
811
            pnan = (valid_mask == 0) & glacier_mask
1✔
812
            gdir.add_to_diagnostics('dem_invalid_perc_in_mask',
1✔
813
                                    np.sum(pnan) / np.sum(glacier_mask))
814

815
        # add some meta stats and close
816
        dem_on_g = dem[np.where(glacier_mask)]
7✔
817
        nc.max_h_glacier = np.nanmax(dem_on_g)
7✔
818
        nc.min_h_glacier = np.nanmin(dem_on_g)
7✔
819

820

821
@entity_task(log, writes=['gridded_data'])
9✔
822
def simple_glacier_masks(gdir):
9✔
823
    """Compute glacier masks based on much simpler rules than OGGM's default.
824

825
    This is therefore more robust: we use this task in a elev_bands workflow.
826

827
    Parameters
828
    ----------
829
    gdir : :py:class:`oggm.GlacierDirectory`
830
        where to write the data
831
    """
832

833
    # In case nominal, just raise
834
    if gdir.is_nominal:
4✔
835
        raise GeometryError('{} is a nominal glacier.'.format(gdir.rgi_id))
×
836

837
    if not os.path.exists(gdir.get_filepath('gridded_data')):
4✔
838
        # In a possible future, we might actually want to raise a
839
        # deprecation warning here
840
        process_dem(gdir)
4✔
841

842
    # Geometries
843
    geometry = gdir.read_shapefile('outlines').geometry[0]
4✔
844

845
    # rio metadata
846
    with rasterio.open(gdir.get_filepath('dem'), 'r', driver='GTiff') as ds:
4✔
847
        data = ds.read(1).astype(rasterio.float32)
4✔
848
        profile = ds.profile
4✔
849

850
    # simple trick to correct invalid polys:
851
    # http://stackoverflow.com/questions/20833344/
852
    # fix-invalid-polygon-python-shapely
853
    geometry = geometry.buffer(0)
4✔
854
    if not geometry.is_valid:
4✔
855
        raise InvalidDEMError('This glacier geometry is not valid.')
×
856

857
    # Compute the glacier mask using rasterio
858
    # Small detour as mask only accepts DataReader objects
859
    profile['dtype'] = 'int16'
4✔
860
    profile.pop('nodata', None)
4✔
861
    with rasterio.io.MemoryFile() as memfile:
4✔
862
        with memfile.open(**profile) as dataset:
4✔
863
            dataset.write(data.astype(np.int16)[np.newaxis, ...])
4✔
864
        dem_data = rasterio.open(memfile.name)
4✔
865
        masked_dem, _ = riomask(dem_data, [shpg.mapping(geometry)],
4✔
866
                                filled=False)
867
    glacier_mask = ~masked_dem[0, ...].mask
4✔
868

869
    # Same without nunataks
870
    with rasterio.io.MemoryFile() as memfile:
4✔
871
        with memfile.open(**profile) as dataset:
4✔
872
            dataset.write(data.astype(np.int16)[np.newaxis, ...])
4✔
873
        dem_data = rasterio.open(memfile.name)
4✔
874
        try:
4✔
875
            poly = [shpg.mapping(shpg.Polygon(geometry.exterior))]
4✔
876
        except AttributeError:
×
877
            if not cfg.PARAMS['keep_multipolygon_outlines']:
×
878
                raise
×
879
            # special treatment for MultiPolygons
880
            parts = []
×
881
            for p in geometry.geoms:
×
882
                parts.append(p)
×
883
            parts = np.array(parts)
×
884

885
            poly = []
×
886
            for part in parts:
×
887
                poly.append(shpg.mapping(shpg.Polygon(part.exterior)))
×
888

889
        masked_dem, _ = riomask(dem_data, poly,
4✔
890
                                filled=False)
891
    glacier_mask_nonuna = ~masked_dem[0, ...].mask
4✔
892

893
    # Glacier exterior excluding nunataks
894
    erode = binary_erosion(glacier_mask_nonuna)
4✔
895
    glacier_ext = glacier_mask_nonuna ^ erode
4✔
896
    glacier_ext = np.where(glacier_mask_nonuna, glacier_ext, 0)
4✔
897

898
    dem = read_geotiff_dem(gdir)
4✔
899

900
    # Last sanity check based on the masked dem
901
    tmp_max = np.nanmax(dem[glacier_mask])
4✔
902
    tmp_min = np.nanmin(dem[glacier_mask])
4✔
903
    if tmp_max < (tmp_min + 1):
4✔
904
        raise InvalidDEMError('({}) min equal max in the masked DEM.'
×
905
                              .format(gdir.rgi_id))
906

907
    # write out the grids in the netcdf file
908
    with GriddedNcdfFile(gdir) as nc:
4✔
909

910
        if 'glacier_mask' not in nc.variables:
4✔
911
            v = nc.createVariable('glacier_mask', 'i1', ('y', 'x', ),
4✔
912
                                  zlib=True)
913
            v.units = '-'
4✔
914
            v.long_name = 'Glacier mask'
4✔
915
        else:
916
            v = nc.variables['glacier_mask']
1✔
917
        v[:] = glacier_mask
4✔
918

919
        if 'glacier_ext' not in nc.variables:
4✔
920
            v = nc.createVariable('glacier_ext', 'i1', ('y', 'x', ),
4✔
921
                                  zlib=True)
922
            v.units = '-'
4✔
923
            v.long_name = 'Glacier external boundaries'
4✔
924
        else:
925
            v = nc.variables['glacier_ext']
1✔
926
        v[:] = glacier_ext
4✔
927

928
        # Log DEM that needed processing within the glacier mask
929
        if 'topo_valid_mask' not in nc.variables:
4✔
930
            msg = ('You seem to be running from old preprocessed directories. '
×
931
                   'See https://github.com/OGGM/oggm/issues/1095 for a fix.')
932
            raise InvalidWorkflowError(msg)
×
933
        valid_mask = nc.variables['topo_valid_mask'][:]
4✔
934
        if gdir.get_diagnostics().get('dem_needed_interpolation', False):
4✔
935
            pnan = (valid_mask == 0) & glacier_mask
×
936
            gdir.add_to_diagnostics('dem_invalid_perc_in_mask',
×
937
                                    np.sum(pnan) / np.sum(glacier_mask))
938

939
        # add some meta stats and close
940
        nc.max_h_dem = np.nanmax(dem)
4✔
941
        nc.min_h_dem = np.nanmin(dem)
4✔
942
        dem_on_g = dem[np.where(glacier_mask)]
4✔
943
        nc.max_h_glacier = np.nanmax(dem_on_g)
4✔
944
        nc.min_h_glacier = np.nanmin(dem_on_g)
4✔
945

946
        # Last sanity check
947
        if nc.max_h_glacier < (nc.min_h_glacier + 1):
4✔
948
            raise InvalidDEMError('({}) min equal max in the masked DEM.'
×
949
                                  .format(gdir.rgi_id))
950

951

952
@entity_task(log, writes=['hypsometry'])
9✔
953
def compute_hypsometry_attributes(gdir, min_perc=0.2):
9✔
954
    """Adds some attributes to the glacier directory.
955

956
    Mostly useful for RGI stuff.
957

958
    Parameters
959
    ----------
960
    gdir : :py:class:`oggm.GlacierDirectory`
961
        where to write the data
962
    """
963
    dem = read_geotiff_dem(gdir)
1✔
964

965
    # This is the very robust way
966
    fp = gdir.get_filepath('glacier_mask')
1✔
967
    with rasterio.open(fp, 'r', driver='GTiff') as ds:
1✔
968
        glacier_mask = ds.read(1).astype(rasterio.int16) == 1
1✔
969

970
    fp = gdir.get_filepath('glacier_mask', filesuffix='_exterior')
1✔
971
    with rasterio.open(fp, 'r', driver='GTiff') as ds:
1✔
972
        glacier_exterior_mask = ds.read(1).astype(rasterio.int16) == 1
1✔
973

974
    valid_mask = glacier_mask & np.isfinite(dem)
1✔
975
    # we cant proceed if we have very little info
976
    avail_perc = valid_mask.sum() / glacier_mask.sum()
1✔
977
    if avail_perc < min_perc:
1✔
978
        raise InvalidDEMError(f"Cant proceed with {avail_perc*100:.1f}%"
×
979
                              f"available.")
980

981
    bsize = 50.
1✔
982
    dem_on_ice = dem[valid_mask]
1✔
983
    if dem_on_ice.min() < -99:
1✔
984
        raise InvalidDEMError(f"Cant proceed with {dem_on_ice.min()}m "
×
985
                              f"minimum DEM elevation.")
986
    bins = np.arange(nicenumber(dem_on_ice.min(), bsize, lower=True),
1✔
987
                     nicenumber(dem_on_ice.max(), bsize) + 0.01, bsize)
988

989
    h, _ = np.histogram(dem_on_ice, bins)
1✔
990
    h = h / np.sum(h) * 1000  # in permil
1✔
991

992
    # We want to convert the bins to ints but preserve their sum to 1000
993
    # Start with everything rounded down, then round up the numbers with the
994
    # highest fractional parts until the desired sum is reached.
995
    hi = np.floor(h).astype(int)
1✔
996
    hup = np.ceil(h).astype(int)
1✔
997
    aso = np.argsort(hup - h)
1✔
998
    for i in aso:
1✔
999
        hi[i] = hup[i]
1✔
1000
        if np.sum(hi) == 1000:
1✔
1001
            break
1✔
1002

1003
    # slope
1004
    sy, sx = np.gradient(dem, gdir.grid.dx)
1✔
1005
    aspect = np.arctan2(np.nanmean(-sx[glacier_mask]), np.nanmean(sy[glacier_mask]))
1✔
1006
    aspect = np.rad2deg(aspect)
1✔
1007
    if aspect < 0:
1✔
1008
        aspect += 360
×
1009
    slope = np.arctan(np.sqrt(sx ** 2 + sy ** 2))
1✔
1010
    avg_slope = np.rad2deg(np.nanmean(slope[glacier_mask]))
1✔
1011

1012
    sec_bins = -22.5 + 45 * np.arange(9)
1✔
1013
    aspect_for_bin = aspect
1✔
1014
    if aspect_for_bin >= sec_bins[-1]:
1✔
1015
        aspect_for_bin -= 360
×
1016
    aspect_sec = np.digitize(aspect_for_bin, sec_bins)
1✔
1017
    dx2 =gdir.grid.dx**2 * 1e-6
1✔
1018

1019
    # Terminus loc
1020
    j, i = np.nonzero((dem[glacier_exterior_mask].min() == dem) & glacier_exterior_mask)
1✔
1021
    if len(j) > 2:
1✔
1022
        # We have a situation - take the closest to the euclidian center
1023
        mi, mj = np.mean(i), np.mean(j)
×
1024
        c = np.argmin((mi - i)**2 + (mj - j)**2)
×
1025
        j, i = j[[c]], i[[c]]
×
1026
    lon, lat = gdir.grid.ij_to_crs(i[0], j[0], crs=salem.wgs84)
1✔
1027

1028
    # write
1029
    df = pd.DataFrame()
1✔
1030
    df['rgi_id'] = [gdir.rgi_id]
1✔
1031
    df['area_km2'] = [gdir.rgi_area_km2]
1✔
1032
    df['area_grid_km2'] = [glacier_mask.sum() * dx2]
1✔
1033
    df['valid_dem_perc'] = [avail_perc]
1✔
1034
    df['grid_dx'] = [gdir.grid.dx]
1✔
1035
    df['zmin_m'] = [np.nanmin(dem_on_ice)]
1✔
1036
    df['zmax_m'] = [np.nanmax(dem_on_ice)]
1✔
1037
    df['zmed_m'] = [np.nanmedian(dem_on_ice)]
1✔
1038
    df['zmean_m'] = [np.nanmean(dem_on_ice)]
1✔
1039
    df['terminus_lon'] = lon
1✔
1040
    df['terminus_lat'] = lat
1✔
1041
    df['slope_deg'] = [avg_slope]
1✔
1042
    df['aspect_deg'] = [aspect]
1✔
1043
    df['aspect_sec'] = [aspect_sec]
1✔
1044
    df['dem_source'] = [gdir.get_diagnostics()['dem_source']]
1✔
1045
    for b, bs in zip(hi, (bins[1:] + bins[:-1])/2):
1✔
1046
        df['{}'.format(np.round(bs).astype(int))] = [b]
1✔
1047
    df.to_csv(gdir.get_filepath('hypsometry'), index=False)
1✔
1048

1049

1050
@entity_task(log, writes=['glacier_mask'])
9✔
1051
def rasterio_glacier_mask(gdir, source=None, no_nunataks=False, overwrite=True):
9✔
1052
    """Writes a 1-0 glacier mask GeoTiff with the same dimensions as dem.tif
1053

1054
    If no_nunataks, does the same but without nunataks. Writes a file
1055
    with the suffix "_no_nunataks" appended.
1056

1057

1058
    Parameters
1059
    ----------
1060
    gdir : :py:class:`oggm.GlacierDirectory`
1061
        the glacier in question
1062
    source : str
1063

1064
        - None (default): the task reads `dem.tif` from the GDir root
1065
        - 'ALL': try to open any folder from `utils.DEM_SOURCE` and use first
1066
        - any of `utils.DEM_SOURCE`: try only that one
1067
    overwrite : bool
1068
        compute even if the file is already there
1069
    """
1070
    # No need to if already there
1071
    filesuffix = '_no_nunataks' if no_nunataks else None
1✔
1072
    if not overwrite and gdir.has_file('glacier_mask', filesuffix=filesuffix):
1✔
1073
        return
×
1074

1075
    if source is None:
1✔
1076
        dempath = gdir.get_filepath('dem')
1✔
1077
    elif source in utils.DEM_SOURCES:
1✔
1078
        dempath = os.path.join(gdir.dir, source, 'dem.tif')
1✔
1079
    else:
1080
        for src in utils.DEM_SOURCES:
×
1081
            dempath = os.path.join(gdir.dir, src, 'dem.tif')
×
1082
            if os.path.isfile(dempath):
×
1083
                break
×
1084

1085
    if not os.path.isfile(dempath):
1✔
1086
        raise ValueError('The specified source does not give a valid DEM file')
1✔
1087

1088
    # read dem profile
1089
    with rasterio.open(dempath, 'r', driver='GTiff') as ds:
1✔
1090
        profile = ds.profile
1✔
1091
        # don't even bother reading the actual DEM, just mimic it
1092
        data = np.zeros((ds.height, ds.width))
1✔
1093

1094
    # Read RGI outlines
1095
    geometry = gdir.read_shapefile('outlines').geometry[0]
1✔
1096

1097
    # simple trick to correct invalid polys:
1098
    # http://stackoverflow.com/questions/20833344/
1099
    # fix-invalid-polygon-python-shapely
1100
    geometry = geometry.buffer(0)
1✔
1101
    if not geometry.is_valid:
1✔
1102
        raise InvalidDEMError('This glacier geometry is not valid.')
×
1103

1104
    if no_nunataks:
1✔
1105
        mapping = shpg.mapping(shpg.Polygon(geometry.exterior))
1✔
1106
    else:
1107
        mapping = shpg.mapping(geometry)
1✔
1108

1109
    # Compute the glacier mask using rasterio
1110
    # Small detour as mask only accepts DataReader objects
1111
    with rasterio.io.MemoryFile() as memfile:
1✔
1112
        with memfile.open(**profile) as dataset:
1✔
1113
            dataset.write(data.astype(profile['dtype'])[np.newaxis, ...])
1✔
1114
        dem_data = rasterio.open(memfile.name)
1✔
1115
        masked_dem, _ = riomask(dem_data, [mapping], filled=False)
1✔
1116
    glacier_mask = ~masked_dem[0, ...].mask
1✔
1117

1118
    # parameters to for the new tif
1119
    nodata = -32767
1✔
1120
    dtype = rasterio.int16
1✔
1121

1122
    # let's use integer
1123
    out = glacier_mask.astype(dtype)
1✔
1124

1125
    # and check for sanity
1126
    if not np.all(np.unique(out) == np.array([0, 1])):
1✔
1127
        raise InvalidDEMError('({}) masked DEM does not consist of 0/1 only.'
×
1128
                              .format(gdir.rgi_id))
1129

1130
    # Update existing profile for output
1131
    profile.update({
1✔
1132
        'dtype': dtype,
1133
        'nodata': nodata,
1134
    })
1135

1136
    if no_nunataks:
1✔
1137
        fp = gdir.get_filepath('glacier_mask', filesuffix='_no_nunataks')
1✔
1138
    else:
1139
        fp = gdir.get_filepath('glacier_mask')
1✔
1140

1141
    with rasterio.open(fp, 'w', **profile) as r:
1✔
1142
        r.write(out.astype(dtype), 1)
1✔
1143

1144

1145
@entity_task(log, writes=['glacier_mask'])
9✔
1146
def rasterio_glacier_exterior_mask(gdir, overwrite=True):
9✔
1147
    """Writes a 1-0 glacier exterior mask GeoTiff with the same dimensions as dem.tif
1148

1149
    This is the "one" grid point on the glacier exterior (ignoring nunataks).
1150
    This is useful to know where the terminus might be, for example.
1151

1152
    Parameters
1153
    ----------
1154
    gdir : :py:class:`oggm.GlacierDirectory`
1155
        the glacier in question
1156
    overwrite : bool
1157
        compute even if the file is already there
1158
    """
1159

1160
    # No need to if already there
1161
    if not overwrite and gdir.has_file('glacier_mask', filesuffix='_exterior'):
1✔
1162
        return
×
1163

1164
    fp = gdir.get_filepath('glacier_mask', filesuffix='_no_nunataks')
1✔
1165
    with rasterio.open(fp, 'r', driver='GTiff') as ds:
1✔
1166
        glacier_mask_nonuna = ds.read(1).astype(rasterio.int16) == 1
1✔
1167
        profile = ds.profile
1✔
1168

1169
    # Glacier exterior excluding nunataks
1170
    erode = binary_erosion(glacier_mask_nonuna)
1✔
1171
    glacier_ext = glacier_mask_nonuna ^ erode
1✔
1172
    glacier_ext = np.where(glacier_mask_nonuna, glacier_ext, 0)
1✔
1173

1174
    # parameters to for the new tif
1175
    fp = gdir.get_filepath('glacier_mask', filesuffix='_exterior')
1✔
1176
    with rasterio.open(fp, 'w', **profile) as r:
1✔
1177
        r.write(glacier_ext.astype(rasterio.int16), 1)
1✔
1178

1179

1180
@entity_task(log, writes=['gridded_data'])
9✔
1181
def gridded_attributes(gdir):
9✔
1182
    """Adds attributes to the gridded file, useful for thickness interpolation.
1183

1184
    This could be useful for distributed ice thickness models.
1185
    The raster data are added to the gridded_data file.
1186

1187
    Parameters
1188
    ----------
1189
    gdir : :py:class:`oggm.GlacierDirectory`
1190
        where to write the data
1191
    """
1192

1193
    # Variables
1194
    grids_file = gdir.get_filepath('gridded_data')
5✔
1195
    with ncDataset(grids_file) as nc:
5✔
1196
        topo_smoothed = nc.variables['topo_smoothed'][:]
5✔
1197
        glacier_mask = nc.variables['glacier_mask'][:]
5✔
1198

1199
    # Glacier exterior including nunataks
1200
    erode = binary_erosion(glacier_mask)
5✔
1201
    glacier_ext = glacier_mask ^ erode
5✔
1202
    glacier_ext = np.where(glacier_mask == 1, glacier_ext, 0)
5✔
1203

1204
    # Intersects between glaciers
1205
    gdfi = gpd.GeoDataFrame(columns=['geometry'])
5✔
1206
    if gdir.has_file('intersects'):
5✔
1207
        # read and transform to grid
1208
        gdf = gdir.read_shapefile('intersects')
4✔
1209
        salem.transform_geopandas(gdf, to_crs=gdir.grid, inplace=True)
4✔
1210
        gdfi = pd.concat([gdfi, gdf[['geometry']]])
4✔
1211

1212
    # Ice divide mask
1213
    # Probably not the fastest way to do this, but it works
1214
    dist = np.array([])
5✔
1215
    jj, ii = np.where(glacier_ext)
5✔
1216
    for j, i in zip(jj, ii):
5✔
1217
        dist = np.append(dist, np.min(gdfi.distance(shpg.Point(i, j))))
5✔
1218

1219
    with warnings.catch_warnings():
5✔
1220
        warnings.filterwarnings("ignore", category=RuntimeWarning)
5✔
1221
        pok = np.where(dist <= 1)
5✔
1222
    glacier_ext_intersect = glacier_ext * 0
5✔
1223
    glacier_ext_intersect[jj[pok], ii[pok]] = 1
5✔
1224

1225
    # Distance from border mask - Scipy does the job
1226
    dx = gdir.grid.dx
5✔
1227
    dis_from_border = 1 + glacier_ext_intersect - glacier_ext
5✔
1228
    dis_from_border = distance_transform_edt(dis_from_border) * dx
5✔
1229

1230
    # Slope
1231
    glen_n = cfg.PARAMS['glen_n']
5✔
1232
    sy, sx = np.gradient(topo_smoothed, dx, dx)
5✔
1233
    slope = np.arctan(np.sqrt(sy**2 + sx**2))
5✔
1234
    min_slope = np.deg2rad(cfg.PARAMS['distributed_inversion_min_slope'])
5✔
1235
    slope_factor = utils.clip_array(slope, min_slope, np.pi/2)
5✔
1236
    slope_factor = 1 / slope_factor**(glen_n / (glen_n+2))
5✔
1237

1238
    aspect = np.arctan2(-sx, sy)
5✔
1239
    aspect[aspect < 0] += 2 * np.pi
5✔
1240

1241
    with ncDataset(grids_file, 'a') as nc:
5✔
1242

1243
        vn = 'glacier_ext_erosion'
5✔
1244
        if vn in nc.variables:
5✔
1245
            v = nc.variables[vn]
1✔
1246
        else:
1247
            v = nc.createVariable(vn, 'i1', ('y', 'x', ))
5✔
1248
        v.units = '-'
5✔
1249
        v.long_name = 'Glacier exterior with binary erosion method'
5✔
1250
        v[:] = glacier_ext
5✔
1251

1252
        vn = 'ice_divides'
5✔
1253
        if vn in nc.variables:
5✔
1254
            v = nc.variables[vn]
1✔
1255
        else:
1256
            v = nc.createVariable(vn, 'i1', ('y', 'x', ))
5✔
1257
        v.units = '-'
5✔
1258
        v.long_name = 'Glacier ice divides'
5✔
1259
        v[:] = glacier_ext_intersect
5✔
1260

1261
        vn = 'slope'
5✔
1262
        if vn in nc.variables:
5✔
1263
            v = nc.variables[vn]
1✔
1264
        else:
1265
            v = nc.createVariable(vn, 'f4', ('y', 'x', ))
5✔
1266
        v.units = 'rad'
5✔
1267
        v.long_name = 'Local slope based on smoothed topography'
5✔
1268
        v[:] = slope
5✔
1269

1270
        vn = 'aspect'
5✔
1271
        if vn in nc.variables:
5✔
1272
            v = nc.variables[vn]
1✔
1273
        else:
1274
            v = nc.createVariable(vn, 'f4', ('y', 'x', ))
5✔
1275
        v.units = 'rad'
5✔
1276
        v.long_name = 'Local aspect based on smoothed topography'
5✔
1277
        v[:] = aspect
5✔
1278

1279
        vn = 'slope_factor'
5✔
1280
        if vn in nc.variables:
5✔
1281
            v = nc.variables[vn]
1✔
1282
        else:
1283
            v = nc.createVariable(vn, 'f4', ('y', 'x', ))
5✔
1284
        v.units = '-'
5✔
1285
        v.long_name = 'Slope factor as defined in Farinotti et al 2009'
5✔
1286
        v[:] = slope_factor
5✔
1287

1288
        vn = 'dis_from_border'
5✔
1289
        if vn in nc.variables:
5✔
1290
            v = nc.variables[vn]
1✔
1291
        else:
1292
            v = nc.createVariable(vn, 'f4', ('y', 'x', ))
5✔
1293
        v.units = 'm'
5✔
1294
        v.long_name = 'Distance from glacier boundaries'
5✔
1295
        v[:] = dis_from_border
5✔
1296

1297

1298
def _all_inflows(cls, cl):
9✔
1299
    """Find all centerlines flowing into the centerline examined.
1300

1301
    Parameters
1302
    ----------
1303
    cls : list
1304
        all centerlines of the examined glacier
1305
    cline : Centerline
1306
        centerline to control
1307

1308
    Returns
1309
    -------
1310
    list of strings of centerlines
1311
    """
1312

1313
    ixs = [str(cls.index(cl.inflows[i])) for i in range(len(cl.inflows))]
×
1314
    for cl in cl.inflows:
×
1315
        ixs.extend(_all_inflows(cls, cl))
×
1316
    return ixs
×
1317

1318

1319
@entity_task(log)
9✔
1320
def gridded_mb_attributes(gdir):
9✔
1321
    """Adds mass balance related attributes to the gridded data file.
1322

1323
    This could be useful for distributed ice thickness models.
1324
    The raster data are added to the gridded_data file.
1325

1326
    Parameters
1327
    ----------
1328
    gdir : :py:class:`oggm.GlacierDirectory`
1329
        where to write the data
1330
    """
1331
    from oggm.core.massbalance import LinearMassBalance, ConstantMassBalance
1✔
1332
    from oggm.core.centerlines import line_inflows
1✔
1333

1334
    # Get the input data
1335
    with ncDataset(gdir.get_filepath('gridded_data')) as nc:
1✔
1336
        topo_2d = nc.variables['topo_smoothed'][:]
1✔
1337
        glacier_mask_2d = nc.variables['glacier_mask'][:]
1✔
1338
        glacier_mask_2d = glacier_mask_2d == 1
1✔
1339
        catchment_mask_2d = glacier_mask_2d * np.NaN
1✔
1340

1341
    topo = topo_2d[glacier_mask_2d]
1✔
1342

1343
    # Prepare the distributed mass balance data
1344
    rho = cfg.PARAMS['ice_density']
1✔
1345
    dx2 = gdir.grid.dx ** 2
1✔
1346

1347
    # Linear
1348
    def to_minimize(ela_h):
1✔
1349
        mbmod = LinearMassBalance(ela_h[0])
1✔
1350
        smb = mbmod.get_annual_mb(heights=topo)
1✔
1351
        return np.sum(smb)**2
1✔
1352
    ela_h = optimization.minimize(to_minimize, [0.], method='Powell')
1✔
1353
    mbmod = LinearMassBalance(float(ela_h['x']))
1✔
1354
    lin_mb_on_z = mbmod.get_annual_mb(heights=topo) * cfg.SEC_IN_YEAR * rho
1✔
1355
    if not np.isclose(np.sum(lin_mb_on_z), 0, atol=10):
1✔
1356
        raise RuntimeError('Spec mass balance should be zero but is: {}'
×
1357
                           .format(np.sum(lin_mb_on_z)))
1358

1359
    # Normal OGGM (a bit tweaked)
1360
    def to_minimize(temp_bias):
1✔
1361
        mbmod = ConstantMassBalance(gdir, temp_bias=temp_bias, y0=1995,
1✔
1362
                                    check_calib_params=False)
1363
        smb = mbmod.get_annual_mb(heights=topo)
1✔
1364
        return np.sum(smb)**2
1✔
1365
    opt = optimization.minimize(to_minimize, [0.], method='Powell')
1✔
1366
    mbmod = ConstantMassBalance(gdir, temp_bias=float(opt['x']), y0=1995,
1✔
1367
                                check_calib_params=False)
1368
    oggm_mb_on_z = mbmod.get_annual_mb(heights=topo) * cfg.SEC_IN_YEAR * rho
1✔
1369
    if not np.isclose(np.sum(oggm_mb_on_z), 0, atol=10):
1✔
1370
        raise RuntimeError('Spec mass balance should be zero but is: {}'
×
1371
                           .format(np.sum(oggm_mb_on_z)))
1372

1373
    # Altitude based mass balance
1374
    catch_area_above_z = topo * np.NaN
1✔
1375
    lin_mb_above_z = topo * np.NaN
1✔
1376
    oggm_mb_above_z = topo * np.NaN
1✔
1377
    for i, h in enumerate(topo):
1✔
1378
        catch_area_above_z[i] = np.sum(topo >= h) * dx2
1✔
1379
        lin_mb_above_z[i] = np.sum(lin_mb_on_z[topo >= h]) * dx2
1✔
1380
        oggm_mb_above_z[i] = np.sum(oggm_mb_on_z[topo >= h]) * dx2
1✔
1381

1382
    # Make 2D again
1383
    def _fill_2d_like(data):
1✔
1384
        out = topo_2d * np.NaN
1✔
1385
        out[glacier_mask_2d] = data
1✔
1386
        return out
1✔
1387

1388
    catch_area_above_z = _fill_2d_like(catch_area_above_z)
1✔
1389
    lin_mb_above_z = _fill_2d_like(lin_mb_above_z)
1✔
1390
    oggm_mb_above_z = _fill_2d_like(oggm_mb_above_z)
1✔
1391

1392
    # Save to file
1393
    with ncDataset(gdir.get_filepath('gridded_data'), 'a') as nc:
1✔
1394

1395
        vn = 'catchment_area'
1✔
1396
        if vn in nc.variables:
1✔
1397
            v = nc.variables[vn]
×
1398
        else:
1399
            v = nc.createVariable(vn, 'f4', ('y', 'x',))
1✔
1400
        v.units = 'm^2'
1✔
1401
        v.long_name = 'Catchment area above point'
1✔
1402
        v.description = ('This is a very crude method: just the area above '
1✔
1403
                         'the points elevation on glacier.')
1404
        v[:] = catch_area_above_z
1✔
1405

1406
        vn = 'lin_mb_above_z'
1✔
1407
        if vn in nc.variables:
1✔
1408
            v = nc.variables[vn]
×
1409
        else:
1410
            v = nc.createVariable(vn, 'f4', ('y', 'x',))
1✔
1411
        v.units = 'kg/year'
1✔
1412
        v.long_name = 'MB above point from linear MB model, without catchments'
1✔
1413
        v.description = ('Mass balance cumulated above the altitude of the'
1✔
1414
                         'point, hence in unit of flux. Note that it is '
1415
                         'a coarse approximation of the real flux. '
1416
                         'The mass balance model is a simple linear function'
1417
                         'of altitude.')
1418
        v[:] = lin_mb_above_z
1✔
1419

1420
        vn = 'oggm_mb_above_z'
1✔
1421
        if vn in nc.variables:
1✔
1422
            v = nc.variables[vn]
×
1423
        else:
1424
            v = nc.createVariable(vn, 'f4', ('y', 'x',))
1✔
1425
        v.units = 'kg/year'
1✔
1426
        v.long_name = 'MB above point from OGGM MB model, without catchments'
1✔
1427
        v.description = ('Mass balance cumulated above the altitude of the'
1✔
1428
                         'point, hence in unit of flux. Note that it is '
1429
                         'a coarse approximation of the real flux. '
1430
                         'The mass balance model is a calibrated temperature '
1431
                         'index model like OGGM.')
1432
        v[:] = oggm_mb_above_z
1✔
1433

1434
    # Hardest part - MB per catchment
1435
    try:
1✔
1436
        cls = gdir.read_pickle('centerlines')
1✔
1437
    except FileNotFoundError:
×
1438
        return
×
1439

1440
    # Make everything we need flat
1441
    # Catchment areas
1442
    cis = gdir.read_pickle('geometries')['catchment_indices']
1✔
1443
    for j, ci in enumerate(cis):
1✔
1444
        catchment_mask_2d[tuple(ci.T)] = j
1✔
1445

1446
    catchment_mask = catchment_mask_2d[glacier_mask_2d].astype(int)
1✔
1447

1448
    catchment_area = topo * np.NaN
1✔
1449
    lin_mb_above_z_on_catch = topo * np.NaN
1✔
1450
    oggm_mb_above_z_on_catch = topo * np.NaN
1✔
1451

1452
    # First, find all inflows indices and min altitude per catchment
1453
    inflows = []
1✔
1454
    lowest_h = []
1✔
1455
    for i, cl in enumerate(cls):
1✔
1456
        lowest_h.append(np.min(topo[catchment_mask == i]))
1✔
1457
        inflows.append([cls.index(l) for l in line_inflows(cl, keep=False)])
1✔
1458

1459
    for i, (catch_id, h) in enumerate(zip(catchment_mask, topo)):
1✔
1460

1461
        if h == np.min(topo):
1✔
1462
            t = 1
1✔
1463

1464
        # Find the catchment area of the point itself by eliminating points
1465
        # below the point altitude. We assume we keep all of them first,
1466
        # then remove those we don't want
1467
        sel_catchs = inflows[catch_id].copy()
1✔
1468
        for catch in inflows[catch_id]:
1✔
1469
            if h >= lowest_h[catch]:
1✔
1470
                for cc in np.append(inflows[catch], catch):
1✔
1471
                    try:
1✔
1472
                        sel_catchs.remove(cc)
1✔
1473
                    except ValueError:
×
1474
                        pass
×
1475

1476
        # At the very least we need or own catchment
1477
        sel_catchs.append(catch_id)
1✔
1478

1479
        # Then select all the catchment points
1480
        sel_points = np.isin(catchment_mask, sel_catchs)
1✔
1481

1482
        # And keep the ones above our altitude
1483
        sel_points = sel_points & (topo >= h)
1✔
1484

1485
        # Compute
1486
        lin_mb_above_z_on_catch[i] = np.sum(lin_mb_on_z[sel_points]) * dx2
1✔
1487
        oggm_mb_above_z_on_catch[i] = np.sum(oggm_mb_on_z[sel_points]) * dx2
1✔
1488
        catchment_area[i] = np.sum(sel_points) * dx2
1✔
1489

1490
    catchment_area = _fill_2d_like(catchment_area)
1✔
1491
    lin_mb_above_z_on_catch = _fill_2d_like(lin_mb_above_z_on_catch)
1✔
1492
    oggm_mb_above_z_on_catch = _fill_2d_like(oggm_mb_above_z_on_catch)
1✔
1493

1494
    # Save to file
1495
    with ncDataset(gdir.get_filepath('gridded_data'), 'a') as nc:
1✔
1496
        vn = 'catchment_area_on_catch'
1✔
1497
        if vn in nc.variables:
1✔
1498
            v = nc.variables[vn]
×
1499
        else:
1500
            v = nc.createVariable(vn, 'f4', ('y', 'x',))
1✔
1501
        v.units = 'm^2'
1✔
1502
        v.long_name = 'Catchment area above point on flowline catchments'
1✔
1503
        v.description = ('Uses the catchments masks of the flowlines to '
1✔
1504
                         'compute the area above the altitude of the given '
1505
                         'point.')
1506
        v[:] = catchment_area
1✔
1507

1508
        vn = 'lin_mb_above_z_on_catch'
1✔
1509
        if vn in nc.variables:
1✔
1510
            v = nc.variables[vn]
×
1511
        else:
1512
            v = nc.createVariable(vn, 'f4', ('y', 'x', ))
1✔
1513
        v.units = 'kg/year'
1✔
1514
        v.long_name = 'MB above point from linear MB model, with catchments'
1✔
1515
        v.description = ('Mass balance cumulated above the altitude of the'
1✔
1516
                         'point in a flowline catchment, hence in unit of '
1517
                         'flux. Note that it is a coarse approximation of the '
1518
                         'real flux. The mass balance model is a simple '
1519
                         'linear function of altitude.')
1520
        v[:] = lin_mb_above_z_on_catch
1✔
1521

1522
        vn = 'oggm_mb_above_z_on_catch'
1✔
1523
        if vn in nc.variables:
1✔
1524
            v = nc.variables[vn]
×
1525
        else:
1526
            v = nc.createVariable(vn, 'f4', ('y', 'x', ))
1✔
1527
        v.units = 'kg/year'
1✔
1528
        v.long_name = 'MB above point from OGGM MB model, with catchments'
1✔
1529
        v.description = ('Mass balance cumulated above the altitude of the'
1✔
1530
                         'point in a flowline catchment, hence in unit of '
1531
                         'flux. Note that it is a coarse approximation of the '
1532
                         'real flux. The mass balance model is a calibrated '
1533
                         'temperature index model like OGGM.')
1534
        v[:] = oggm_mb_above_z_on_catch
1✔
1535

1536

1537
def merged_glacier_masks(gdir, geometry):
9✔
1538
    """Makes a gridded mask of a merged glacier outlines.
1539

1540
    This is a simplified version of glacier_masks. We don't need fancy
1541
    corrections or smoothing here: The flowlines for the actual model run are
1542
    based on a proper call of glacier_masks.
1543

1544
    This task is only to get outlines etc. for visualization!
1545

1546
    Parameters
1547
    ----------
1548
    gdir : :py:class:`oggm.GlacierDirectory`
1549
        where to write the data
1550
    geometry: shapely.geometry.multipolygon.MultiPolygon
1551
        united outlines of the merged glaciers
1552
    """
1553

1554
    # open srtm tif-file:
1555
    dem = read_geotiff_dem(gdir)
×
1556

1557
    if np.min(dem) == np.max(dem):
×
1558
        raise RuntimeError('({}) min equal max in the DEM.'
×
1559
                           .format(gdir.rgi_id))
1560

1561
    # Clip topography to 0 m a.s.l.
1562
    utils.clip_min(dem, 0, out=dem)
×
1563

1564
    # Interpolate shape to a regular path
1565
    glacier_poly_hr = tolist(geometry)
×
1566

1567
    for nr, poly in enumerate(glacier_poly_hr):
×
1568
        # transform geometry to map
1569
        _geometry = salem.transform_geometry(poly, to_crs=gdir.grid.proj)
×
1570
        glacier_poly_hr[nr] = _interp_polygon(_geometry, gdir.grid.dx)
×
1571

1572
    glacier_poly_hr = shpg.MultiPolygon(glacier_poly_hr)
×
1573

1574
    # Transform geometry into grid coordinates
1575
    # It has to be in pix center coordinates because of how skimage works
1576
    def proj(x, y):
×
1577
        grid = gdir.grid.center_grid
×
1578
        return grid.transform(x, y, crs=grid.proj)
×
1579

1580
    glacier_poly_hr = shapely.ops.transform(proj, glacier_poly_hr)
×
1581

1582
    # simple trick to correct invalid polys:
1583
    # http://stackoverflow.com/questions/20833344/
1584
    # fix-invalid-polygon-python-shapely
1585
    glacier_poly_hr = glacier_poly_hr.buffer(0)
×
1586
    if not glacier_poly_hr.is_valid:
×
1587
        raise RuntimeError('This glacier geometry is not valid.')
×
1588

1589
    # Rounded geometry to nearest pix
1590
    # I can not use _polyg
1591
    # glacier_poly_pix = _polygon_to_pix(glacier_poly_hr)
1592
    def project(x, y):
×
1593
        return np.rint(x).astype(np.int64), np.rint(y).astype(np.int64)
×
1594

1595
    glacier_poly_pix = shapely.ops.transform(project, glacier_poly_hr)
×
1596
    glacier_poly_pix_iter = tolist(glacier_poly_pix)
×
1597

1598
    # Compute the glacier mask (currently: center pixels + touched)
1599
    nx, ny = gdir.grid.nx, gdir.grid.ny
×
1600
    glacier_mask = np.zeros((ny, nx), dtype=np.uint8)
×
1601
    glacier_ext = np.zeros((ny, nx), dtype=np.uint8)
×
1602

1603
    for poly in glacier_poly_pix_iter:
×
1604
        (x, y) = poly.exterior.xy
×
1605
        glacier_mask[skdraw.polygon(np.array(y), np.array(x))] = 1
×
1606
        for gint in poly.interiors:
×
1607
            x, y = tuple2int(gint.xy)
×
1608
            glacier_mask[skdraw.polygon(y, x)] = 0
×
1609
            glacier_mask[y, x] = 0  # on the nunataks, no
×
1610
        x, y = tuple2int(poly.exterior.xy)
×
1611
        glacier_mask[y, x] = 1
×
1612
        glacier_ext[y, x] = 1
×
1613

1614
    # Last sanity check based on the masked dem
1615
    tmp_max = np.max(dem[np.where(glacier_mask == 1)])
×
1616
    tmp_min = np.min(dem[np.where(glacier_mask == 1)])
×
1617
    if tmp_max < (tmp_min + 1):
×
1618
        raise RuntimeError('({}) min equal max in the masked DEM.'
×
1619
                           .format(gdir.rgi_id))
1620

1621
    # write out the grids in the netcdf file
1622
    with GriddedNcdfFile(gdir, reset=True) as nc:
×
1623

1624
        v = nc.createVariable('topo', 'f4', ('y', 'x', ), zlib=True)
×
1625
        v.units = 'm'
×
1626
        v.long_name = 'DEM topography'
×
1627
        v[:] = dem
×
1628

1629
        v = nc.createVariable('glacier_mask', 'i1', ('y', 'x', ), zlib=True)
×
1630
        v.units = '-'
×
1631
        v.long_name = 'Glacier mask'
×
1632
        v[:] = glacier_mask
×
1633

1634
        v = nc.createVariable('glacier_ext', 'i1', ('y', 'x', ), zlib=True)
×
1635
        v.units = '-'
×
1636
        v.long_name = 'Glacier external boundaries'
×
1637
        v[:] = glacier_ext
×
1638

1639
        # add some meta stats and close
1640
        nc.max_h_dem = np.nanmax(dem)
×
1641
        nc.min_h_dem = np.nanmin(dem)
×
1642
        dem_on_g = dem[np.where(glacier_mask)]
×
1643
        nc.max_h_glacier = np.max(dem_on_g)
×
1644
        nc.min_h_glacier = np.min(dem_on_g)
×
1645

1646
    geometries = dict()
×
1647
    geometries['polygon_hr'] = glacier_poly_hr
×
1648
    geometries['polygon_pix'] = glacier_poly_pix
×
1649
    geometries['polygon_area'] = geometry.area
×
1650
    gdir.write_pickle(geometries, 'geometries')
×
1651

1652

1653
@entity_task(log)
9✔
1654
def gridded_data_var_to_geotiff(gdir, varname, fname=None):
9✔
1655
    """Writes a NetCDF variable to a georeferenced geotiff file.
1656

1657
    The geotiff file will be written in the gdir directory.
1658

1659
    Parameters
1660
    ----------
1661
    gdir : :py:class:`oggm.GlacierDirectory`
1662
        where to write the data
1663
    varname : str
1664
        variable name in gridded_data.nc
1665
    fname : str
1666
        output file name (should end with `tif`), default is `varname.tif`
1667
    """
1668

1669
    # Assign the output path
1670
    if fname is None:
1✔
1671
        fname = varname+'.tif'
1✔
1672
    outpath = os.path.join(gdir.dir, fname)
1✔
1673

1674
    # Locate gridded_data.nc file and read it
1675
    nc_path = gdir.get_filepath('gridded_data')
1✔
1676
    with xr.open_dataset(nc_path) as ds:
1✔
1677

1678
        # Prepare the profile dict
1679
        crs = ds.pyproj_srs
1✔
1680
        var = ds[varname]
1✔
1681
        grid = ds.salem.grid
1✔
1682
        data = var.data
1✔
1683
        data_type = data.dtype.name
1✔
1684
        height, width = var.data.shape
1✔
1685
        dx, dy = grid.dx, grid.dy
1✔
1686
        x0, y0 = grid.x0, grid.y0
1✔
1687

1688
        profile = {'driver': 'GTiff', 'dtype': data_type, 'nodata': None,
1✔
1689
                   'width': width, 'height': height, 'count': 1,
1690
                   'crs': crs,
1691
                   'transform': rasterio.Affine(dx, 0.0, x0,
1692
                                                0.0, dy, y0),
1693
                   'tiled': True,
1694
                   'interleave': 'band'}
1695

1696
        # Write GeoTiff file
1697
        with rasterio.open(outpath, 'w', **profile) as dst:
1✔
1698
            dst.write(data, 1)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc