def filter_inversion_output(gdir): """Filters the last few grid point whilst conserving total volume. The last few grid points sometimes are noisy or can have a negative slope. This function filters them while conserving the total volume. Parameters ---------- gdir : :py:class:`oggm.GlacierDirectory` the glacier directory to process """ if gdir.is_tidewater: # No need for filter in tidewater case return cls = gdir.read_pickle('inversion_output') for cl in cls: init_vol = np.sum(cl['volume']) if init_vol == 0 or not cl['is_last']: continue w = cl['width'] out_thick = cl['thick'] fac = np.where(cl['is_rectangular'], 1, 2./3.) # Last thicknesses can be noisy sometimes: interpolate out_thick[-4:] = np.NaN out_thick = utils.interp_nans(np.append(out_thick, 0))[:-1] assert len(out_thick) == len(fac) # final volume volume = fac * out_thick * w * cl['dx'] # conserve it new_vol = np.nansum(volume) if new_vol == 0: # Very small glaciers return volume = init_vol / new_vol * volume np.testing.assert_allclose(np.nansum(volume), init_vol) # recompute thickness on that base out_thick = volume / (fac * w * cl['dx']) # output cl['thick'] = out_thick cl['volume'] = volume gdir.write_pickle(cls, 'inversion_output')
def filter_inversion_output(gdir): """Filters the last few grid point whilst conserving total volume. The last few grid points sometimes are noisy or can have a negative slope. This function filters them while conserving the total volume. Parameters ---------- gdir : :py:class:`oggm.GlacierDirectory` the glacier directory to process """ if gdir.is_tidewater: # No need for filter in tidewater case return cls = gdir.read_pickle('inversion_output') for cl in cls: init_vol = np.sum(cl['volume']) if init_vol == 0 or not cl['is_last']: continue w = cl['width'] out_thick = cl['thick'] fac = np.where(cl['is_rectangular'], 1, 2. / 3.) # Last thicknesses can be noisy sometimes: interpolate out_thick[-4:] = np.NaN out_thick = utils.interp_nans(np.append(out_thick, 0))[:-1] assert len(out_thick) == len(fac) # final volume volume = fac * out_thick * w * cl['dx'] # conserve it new_vol = np.nansum(volume) if new_vol == 0: # Very small glaciers return volume = init_vol / new_vol * volume np.testing.assert_allclose(np.nansum(volume), init_vol) # recompute thickness on that base out_thick = volume / (fac * w * cl['dx']) # output cl['thick'] = out_thick cl['volume'] = volume gdir.write_pickle(cls, 'inversion_output')
def filter_inversion_output(gdir): """Filters the last few grid point whilst conserving total volume. """ if gdir.is_tidewater: # No need for filter in tidewater case return cls = gdir.read_pickle('inversion_output') for cl in cls: init_vol = np.sum(cl['volume']) if init_vol == 0 or not cl['is_last']: continue w = cl['width'] out_thick = cl['thick'] fac = np.where(cl['is_rectangular'], 1, 2. / 3.) # Last thicknesses can be noisy sometimes: interpolate out_thick[-4:] = np.NaN out_thick = utils.interp_nans(np.append(out_thick, 0))[:-1] assert len(out_thick) == len(fac) # final volume volume = fac * out_thick * w * cl['dx'] # conserve it new_vol = np.nansum(volume) assert new_vol != 0 volume = init_vol / new_vol * volume np.testing.assert_allclose(np.nansum(volume), init_vol) # recompute thickness on that base out_thick = volume / (fac * w * cl['dx']) # output cl['thick'] = out_thick cl['volume'] = volume gdir.write_pickle(cls, 'inversion_output')
def filter_inversion_output(gdir): """Filters the last few grid point whilst conserving total volume. """ for div in gdir.divide_ids: cls = gdir.read_pickle('inversion_output', div_id=div) for cl in cls: init_vol = np.sum(cl['volume']) if init_vol == 0 or gdir.is_tidewater or not cl['is_last']: continue w = cl['width'] out_thick = cl['thick'] fac = np.where(cl['is_rectangular'], 1, cfg.TWO_THIRDS) # Last thicknesses can be noisy sometimes: interpolate out_thick[-4:] = np.NaN out_thick = utils.interp_nans(np.append(out_thick, 0))[:-1] assert len(out_thick) == len(fac) # final volume volume = fac * out_thick * w * cl['dx'] # conserve it new_vol = np.nansum(volume) assert new_vol != 0 volume = init_vol / new_vol * volume np.testing.assert_allclose(np.nansum(volume), init_vol) # recompute thickness on that base out_thick = volume / (fac * w * cl['dx']) # output cl['thick'] = out_thick cl['volume'] = volume gdir.write_pickle(cls, 'inversion_output', div_id=div)
def invert_parabolic_bed(gdir, glen_a=cfg.A, fs=0., write=True): """ Compute the glacier thickness along the flowlines More or less following Farinotti et al., (2009). The thickness estimation is computed under the assumption of a parabolic bed section. It returns (vol, thick) in (m3, m). Parameters ---------- gdir : oggm.GlacierDirectory glen_a : float glen's creep parameter A fs : float sliding parameter write: bool default behavior is to compute the thickness and write the results in the pickle. Set to False in order to spare time during calibration. """ # Check input if fs == 0.: _inv_function = _inversion_simple else: _inv_function = _inversion_poly # Ice flow params fd = 2. / (cfg.N+2) * glen_a a3 = fs / fd # sometimes the width is small and the flux is big. crop this max_ratio = cfg.PARAMS['max_thick_to_width_ratio'] max_shape = cfg.PARAMS['max_shape_param'] # sigma of the smoothing window after inversion sec_smooth = cfg.PARAMS['section_smoothing'] # Clip the slope, in degrees clip_angle = cfg.PARAMS['min_slope'] out_volume = 0. for div in gdir.divide_ids: cls = gdir.read_pickle('inversion_input', div_id=div) for cl in cls: # Clip slope to avoid negative and small slopes slope = cl['slope_angle'] slope = np.clip(slope, np.deg2rad(clip_angle), np.pi/2.) # Parabolic bed rock w = cl['width'] a0s = -(3*cl['flux'])/(2*w*((cfg.RHO*cfg.G*slope)**3)*fd) if np.any(~np.isfinite(a0s)): raise RuntimeError('{}: something went wrong with ' 'inversion'.format(gdir.rgi_id)) # GO out_thick = np.zeros(len(slope)) for i, (a0, Q) in enumerate(zip(a0s, cl['flux'])): if Q > 0.: out_thick[i] = _inv_function(a3, a0) else: out_thick[i] = 0. # Check for thick to width ratio (should ne be too large) ratio = out_thick / w # there's no 0 width so we're good pno = np.where(ratio > max_ratio) if len(pno[0]) > 0: ratio[pno] = np.NaN ratio = utils.interp_nans(ratio, default=max_ratio) out_thick = w * ratio # TODO: last thicknesses can be noisy sometimes: interpolate? if cl['is_last']: out_thick[-4:-1] = np.NaN out_thick = utils.interp_nans(out_thick) # Check for the shape parameter (should not be too large) out_shape = (4*out_thick)/(w**2) pno = np.where(out_shape > max_shape) if len(pno[0]) > 0: out_shape[pno] = np.NaN out_shape = utils.interp_nans(out_shape, default=max_shape) out_thick = (out_shape * w**2) / 4 # smooth section if sec_smooth != 0.: section = cfg.TWO_THIRDS * w * out_thick * cl['dx'] section = gaussian_filter1d(section, sec_smooth) out_thick = section / (w * cfg.TWO_THIRDS * cl['dx']) # volume volume = cfg.TWO_THIRDS * out_thick * w * cl['dx'] if write: cl['thick'] = out_thick cl['volume'] = volume out_volume += np.nansum(volume) if write: gdir.write_pickle(cls, 'inversion_output', div_id=div) return out_volume, gdir.rgi_area_km2 * 1e6
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 initialize_flowlines(gdir, div_id=None): """ Transforms the geometrical Centerlines in the more "physical" "Inversion Flowlines". This interpolates the centerlines on a regular spacing (i.e. not the grid's (i, j) indices. Cuts out the tail of the tributaries to make more realistic junctions. Also checks for low and negative slopes and corrects them by interpolation. Parameters ---------- gdir : oggm.GlacierDirectory """ # variables if div_id == 0 and not gdir.has_file('centerlines', div_id=div_id): # downstream lines haven't been computed return cls = gdir.read_pickle('centerlines', div_id=div_id) poly = gdir.read_pickle('geometries', div_id=div_id) poly = poly['polygon_pix'].buffer(0.5) # a small buffer around to be sure # Initialise the flowlines dx = cfg.PARAMS['flowline_dx'] do_filter = cfg.PARAMS['filter_min_slope'] lid = int(cfg.PARAMS['flowline_junction_pix']) fls = [] # Topo for heights fpath = gdir.get_filepath('gridded_data', div_id=div_id) with netCDF4.Dataset(fpath) as nc: topo = nc.variables['topo_smoothed'][:] # Bilinear interpolation # Geometries coordinates are in "pixel centered" convention, i.e # (0, 0) is also located in the center of the pixel xy = (np.arange(0, gdir.grid.ny-0.1, 1), np.arange(0, gdir.grid.nx-0.1, 1)) interpolator = RegularGridInterpolator(xy, topo) # Smooth window sw = cfg.PARAMS['flowline_height_smooth'] for ic, cl in enumerate(cls): points = line_interpol(cl.line, dx) # For tributaries, remove the tail if ic < (len(cls)-1): points = points[0:-lid] new_line = shpg.LineString(points) # Interpolate heights xx, yy = new_line.xy hgts = interpolator((yy, xx)) assert len(hgts) >= 5 # Check where the glacier is and where not isglacier = [poly.contains(shpg.Point(x, y)) for x, y in zip(xx, yy)] if div_id != 0: assert np.all(isglacier) # If smoothing, this is the moment hgts = gaussian_filter1d(hgts, sw) # Check for min slope issues and correct if needed if do_filter: # Correct only where glacier nhgts = _filter_small_slopes(hgts[isglacier], dx*gdir.grid.dx) isfin = np.isfinite(nhgts) assert np.any(isfin) perc_bad = np.sum(~isfin) / len(isfin) if perc_bad > 0.8: log.warning('{}: more than {:.0%} of the flowline is cropped ' 'due to negative slopes.'.format(gdir.rgi_id, perc_bad)) hgts[isglacier] = nhgts sp = np.min(np.where(np.isfinite(nhgts))[0]) while len(hgts[sp:]) < 5: sp -= 1 hgts = utils.interp_nans(hgts[sp:]) isglacier = isglacier[sp:] assert np.all(np.isfinite(hgts)) assert len(hgts) >= 5 new_line = shpg.LineString(points[sp:]) l = Centerline(new_line, dx=dx, surface_h=hgts, is_glacier=isglacier) l.order = cl.order fls.append(l) # All objects are initialized, now we can link them. for cl, fl in zip(cls, fls): if cl.flows_to is None: continue fl.set_flows_to(fls[cls.index(cl.flows_to)]) # Write the data gdir.write_pickle(fls, 'inversion_flowlines', div_id=div_id)
def init_present_time_glacier(gdir): """First task after inversion. Merges the data from the various preprocessing tasks into a stand-alone dataset ready for run. In a first stage we assume that all divides CAN be merged as for HEF, so that the concept of divide is not necessary anymore. This task is horribly coded and needs work Parameters ---------- gdir : oggm.GlacierDirectory """ # Topo for heights nc = netCDF4.Dataset(gdir.get_filepath('gridded_data', div_id=0)) topo = nc.variables['topo_smoothed'][:] nc.close() # Bilinear interpolation # Geometries coordinates are in "pixel centered" convention, i.e # (0, 0) is also located in the center of the pixel xy = (np.arange(0, gdir.grid.ny - 0.1, 1), np.arange(0, gdir.grid.nx - 0.1, 1)) interpolator = RegularGridInterpolator(xy, topo) # Smooth window sw = cfg.PARAMS['flowline_height_smooth'] # Map map_dx = gdir.grid.dx # OK. Dont try to solve problems you don't know about yet - i.e. # rethink about all this when we will have proper divides everywhere. # for HEF the following will work, and this is very ugly. major_div = gdir.read_pickle('major_divide', div_id=0) div_ids = list(gdir.divide_ids) div_ids.remove(major_div) div_ids = [major_div] + div_ids fls_list = [] fls_per_divide = [] inversion_per_divide = [] for div_id in div_ids: fls = gdir.read_pickle('inversion_flowlines', div_id=div_id) fls_list.extend(fls) fls_per_divide.append(fls) invs = gdir.read_pickle('inversion_output', div_id=div_id) inversion_per_divide.append(invs) # Which kind of bed? if cfg.PARAMS['bed_shape'] == 'mixed': flobject = partial(MixedFlowline, min_shape=cfg.PARAMS['mixed_min_shape'], lambdas=cfg.PARAMS['trapezoid_lambdas']) elif cfg.PARAMS['bed_shape'] == 'parabolic': flobject = ParabolicFlowline else: raise NotImplementedError('bed: {}'.format(cfg.PARAMS['bed_shape'])) max_shape = cfg.PARAMS['max_shape_param'] # Extend the flowlines with the downstream lines, make a new object new_fls = [] flows_to_ids = [] major_id = None for fls, invs, did in zip(fls_per_divide, inversion_per_divide, div_ids): for fl, inv in zip(fls[0:-1], invs[0:-1]): bed_h = fl.surface_h - inv['thick'] w = fl.widths * map_dx bed_shape = (4 * inv['thick']) / (w**2) bed_shape = bed_shape.clip(0, max_shape) bed_shape = np.where(inv['thick'] < 1., np.NaN, bed_shape) bed_shape = utils.interp_nans(bed_shape) nfl = flobject(fl.line, fl.dx, map_dx, fl.surface_h, bed_h, bed_shape) flows_to_ids.append(fls_list.index(fl.flows_to)) new_fls.append(nfl) # The last one is extended with the downstream # TODO: copy-paste code smell fl = fls[-1] inv = invs[-1] dline = gdir.read_pickle('downstream_line', div_id=did) long_line = oggm.core.preprocessing.geometry._line_extend( fl.line, dline, fl.dx) # Interpolate heights x, y = long_line.xy hgts = interpolator((y, x)) # Inversion stuffs bed_h = hgts.copy() bed_h[0:len(fl.surface_h)] -= inv['thick'] # Shapes w = fl.widths * map_dx bed_shape = (4 * inv['thick']) / (w**2) bed_shape = bed_shape.clip(0, max_shape) bed_shape = np.where(inv['thick'] < 1., np.NaN, bed_shape) bed_shape = utils.interp_nans(bed_shape) # But forbid too small shape close to the end if cfg.PARAMS['bed_shape'] == 'mixed': bed_shape[-4:] = bed_shape[-4:].clip(cfg.PARAMS['mixed_min_shape']) # Take the median of the last 30% ashape = np.median( bed_shape[-np.floor(len(bed_shape) / 3.).astype(np.int64):]) # But forbid too small shape if cfg.PARAMS['bed_shape'] == 'mixed': ashape = ashape.clip(cfg.PARAMS['mixed_min_shape']) bed_shape = np.append(bed_shape, np.ones(len(bed_h) - len(bed_shape)) * ashape) nfl = flobject(long_line, fl.dx, map_dx, hgts, bed_h, bed_shape) if major_id is None: flid = -1 major_id = len(fls) - 1 else: flid = major_id flows_to_ids.append(flid) new_fls.append(nfl) # Finalize the linkages for fl, fid in zip(new_fls, flows_to_ids): if fid == -1: continue fl.set_flows_to(new_fls[fid]) # Adds the line level for fl in new_fls: fl.order = oggm.core.preprocessing.centerlines._line_order(fl) # And sort them per order fls = [] for i in np.argsort([fl.order for fl in new_fls]): fls.append(new_fls[i]) # Write the data gdir.write_pickle(fls, 'model_flowlines')
def init_present_time_glacier(gdir): """First task after inversion. Merges the data from the various preprocessing tasks into a stand-alone dataset ready for run. In a first stage we assume that all divides CAN be merged as for HEF, so that the concept of divide is not necessary anymore. This task is horribly coded and needs work Parameters ---------- gdir : oggm.GlacierDirectory """ # Topo for heights with netCDF4.Dataset(gdir.get_filepath('gridded_data', div_id=0)) as nc: topo = nc.variables['topo_smoothed'][:] # Bilinear interpolation # Geometries coordinates are in "pixel centered" convention, i.e # (0, 0) is also located in the center of the pixel xy = (np.arange(0, gdir.grid.ny-0.1, 1), np.arange(0, gdir.grid.nx-0.1, 1)) interpolator = RegularGridInterpolator(xy, topo) # Smooth window sw = cfg.PARAMS['flowline_height_smooth'] # Map map_dx = gdir.grid.dx # OK. Dont try to solve problems you don't know about yet - i.e. # rethink about all this when we will have proper divides everywhere. # for HEF the following will work, and this is very ugly. major_div = gdir.read_pickle('major_divide', div_id=0) div_ids = list(gdir.divide_ids) div_ids.remove(major_div) div_ids = [major_div] + div_ids fls_list = [] fls_per_divide = [] inversion_per_divide = [] for div_id in div_ids: fls = gdir.read_pickle('inversion_flowlines', div_id=div_id) fls_list.extend(fls) fls_per_divide.append(fls) invs = gdir.read_pickle('inversion_output', div_id=div_id) inversion_per_divide.append(invs) # Which kind of bed? if cfg.PARAMS['bed_shape'] == 'mixed': flobject = partial(MixedFlowline, min_shape=cfg.PARAMS['mixed_min_shape'], lambdas=cfg.PARAMS['trapezoid_lambdas']) elif cfg.PARAMS['bed_shape'] == 'parabolic': flobject = ParabolicFlowline else: raise NotImplementedError('bed: {}'.format(cfg.PARAMS['bed_shape'])) max_shape = cfg.PARAMS['max_shape_param'] # Extend the flowlines with the downstream lines, make a new object new_fls = [] flows_to_ids = [] major_id = None for fls, invs, did in zip(fls_per_divide, inversion_per_divide, div_ids): for fl, inv in zip(fls[0:-1], invs[0:-1]): bed_h = fl.surface_h - inv['thick'] w = fl.widths * map_dx bed_shape = (4*inv['thick'])/(w**2) bed_shape = bed_shape.clip(0, max_shape) bed_shape = np.where(inv['thick'] < 1., np.NaN, bed_shape) bed_shape = utils.interp_nans(bed_shape) nfl = flobject(fl.line, fl.dx, map_dx, fl.surface_h, bed_h, bed_shape) flows_to_ids.append(fls_list.index(fl.flows_to)) new_fls.append(nfl) # The last one is extended with the downstream # TODO: copy-paste code smell fl = fls[-1] inv = invs[-1] dline = gdir.read_pickle('downstream_line', div_id=did) long_line = oggm.core.preprocessing.geometry._line_extend(fl.line, dline, fl.dx) # Interpolate heights x, y = long_line.xy hgts = interpolator((y, x)) # Inversion stuffs bed_h = hgts.copy() bed_h[0:len(fl.surface_h)] -= inv['thick'] # Shapes w = fl.widths * map_dx bed_shape = (4*inv['thick'])/(w**2) bed_shape = bed_shape.clip(0, max_shape) bed_shape = np.where(inv['thick'] < 1., np.NaN, bed_shape) bed_shape = utils.interp_nans(bed_shape) # But forbid too small shape close to the end if cfg.PARAMS['bed_shape'] == 'mixed': bed_shape[-4:] = bed_shape[-4:].clip(cfg.PARAMS['mixed_min_shape']) # Take the median of the last 30% ashape = np.median(bed_shape[-np.floor(len(bed_shape)/3.).astype(np.int64):]) # But forbid too small shape if cfg.PARAMS['bed_shape'] == 'mixed': ashape = ashape.clip(cfg.PARAMS['mixed_min_shape']) bed_shape = np.append(bed_shape, np.ones(len(bed_h)-len(bed_shape))*ashape) nfl = flobject(long_line, fl.dx, map_dx, hgts, bed_h, bed_shape) if major_id is None: flid = -1 major_id = len(fls)-1 else: flid = major_id flows_to_ids.append(flid) new_fls.append(nfl) # Finalize the linkages for fl, fid in zip(new_fls, flows_to_ids): if fid == -1: continue fl.set_flows_to(new_fls[fid]) # Adds the line level for fl in new_fls: fl.order = oggm.core.preprocessing.centerlines._line_order(fl) # And sort them per order fls = [] for i in np.argsort([fl.order for fl in new_fls]): fls.append(new_fls[i]) # Write the data gdir.write_pickle(fls, 'model_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) 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']) # 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 # # TODO: smooth them ? # widths = utils.smooth1d(widths) # if np.nanmax(_width_change_factor(new_widths)[:-5]) < 2.: # break bsize += 5 # Add a security for infinite loops if bsize > 250: 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') # Write it fl.widths = new_widths return flowlines
def initialize_flowlines(gdir, div_id=None): """ Transforms the geometrical Centerlines in the more "physical" "Inversion Flowlines". This interpolates the centerlines on a regular spacing (i.e. not the grid's (i, j) indices. Cuts out the tail of the tributaries to make more realistic junctions. It also checks for low and negatve slopes and corrects them by interpolation. Parameters ---------- gdir : oggm.GlacierDirectory """ # variables cls = gdir.read_pickle('centerlines', div_id=div_id) # Initialise the flowlines dx = cfg.PARAMS['flowline_dx'] ms = cfg.PARAMS['min_slope'] do_filter = cfg.PARAMS['filter_min_slope'] lid = int(cfg.PARAMS['flowline_junction_pix']) fls = [] # Topo for heights fpath = gdir.get_filepath('gridded_data', div_id=div_id) with netCDF4.Dataset(fpath) as nc: topo = nc.variables['topo_smoothed'][:] # Bilinear interpolation # Geometries coordinates are in "pixel centered" convention, i.e # (0, 0) is also located in the center of the pixel xy = (np.arange(0, gdir.grid.ny-0.1, 1), np.arange(0, gdir.grid.nx-0.1, 1)) interpolator = RegularGridInterpolator(xy, topo) # Smooth window sw = cfg.PARAMS['flowline_height_smooth'] for ic, cl in enumerate(cls): points = _line_interpol(cl.line, dx) # For tributaries, remove the tail if ic < (len(cls)-1): points = points[0:-lid] new_line = shpg.LineString(points) # Interpolate heights x, y = new_line.xy hgts = interpolator((y, x)) # If smoothing, this is the moment hgts = gaussian_filter1d(hgts, sw) # Check for min slope issues and correct if needed if do_filter: hgts = _filter_small_slopes(hgts, dx*gdir.grid.dx, min_slope=ms) isfin = np.isfinite(hgts) assert np.any(isfin) perc_bad = np.sum(~isfin) / len(isfin) if perc_bad > 0.1: log.warning('{}: more than {:.0%} of the flowline is cropped ' 'due to negative slopes.'.format(gdir.rgi_id, perc_bad)) sp = np.min(np.where(isfin)[0]) hgts = utils.interp_nans(hgts[sp:]) assert np.all(np.isfinite(hgts)) assert len(hgts) > 5 new_line = shpg.LineString(points[sp:]) l = Centerline(new_line, dx=dx, surface_h=hgts) l.order = cl.order fls.append(l) # All objects are initialized, now we can link them. for cl, fl in zip(cls, fls): if cl.flows_to is None: continue fl.set_flows_to(fls[cls.index(cl.flows_to)]) # Write the data gdir.write_pickle(fls, 'inversion_flowlines', div_id=div_id)
def filter_inversion_output(gdir): """Overwrites the inversion output with filtered one. This conserves the total volume. """ # sometimes the width is small and the flux is big. crop this max_ratio = cfg.PARAMS['max_thick_to_width_ratio'] max_shape = cfg.PARAMS['max_shape_param'] # sigma of the smoothing window after inversion sec_smooth = cfg.PARAMS['section_smoothing'] for div in gdir.divide_ids: cls = gdir.read_pickle('inversion_output', div_id=div) for cl in cls: # this filtering stuff below is not explained in Farinotti's # paper. I did this because it looks better, but I'm not sure # (yet) that this is a good idea fac = np.where(cl['is_rectangular'], 1, cfg.TWO_THIRDS) init_vol = np.sum(cl['volume']) if init_vol == 0: # this can happen continue w = cl['width'] out_thick = cl['thick'] # However for tidewater we have to be carefull at the tongue if gdir.is_tidewater and cl['is_last']: # store it to restore it later tongue_thick = out_thick[-5:] # Check for thick to width ratio (should ne be too large) ratio = out_thick / w # there's no 0 width so we're good pno = np.where((~ cl['is_rectangular']) & (ratio > max_ratio)) if len(pno[0]) > 0: ratio[pno] = np.NaN ratio = utils.interp_nans(ratio, default=max_ratio) out_thick[pno] = w[pno] * ratio[pno] # TODO: last thicknesses can be noisy sometimes: interpolate? if cl['is_last']: out_thick[-4:-1] = np.NaN out_thick = utils.interp_nans(out_thick) # Check for the shape parameter (should not be too large) out_shape = (4 * out_thick) / (w ** 2) pno = np.where((~ cl['is_rectangular']) & (out_shape > max_shape)) if len(pno[0]) > 0: out_shape[pno] = np.NaN out_shape = utils.interp_nans(out_shape, default=max_shape) out_thick[pno] = (out_shape[pno] * w[pno] ** 2) / 4 # smooth section if sec_smooth != 0.: section = out_thick * fac * w * cl['dx'] section = gaussian_filter1d(section, sec_smooth) out_thick = section / (fac * w * cl['dx']) if gdir.is_tidewater and cl['is_last']: # restore the last thicknesses out_thick[-5:] = tongue_thick # final volume volume = fac * out_thick * w * cl['dx'] # conserve it new_vol = np.nansum(volume) volume = init_vol / new_vol * volume np.testing.assert_allclose(np.nansum(volume), init_vol) # recompute thickness on that base out_thick = volume / (fac * w * cl['dx']) # output cl['thick'] = out_thick cl['volume'] = volume gdir.write_pickle(cls, 'inversion_output', div_id=div)
def _parabolic_bed_from_topo(gdir, idl, interpolator): """this returns the parabolic bedhape for all points on idl""" # Volume area scaling formula for the probable ice thickness h_mean = 0.034 * gdir.rgi_area_km2**0.375 * 1000 gnx, gny = gdir.grid.nx, gdir.grid.ny # Far Factor r = 40 # number of points cs_n = 20 # normals ns = [i[0] for i in idl.normals] cs = [] donot_compute = [] for pcoords, n, isgl in zip(idl.line.coords, ns, idl.is_glacier): xi, yi = pcoords vx, vy = n modul = np.sqrt(vx**2 + vy**2) ci = [] _isborder = False for ro in np.linspace(0, r / 2.0, cs_n): t = ro / modul cp1 = HashablePoint(xi + t * vx, yi + t * vy) cp2 = HashablePoint(xi - t * vx, yi - t * vy) # check if out of the frame if not (0 < cp2.y < gny - 1) or \ not (0 < cp2.x < gnx - 1) or \ not (0 < cp1.y < gny - 1) or \ not (0 < cp1.x < gnx - 1): _isborder = True ci.append((cp1, ro)) ci.append((cp2, -ro)) ci = list(set(ci)) cs.append(ci) donot_compute.append(_isborder or isgl) bed = [] for ic, (cc, dontcomp) in enumerate(zip(cs, donot_compute)): if dontcomp: bed.append(np.NaN) continue z = [] ro = [] for i in cc: z.append(interpolator((i[0].y, i[0].x))) ro.append(i[1]) aso = np.argsort(ro) ro, z = np.array(ro)[aso], np.array(z)[aso] # find top of parabola roHead = ro[np.argmin(z)] zero = np.argmin(z) # it is index of roHead/zHead zHead = np.amin(z) dsts = abs(h_mean + zHead - z) # find local minima in set of distances extr = scipy.signal.argrelextrema(dsts, np.less, mode='wrap') if len(extr[0]) == 0: bed.append(np.NaN) continue # from local minima find that with the minimum |x| idx = extr[0][np.argmin(abs(ro[extr]))] # x<0 => x=0 # (|x|+x)/2 roN = ro[int((abs(zero - abs(zero - idx)) + zero - abs(zero - idx)) / 2):zero + abs(zero - idx) + 1] zN = z[int((abs(zero - abs(zero - idx)) + zero - abs(zero - idx)) / 2):zero + abs(zero - idx) + 1] roNx = roN - roHead # zN=zN-zHead# p = _approx_parabola(roNx, zN, y0=zHead) # shift parabola to the ds-line p2 = np.copy(p) p2[2] = z[ro == 0] err = _parabola_error(roN, zN, p2) * 100 # The original implementation of @anton-ub stored all three parabola # params. We just keep the one important here for now if err < 1.5: bed.append(p2[0]) else: bed.append(np.NaN) bed = np.asarray(bed) assert len(bed) == idl.nx pvalid = np.sum(np.isfinite(bed)) / len(bed) * 100 log.debug('%s: percentage of valid parabolas total: %d', gdir.rgi_id, int(pvalid)) bedg = bed[~idl.is_glacier] if len(bedg) > 0: pvalid = np.sum(np.isfinite(bedg)) / len(bedg) * 100 log.debug('%s: percentage of valid parabolas out glacier: %d', gdir.rgi_id, int(pvalid)) if pvalid < 10: log.warning('{}: {}% of valid bedshapes.'.format( gdir.rgi_id, int(pvalid))) # interpolation, filling the gaps default = cfg.PARAMS['default_parabolic_bedshape'] bed_int = interp_nans(bed, default=default) # Scale for dx (we worked in grid coords but need meters) bed_int = bed_int / gdir.grid.dx**2 # Smoothing bed_ma = pdSeries(bed_int) bed_ma = bed_ma.rolling(window=5, center=True, min_periods=1).mean() return bed_ma.values
def invert_parabolic_bed(gdir, glen_a=cfg.A, fs=0., write=True): """ Compute the glacier thickness along the flowlines More or less following Farinotti et al., (2009). The thickness estimation is computed under the assumption of a parabolic bed section. It returns (vol, thick) in (m3, m). Parameters ---------- gdir : oggm.GlacierDirectory glen_a : float glen's creep parameter A fs : float sliding parameter write: bool default behavior is to compute the thickness and write the results in the pickle. Set to False in order to spare time during calibration. """ # Check input if fs == 0.: _inv_function = _inversion_simple else: _inv_function = _inversion_poly # Ice flow params fd = 2. / (cfg.N + 2) * glen_a a3 = fs / fd # sometimes the width is small and the flux is big. crop this max_ratio = cfg.PARAMS['max_thick_to_width_ratio'] max_shape = cfg.PARAMS['max_shape_param'] # sigma of the smoothing window after inversion sec_smooth = cfg.PARAMS['section_smoothing'] # Clip the slope, in degrees clip_angle = cfg.PARAMS['min_slope'] out_volume = 0. for div in gdir.divide_ids: cls = gdir.read_pickle('inversion_input', div_id=div) for cl in cls: # Clip slope to avoid negative and small slopes slope = cl['slope_angle'] slope = np.clip(slope, np.deg2rad(clip_angle), np.pi / 2.) # Parabolic bed rock w = cl['width'] a0s = -(3 * cl['flux']) / (2 * w * ((cfg.RHO * cfg.G * slope)**3) * fd) if np.any(~np.isfinite(a0s)): raise RuntimeError('{}: something went wrong with ' 'inversion'.format(gdir.rgi_id)) # GO out_thick = np.zeros(len(slope)) for i, (a0, Q) in enumerate(zip(a0s, cl['flux'])): if Q > 0.: out_thick[i] = _inv_function(a3, a0) else: out_thick[i] = 0. if gdir.terminus_type == 'Land-terminating': # this filtering stuff below is not explained in Farinotti's # paper. I did this because it looks better, but I'm not sure # (yet) that this is a good idea # Check for thick to width ratio (should ne be too large) ratio = out_thick / w # there's no 0 width so we're good pno = np.where(ratio > max_ratio) if len(pno[0]) > 0: ratio[pno] = np.NaN ratio = utils.interp_nans(ratio, default=max_ratio) out_thick = w * ratio # TODO: last thicknesses can be noisy sometimes: interpolate? if cl['is_last']: out_thick[-4:-1] = np.NaN out_thick = utils.interp_nans(out_thick) # Check for the shape parameter (should not be too large) out_shape = (4 * out_thick) / (w**2) pno = np.where(out_shape > max_shape) if len(pno[0]) > 0: out_shape[pno] = np.NaN out_shape = utils.interp_nans(out_shape, default=max_shape) out_thick = (out_shape * w**2) / 4 # smooth section if sec_smooth != 0.: section = cfg.TWO_THIRDS * w * out_thick * cl['dx'] section = gaussian_filter1d(section, sec_smooth) out_thick = section / (w * cfg.TWO_THIRDS * cl['dx']) # volume volume = cfg.TWO_THIRDS * out_thick * w * cl['dx'] if write: cl['thick'] = out_thick cl['volume'] = volume out_volume += np.nansum(volume) if write: gdir.write_pickle(cls, 'inversion_output', div_id=div) return out_volume, gdir.rgi_area_km2 * 1e6
def inversion_parabolic_point_slope(gdir, fs=5.7e-20, fd=1.9e-24, write=True): """ Compute thickness and bed topography Parameters ---------- gdir: GlacierDir object fs: sliding param fd: deformation param write: default behavior is to compute the thickness and write the results in the pickle. Set to False in order to spare time during calibration. Returns ------- glacier volume (m^3), glacier area (m^2) """ # Check input if fs == 0.: _inv = _inversion_simple else: _inv = _inversion_poly a3 = fs / fd # sometimes the width is small and the flux is big. crop this too max_ratio = cfg.params['max_thick_to_width_ratio'] max_shape = cfg.params['max_shape_param'] # sigma of the smoothing window after inversion sec_smooth = cfg.params['section_smoothing'] # Clip the slope, in degrees clip_angle = cfg.params['min_slope'] out_volume = 0. for div in gdir.divide_ids: cls = gdir.read_pickle('inversion_input', div_id=div) for cl in cls: # Clip slope to avoid negative and small slopes slope = cl['slope_angle'] slope = np.clip(slope, np.deg2rad(clip_angle), np.pi/2.) # Parabolic bed rock w = cl['width'] a0s = -(3*cl['flux'])/(2*w*((rho*g*slope)**3)*fd) assert np.all(np.isfinite(a0s)) # GO out_thick = np.zeros(len(slope)) for i, (a0, Q) in enumerate(zip(a0s, cl['flux'])): if Q > 0.: out_thick[i] = _inv(a3, a0) else: out_thick[i] = 0. # Check for thick to width ratio (should ne be too large) ratio = out_thick / w # there's no 0 width so we're good pno = np.where(ratio > max_ratio) # TODO: investigate this if gdir.rgi_id != 'RGI40-11.03002': if len(pno[0]) > 0: ratio[pno] = np.NaN ratio = utils.interp_nans(ratio) out_thick = w * ratio # Check for the shape parameter (should ne be too large) out_shape = (4*out_thick)/(w**2) pno = np.where(out_shape > max_shape) if len(pno[0]) > 0 and (len(pno[0]) < (len(out_shape)/1.2)): out_shape[pno] = np.NaN out_shape = utils.interp_nans(out_shape) out_thick = 0.25 * out_shape * w**2 # smooth section section = twothirds * w * out_thick section = gaussian_filter1d(section, sec_smooth) out_thick = section / (w * twothirds) # volume volume = twothirds * out_thick * w * cl['dx'] if write: cl['thick'] = out_thick out_shape = (4*out_thick)/(w**2) out_shape = np.where(out_thick == 0., np.NaN, out_shape) cl['shape'] = out_shape cl['volume'] = volume out_volume += np.nansum(volume) if write: gdir.write_pickle(cls, 'inversion_output', div_id=div) return out_volume, gdir.glacier_area * 1e6