def test_smooth(self): a = np.array([1., 4, 7, 7, 4, 1]) b = utils.smooth1d(a, 3, kernel='mean') assert_allclose(b, [3, 4, 6, 6, 4, 3]) kernel = [0.60653066, 1., 0.60653066] b = utils.smooth1d(a, 3, kernel=kernel) c = utils.smooth1d(a, 3) assert_allclose(b, c)
def filter_inversion_output(gdir): """Filters the last few grid points after the physically-based inversion. For various reasons (but mostly: the equilibrium assumption), the last few grid points on a glacier flowline are often noisy and create unphysical depressions. Here we try to correct for that. It is not volume conserving, but area conserving. Parameters ---------- gdir : :py:class:`oggm.GlacierDirectory` the glacier directory to process """ if gdir.is_tidewater: # No need for filter in tidewater case return if not gdir.has_file('downstream_line'): raise InvalidWorkflowError('filter_inversion_output now needs a ' 'previous call to the ' 'compute_dowstream_line and ' 'compute_downstream_bedshape tasks') dic_ds = gdir.read_pickle('downstream_line') bs = np.average(dic_ds['bedshapes'][:3]) n = -5 cls = gdir.read_pickle('inversion_output') cl = cls[-1] # First guess thickness based on width w = cl['width'][n:] s = w**3 * bs / 6 h = 3 / 2 * s / w # Smoothing things out a bit hts = np.append(np.append(cl['thick'][n - 3:n], h), 0) h = utils.smooth1d(hts, 3)[n - 1:-1] # Recompute bedshape based on that bs = utils.clip_min(4 * h / w**2, cfg.PARAMS['mixed_min_shape']) # OK, done s = w**3 * bs / 6 cl['thick'][n:] = 3 / 2 * s / w cl['volume'][n:] = s * cl['dx'] cl['is_trapezoid'][n:] = False cl['is_rectangular'][n:] = False gdir.write_pickle(cls, 'inversion_output') # output the volume here - this simplifies code for some downstream funcs return np.sum([np.sum(cl['volume']) for cl in cls])
def catchment_width_correction(gdir, div_id=None): """Corrects for NaNs and inconsistencies in the geometrical widths. Interpolates missing values, ensures consistency of the surface-area distribution AND with the geometrical area of the glacier polygon, avoiding errors due to gridded representation. Updates the 'inversion_flowlines' save file. Parameters ---------- gdir : oggm.GlacierDirectory """ # The code below makes of this task a "special" divide task. # We keep it as is and remove the divide task decorator if div_id is None: # This is the original call # This time instead of just looping over the divides we add a test # to check for the conservation of the shapefile's area. area = 0. divides = [] for i in gdir.divide_ids: log.info('%s: width correction, divide %d', gdir.rgi_id, i) fls = catchment_width_correction(gdir, div_id=i, reset=True) for fl in fls: area += np.sum(fl.widths) * fl.dx divides.append(fls) # Final correction - because of the raster, the gridded area of the # glacier is not that of the actual geometry. correct for that fac = gdir.rgi_area_km2 / (area * gdir.grid.dx**2 * 10**-6) log.debug('%s: corrected widths with a factor %.2f', gdir.rgi_id, fac) for i in gdir.divide_ids: fls = divides[i-1] for fl in fls: fl.widths *= fac # Overwrite centerlines gdir.write_pickle(fls, 'inversion_flowlines', div_id=i) return None # variables flowlines = gdir.read_pickle('inversion_flowlines', div_id=div_id) catchment_indices = gdir.read_pickle('catchment_indices', div_id=div_id) # Topography for altitude-area distribution # I take the non-smoothed topography and remove the borders fpath = gdir.get_filepath('gridded_data', div_id=div_id) with netCDF4.Dataset(fpath) as nc: topo = nc.variables['topo'][:] ext = nc.variables['glacier_ext'][:] topo[np.where(ext==1)] = np.NaN # Param nmin = int(cfg.PARAMS['min_n_per_bin']) smooth_ws = int(cfg.PARAMS['smooth_widths_window_size']) # Per flowline (important so that later, the indices can be moved) catchment_heights = [] for ci in catchment_indices: _t = topo[tuple(ci.T)][:] catchment_heights.append(list(_t[np.isfinite(_t)])) # Loop over lines in a reverse order for fl, catch_h in zip(flowlines, catchment_heights): # Interpolate widths widths = utils.interp_nans(fl.widths) widths = np.clip(widths, 0.1, np.max(widths)) # Get topo per catchment and per flowline point fhgt = fl.surface_h # Sometimes, the centerline does not reach as high as each pix on the # glacier. (e.g. RGI40-11.00006) catch_h = np.clip(catch_h, 0, np.max(fhgt)) # Max and mins for the histogram maxh = np.max(fhgt) if fl.flows_to is None: minh = np.min(fhgt) catch_h = np.clip(catch_h, minh, np.max(catch_h)) else: minh = np.min(fhgt) # Min just for flowline (this has reasons) # Now decide on a binsize which ensures at least N element per bin bsize = cfg.PARAMS['base_binsize'] while True: maxb = utils.nicenumber(maxh, 1) minb = utils.nicenumber(minh, 1, lower=True) bins = np.arange(minb, maxb+bsize+0.01, bsize) minb = np.min(bins) # Ignore the topo pixels below the last bin tmp_ght = catch_h[np.where(catch_h >= minb)] topo_digi = np.digitize(tmp_ght, bins) - 1 # I prefer the left fl_digi = np.digitize(fhgt, bins) - 1 # I prefer the left if nmin == 1: # No need for complicated count _c = set(topo_digi) _fl = set(fl_digi) else: # Keep indexes with at least n counts _c = Counter(topo_digi.tolist()) _c = set([k for (k, v) in _c.items() if v >= nmin]) _fl = Counter(fl_digi.tolist()) _fl = set([k for (k, v) in _fl.items() if v >= nmin]) ref_set = set(range(len(bins)-1)) if (_c == ref_set) and (_fl == ref_set): # For each bin, the width(s) have to represent the "real" area new_widths = widths.copy() for bi in range(len(bins) - 1): bintopoarea = len(np.where(topo_digi == bi)[0]) wherewiths = np.where(fl_digi == bi) binflarea = np.sum(new_widths[wherewiths]) * fl.dx new_widths[wherewiths] = (bintopoarea / binflarea) * \ new_widths[wherewiths] break bsize += 5 # Add a security for infinite loops if bsize > 500: nmin -= 1 bsize = cfg.PARAMS['base_binsize'] log.warning('%s: reduced min n per bin to %d', gdir.rgi_id, nmin) if nmin == 0: raise RuntimeError('NO binsize could be chosen for: ' '{}'.format(gdir.rgi_id)) if bsize > 150: log.warning('%s: chosen binsize %d', gdir.rgi_id, bsize) else: log.debug('%s: chosen binsize %d', gdir.rgi_id, bsize) # Now keep the good topo pixels and send the unattributed ones to the # next flowline tosend = list(catch_h[np.where(catch_h < minb)]) if (len(tosend) > 0) and (fl.flows_to is not None): ide = flowlines.index(fl.flows_to) catchment_heights[ide] = np.append(catchment_heights[ide], tosend) if (len(tosend) > 0) and (fl.flows_to is None): raise RuntimeError('This should not happen') # Now we have a width which is the "best" representation of our # tributary according to the altitude area distribution. # This sometimes leads to abrupt changes in the widths from one # grid point to another. I think it's not too harmful to smooth them # here, at the cost of a less perfect altitude area distribution if smooth_ws != 0: if smooth_ws == 1: new_widths = utils.smooth1d(new_widths) else: new_widths = utils.smooth1d(new_widths, window_size=smooth_ws) # Write it fl.widths = new_widths return flowlines
def catchment_width_correction(gdir, div_id=None): """Corrects for NaNs and inconsistencies in the geometrical widths. Interpolates missing values, ensures consistency of the surface-area distribution AND with the geometrical area of the glacier polygon, avoiding errors due to gridded representation. Updates the 'inversion_flowlines' save file. Parameters ---------- gdir : oggm.GlacierDirectory """ # The code below makes of this task a "special" divide task. # We keep it as is and remove the divide task decorator if div_id is None: # This is the original call # This time instead of just looping over the divides we add a test # to check for the conservation of the shapefile's area. area = 0. divides = [] for i in gdir.divide_ids: log.info('%s: width correction, divide %d', gdir.rgi_id, i) fls = catchment_width_correction(gdir, div_id=i, reset=True) for fl in fls: area += np.sum(fl.widths) * fl.dx divides.append(fls) # Final correction - because of the raster, the gridded area of the # glacier is not that of the actual geometry. correct for that fac = gdir.rgi_area_km2 / (area * gdir.grid.dx**2 * 10**-6) log.debug('%s: corrected widths with a factor %.2f', gdir.rgi_id, fac) for i in gdir.divide_ids: fls = divides[i - 1] for fl in fls: fl.widths *= fac # Overwrite centerlines gdir.write_pickle(fls, 'inversion_flowlines', div_id=i) return None # variables flowlines = gdir.read_pickle('inversion_flowlines', div_id=div_id) catchment_indices = gdir.read_pickle('catchment_indices', div_id=div_id) # Topography for altitude-area distribution # I take the non-smoothed topography and remove the borders fpath = gdir.get_filepath('gridded_data', div_id=div_id) with netCDF4.Dataset(fpath) as nc: topo = nc.variables['topo'][:] ext = nc.variables['glacier_ext'][:] topo[np.where(ext == 1)] = np.NaN # Param nmin = int(cfg.PARAMS['min_n_per_bin']) smooth_ws = int(cfg.PARAMS['smooth_widths_window_size']) # Per flowline (important so that later, the indices can be moved) catchment_heights = [] for ci in catchment_indices: _t = topo[tuple(ci.T)][:] catchment_heights.append(list(_t[np.isfinite(_t)])) # Loop over lines in a reverse order for fl, catch_h in zip(flowlines, catchment_heights): # Interpolate widths widths = utils.interp_nans(fl.widths) widths = np.clip(widths, 0.1, np.max(widths)) # Get topo per catchment and per flowline point fhgt = fl.surface_h # Sometimes, the centerline does not reach as high as each pix on the # glacier. (e.g. RGI40-11.00006) catch_h = np.clip(catch_h, 0, np.max(fhgt)) # Max and mins for the histogram maxh = np.max(fhgt) if fl.flows_to is None: minh = np.min(fhgt) catch_h = np.clip(catch_h, minh, np.max(catch_h)) else: minh = np.min(fhgt) # Min just for flowline (this has reasons) # Now decide on a binsize which ensures at least N element per bin bsize = cfg.PARAMS['base_binsize'] while True: maxb = utils.nicenumber(maxh, 1) minb = utils.nicenumber(minh, 1, lower=True) bins = np.arange(minb, maxb + bsize + 0.01, bsize) minb = np.min(bins) # Ignore the topo pixels below the last bin tmp_ght = catch_h[np.where(catch_h >= minb)] topo_digi = np.digitize(tmp_ght, bins) - 1 # I prefer the left fl_digi = np.digitize(fhgt, bins) - 1 # I prefer the left if nmin == 1: # No need for complicated count _c = set(topo_digi) _fl = set(fl_digi) else: # Keep indexes with at least n counts _c = Counter(topo_digi.tolist()) _c = set([k for (k, v) in _c.items() if v >= nmin]) _fl = Counter(fl_digi.tolist()) _fl = set([k for (k, v) in _fl.items() if v >= nmin]) ref_set = set(range(len(bins) - 1)) if (_c == ref_set) and (_fl == ref_set): # For each bin, the width(s) have to represent the "real" area new_widths = widths.copy() for bi in range(len(bins) - 1): bintopoarea = len(np.where(topo_digi == bi)[0]) wherewiths = np.where(fl_digi == bi) binflarea = np.sum(new_widths[wherewiths]) * fl.dx new_widths[wherewiths] = (bintopoarea / binflarea) * \ new_widths[wherewiths] break bsize += 5 # Add a security for infinite loops if bsize > 500: nmin -= 1 bsize = cfg.PARAMS['base_binsize'] log.warning('%s: reduced min n per bin to %d', gdir.rgi_id, nmin) if nmin == 0: raise RuntimeError('NO binsize could be chosen for: ' '{}'.format(gdir.rgi_id)) if bsize > 150: log.warning('%s: chosen binsize %d', gdir.rgi_id, bsize) else: log.debug('%s: chosen binsize %d', gdir.rgi_id, bsize) # Now keep the good topo pixels and send the unattributed ones to the # next flowline tosend = list(catch_h[np.where(catch_h < minb)]) if (len(tosend) > 0) and (fl.flows_to is not None): ide = flowlines.index(fl.flows_to) catchment_heights[ide] = np.append(catchment_heights[ide], tosend) if (len(tosend) > 0) and (fl.flows_to is None): raise RuntimeError('This should not happen') # Now we have a width which is the "best" representation of our # tributary according to the altitude area distribution. # This sometimes leads to abrupt changes in the widths from one # grid point to another. I think it's not too harmful to smooth them # here, at the cost of a less perfect altitude area distribution if smooth_ws != 0: if smooth_ws == 1: new_widths = utils.smooth1d(new_widths) else: new_widths = utils.smooth1d(new_widths, window_size=smooth_ws) # Write it fl.widths = new_widths return flowlines