def re_set_linkdirs(links, nodes, imshape): """ Reset link directions. Resets the link directions for a braided river channel network. This function is called to reset directions of links that belong to a cycle. Parameters ---------- links : dict Network links and associated properties. nodes : dict Network nodes and associated properties. imshape : tuple Shape of binary mask as (nrows, ncols). Returns ------- links : dict Network links and associated properties with the directions re-set. nodes : dict Network nodes and associated properties with the directions re-set. """ links, nodes = dy.set_continuity(links, nodes) # Set the directions of the links that are more certain via centerline angle method # alg = 23.1 alg = dy.algmap('cl_ang_rs') cl_angthresh = np.percentile( links['clangs'][np.isnan(links['clangs']) == 0], 40) for lid, cla, lg, lga, cert in zip(links['id'], links['clangs'], links['guess'], links['guess_alg'], links['certain']): if cert == 1: continue if np.isnan(cla) == True: continue if cla <= cl_angthresh: linkidx = links['id'].index(lid) if dy.algmap('cl_ang_guess') in lga: usnode = lg[lga.index(dy.algmap('cl_ang_guess'))] links, nodes = dy.set_link(links, nodes, linkidx, usnode, alg) angthreshs = np.linspace(0, 1.3, 20) for a in angthreshs: links, nodes = dy.set_by_known_flow_directions( links, nodes, imshape, angthresh=a, lenthresh=0, alg=dy.algmap('known_fdr_rs')) if np.sum(links['certain']) != len(links['id']): links_notset = links['id'][np.where(links['certain'] == 0)[0][0]] print('Links {} were not set by re_set_linkdirs.'.format(links_notset)) return links, nodes
def set_unknown_cluster_by_widthpct(links, nodes): """ Set unknown links based on width differences at endpoints. (flow goes wide->narrow) """ alg = 26 # Get indices of uncertain links uc_idx = np.where(links['certain'] == 0)[0].tolist() # Create graph to find clusters of uncertains G = nx.Graph() for idx in uc_idx: lc = links['conn'][idx] G.add_edge(lc[0], lc[1]) # Compute connected components (nodes) cc = nx.connected_components(G) ccs = [c for c in cc if len(c) > 1] # Convert cc nodes to cc edges cc_edges = [] for ccnodes in ccs: ccedge = [] for idx in uc_idx: lc = links['conn'][idx] if lc[0] in ccnodes and lc[1] in ccnodes: ccedge.append(links['id'][idx]) cc_edges.append(ccedge) # Loop through all the unknown clusters, setting the most-certain-by-width for lclust in cc_edges: widpcts = [ links['wid_pctdiff'][links['id'].index(l)][0] for l in lclust ] link_toset = lclust[widpcts.index(max(widpcts))] linkidx = links['id'].index(link_toset) usnode = links['guess'][linkidx][links['guess_alg'][linkidx].index(26)] links, nodes = dy.set_link(links, nodes, linkidx, usnode, alg=alg) return links, nodes
def set_initial_directionality(links, nodes, imshape): """ Make initial attempt to set flow directions. Makes an initial attempt to set all flow directions within the network. This represents the core of the "delta recipe" described in the following open access paper: https://esurf.copernicus.org/articles/8/87/2020/esurf-8-87-2020.pdf However, note that as RivGraph develops, this recipe may no longer match the one presented in that paper. The recipe chains together a number of exploitative algorithms to iteratively set flow directions for the most certain links first. Parameters ---------- links : dict Network links and associated properties. nodes : dict Network nodes and associated properties. imshape : tuple Shape of binary mask as (nrows, ncols). Returns ------- links : dict Network links and associated properties with initial directions set. nodes : dict Network nodes and associated properties with initial directions set. """ # Compute all the "guesses" links, nodes = dy.dir_main_channel(links, nodes) links, nodes = dir_synthetic_DEM(links, nodes, imshape) links, nodes = dy.dir_shortest_paths_nodes(links, nodes) links, nodes = dy.dir_shortest_paths_links(links, nodes) links, nodes = dy.dir_bridges(links, nodes) # Set link directions # First, set inlet/outlet directions as they are always 100% accurate links, nodes = dy.set_inletoutlet(links, nodes) # Use bridges to set links as they are always 100% accurate # alg = 5 alg = dy.algmap('bridges') for lid, idcs, lg, lga, cert in zip(links['id'], links['idx'], links['guess'], links['guess_alg'], links['certain']): # Only need to set links that haven't been set if cert == 1: continue linkidx = links['id'].index(lid) # Set all the links that are known from bridge links if alg in lga: links, nodes = dy.set_link(links, nodes, linkidx, lg[lga.index(alg)], alg) # Use main channels (4) to set links # alg = 4 alg = dy.algmap('main_chans') for lid, idcs, lg, lga, cert in zip(links['id'], links['idx'], links['guess'], links['guess_alg'], links['certain']): # Only need to set links that haven't been set if cert == 1: continue linkidx = links['id'].index(lid) # Set all the links that are known from main_channel if alg in lga: links, nodes = dy.set_link(links, nodes, linkidx, lg[lga.index(alg)], alg) # Set the longest, steepest links according to io_surface # (these are those we are most certain of) # alg = 13 alg = dy.algmap('longest_steepest') len75 = np.percentile(links['len_adj'], 75) slope50 = np.percentile(np.abs(links['slope']), 50) for lid, llen, lg, lga, cert, lslope in zip(links['id'], links['len_adj'], links['guess'], links['guess_alg'], links['certain'], links['slope']): if cert == 1: continue if llen > len75 and abs(lslope) > slope50: linkidx = links['id'].index(lid) if dy.algmap('syn_dem') in lga: usnode = lg[lga.index(dy.algmap('syn_dem'))] links, nodes = dy.set_link(links, nodes, linkidx, usnode, alg) # Set the most certain angles angthreshs = np.linspace(0, 0.5, 10) for a in angthreshs: links, nodes = dy.set_by_known_flow_directions(links, nodes, imshape, angthresh=a) # Set using direction of nearest main channel links, nodes = dy.set_by_nearest_main_channel(links, nodes, imshape, nodethresh=2) # Set the most certain angles angthreshs = np.linspace(0, 0.7, 10) for a in angthreshs: links, nodes = dy.set_by_known_flow_directions(links, nodes, imshape, angthresh=a) # Set using direction of nearest main channel links, nodes = dy.set_by_nearest_main_channel(links, nodes, imshape, nodethresh=1) angthreshs = np.linspace(0, 0.8, 10) for a in angthreshs: links, nodes = dy.set_by_known_flow_directions(links, nodes, imshape, angthresh=a, lenthresh=3) # Use io_surface (3) to set links that are longer # than the median link length alg = dy.algmap('syn_dem') medlinklen = np.median(links['len']) for lid, llen, lg, lga, cert in zip(links['id'], links['len'], links['guess'], links['guess_alg'], links['certain']): if cert == 1: continue if llen > medlinklen and dy.algmap('syn_dem') in lga: linkidx = links['id'].index(lid) usnode = lg[lga.index(dy.algmap('syn_dem'))] links, nodes = dy.set_link(links, nodes, linkidx, usnode, alg) # Set again by angles, but reduce the lenthresh # (shorter links will be set that were not previously) angthreshs = np.linspace(0, 0.6, 10) for a in angthreshs: links, nodes = dy.set_by_known_flow_directions(links, nodes, imshape, angthresh=a, lenthresh=0) # If any three methods agree, set that link to whatever they agree on # alg = 15 alg = dy.algmap('three_agree') for lid, idcs, lg, lga, cert in zip(links['id'], links['idx'], links['guess'], links['guess_alg'], links['certain']): # Only need to set links that haven't been set if cert == 1: continue linkidx = links['id'].index(lid) # Set all the links with 3 or more guesses that agree m = mode(lg) if m.count[0] > 2: links, nodes = dy.set_link(links, nodes, linkidx, m.mode[0], alg) # Set again by angles, but reduce the lenthresh # (shorter links will be set that were not previously) angthreshs = np.linspace(0, 0.8, 10) for a in angthreshs: links, nodes = dy.set_by_known_flow_directions(links, nodes, imshape, angthresh=a, lenthresh=0) # If artificial DEM and at least one shortest path method agree, # set link to be their agreement # alg = 16 alg = dy.algmap('syn_dem_and_sp') for lid, idcs, lg, lga, cert in zip(links['id'], links['idx'], links['guess'], links['guess_alg'], links['certain']): # Only need to set links that haven't been set if cert == 1: continue linkidx = links['id'].index(lid) # Set all the links with 2 or more same guesses that are not # shortest path (one may be shortest path) if dy.algmap('syn_dem') in lga and dy.algmap('sp_links') in lga: if lg[lga.index(dy.algmap('syn_dem'))] == lg[lga.index( dy.algmap('sp_links'))]: links, nodes = dy.set_link(links, nodes, linkidx, lg[lga.index(dy.algmap('syn_dem'))], alg) elif dy.algmap('syn_dem') in lga and dy.algmap('sp_nodes') in lga: if lg[lga.index(dy.algmap('syn_dem'))] == lg[lga.index( dy.algmap('sp_nodes'))]: links, nodes = dy.set_link(links, nodes, linkidx, lg[lga.index(dy.algmap('syn_dem'))], alg) # Find remaining uncertain links uncertain = [l for l, lc in zip(links['id'], links['certain']) if lc != 1] # Set remaining uncertains according to io_surface (3) # alg = 10 # change this one! alg = dy.algmap('syn_dem') for lid in uncertain: linkidx = links['id'].index(lid) if alg in links['guess_alg'][linkidx]: usnode = links['guess'][linkidx][links['guess_alg'][linkidx].index( alg)] links, nodes = dy.set_link(links, nodes, linkidx, usnode, alg) return links, nodes
def set_directionality(links, nodes, Imask, exit_sides, gt, meshlines, meshpolys, Idt, pixlen, manual_set_csv): """ Set direction of each link. This function sets the direction of each link within the network. It calls a number of helping functions and uses a somewhat-complicated logic to achieve this. The algorithms and logic is described in this open-access paper: https://esurf.copernicus.org/articles/8/87/2020/esurf-8-87-2020.pdf Every time this is run, all directionality information is reset and recomputed. This includes checking for manually set links via the provided csv. Parameters ---------- links : dict Network links and associated properties. nodes : dict Network nodes and associated properties. Imask : np.array Binary mask of the network. exit_sides : str Two-character string of cardinal directions denoting the upstream and downsteram sides of the image that the network intersects (e.g. 'SW'). gt : tuple gdal-type GeoTransform of the original binary mask. meshlines : list List of shapely.geometry.LineStrings that define the valleyline mesh. meshpolys : list List of shapely.geometry.Polygons that define the valleyline mesh. Idt : np.array() Distance transform of Imask. pixlen : float Length resolution of each pixel. manual_set_csv : str, optional Path to a user-provided csv file of known link directions. The default is None. Returns ------- links : dict Network links and associated properties with all directions set. nodes : dict Network nodes and associated properties with all directions set. """ imshape = Imask.shape # Add fields to links dict for tracking and setting directionality links, nodes = dy.add_directionality_trackers(links, nodes, 'river') # If a manual fix csv has been provided, set those links first links, nodes = dy.dir_set_manually(links, nodes, manual_set_csv) # Append morphological information used to set directionality to links dict links, nodes = directional_info(links, nodes, Imask, pixlen, exit_sides, gt, meshlines, meshpolys, Idt) # Begin setting link directionality # First, set inlet/outlet directions as they are always 100% accurate links, nodes = dy.set_inletoutlet(links, nodes) # Set the directions of the links that are more certain via centerline # distance method # alg = 22 alg = dy.algmap('cl_dist_set') cl_distthresh = np.percentile(links['cldists'], 85) for lid, cld, lg, lga, cert in zip(links['id'], links['cldists'], links['guess'], links['guess_alg'], links['certain']): if cert == 1: continue if cld >= cl_distthresh: linkidx = links['id'].index(lid) if dy.algmap('cl_dist_guess') in lga: usnode = lg[lga.index(dy.algmap('cl_dist_guess'))] links, nodes = dy.set_link(links, nodes, linkidx, usnode, alg) # Set the directions of the links that are more certain via centerline # angle method # alg = 23 alg = dy.algmap('cl_ang_set') cl_angthresh = np.percentile( links['clangs'][np.isnan(links['clangs']) == 0], 25) for lid, cla, lg, lga, cert in zip(links['id'], links['clangs'], links['guess'], links['guess_alg'], links['certain']): if cert == 1: continue if np.isnan(cla) == True: continue if cla <= cl_angthresh: linkidx = links['id'].index(lid) if dy.algmap('cl_ang_guess') in lga: usnode = lg[lga.index(dy.algmap('cl_ang_guess'))] links, nodes = dy.set_link(links, nodes, linkidx, usnode, alg) # Set the directions of the links that are more certain via centerline # distance AND centerline angle methods # alg = 24 alg = dy.algmap('cl_dist_and_ang') cl_distthresh = np.percentile(links['cldists'], 70) ang_thresh = np.percentile(links['clangs'][np.isnan(links['clangs']) == 0], 35) for lid, cld, cla, lg, lga, cert in zip(links['id'], links['cldists'], links['clangs'], links['guess'], links['guess_alg'], links['certain']): if cert == 1: continue if cld >= cl_distthresh and cla < ang_thresh: linkidx = links['id'].index(lid) if dy.algmap('cl_dist_guess') in lga and dy.algmap( 'cl_ang_guess') in lga: if lg[lga.index(dy.algmap('cl_dist_guess'))] == lg[lga.index( dy.algmap('cl_ang_guess'))]: usnode = lg[lga.index(dy.algmap('cl_dist_guess'))] links, nodes = dy.set_link(links, nodes, linkidx, usnode, alg) # Set directions by most-certain angles angthreshs = np.linspace(0, 0.4, 10) for a in angthreshs: links, nodes = dy.set_by_known_flow_directions(links, nodes, imshape, angthresh=a, lenthresh=3) # Set using direction of nearest main channel links, nodes = dy.set_by_nearest_main_channel(links, nodes, imshape, nodethresh=1) angthreshs = np.linspace(0, 1.5, 20) for a in angthreshs: links, nodes = dy.set_by_known_flow_directions(links, nodes, imshape, angthresh=a) # At this point, if any links remain unset, they are just set randomly if np.sum(links['certain']) != len(links['id']): print('{} links were randomly set.'.format( len(links['id']) - np.sum(links['certain']))) links['certain'] = np.ones((len(links['id']), 1)) # Check for and try to fix cycles in the graph links, nodes, cantfix_cyclelinks, cantfix_cyclenodes = fix_river_cycles( links, nodes, imshape) # Check for sources or sinks within the graph cont_violators = dy.check_continuity(links, nodes) # Summary of problems: manual_fix = 0 if len(cantfix_cyclelinks) > 0: print('Could not fix cycle links: {}.'.format(cantfix_cyclelinks)) manual_fix = 1 else: print('All cycles were resolved.') if len(cont_violators) > 0: print('Continuity violated at nodes {}.'.format(cont_violators)) manual_fix = 1 # Create a csv to store manual edits to directionality if does not exist if manual_fix == 1: if os.path.isfile(manual_set_csv) is False: io.create_manual_dir_csv(manual_set_csv) print('A .csv file for manual fixes to link directions at {}.'. format(manual_set_csv)) else: print('Use the csv file at {} to manually fix link directions.'. format(manual_set_csv)) return links, nodes