def arbolate_sum(segment, lengths, routing): """Compute the total length of all tributaries upstream from segment, including that segment, using the supplied lengths and routing connections. Parameters ---------- segment : int or list of ints Segment or node number that is also a key in the lengths and routing dictionaries. lengths : dict Dictionary of lengths keyed by segment (node) numbers, including those in segment. routing : dict Dictionary describing routing connections between segments (nodes); values represent downstream connections. Returns ------- asum : float or dict Arbolate sums for each segment. """ scalar = False if np.isscalar(segment): scalar = True segment = [segment] graph_r = make_graph(list(routing.values()), list(routing.keys())) asum = {} for s in segment: upsegs = get_upsegs(graph_r, s) lnupsegs = [lengths[s] for s in upsegs] asum[s] = np.sum(lnupsegs) + lengths[s] return asum
def valid_nsegs(nsegs, outsegs=None, increasing=True): """Check that segment numbers are valid. Parameters ---------- nsegs : list of segment numbers outsegs : list of corresponding routing connections Required if increasing=True. increasing : bool If True, segment numbers must also only increase downstream. """ # cast to array if list or series nsegs = np.atleast_1d(nsegs) outsegs = np.atleast_1d(outsegs) consecutive_and_onebased = valid_rnos(nsegs) if increasing: assert outsegs is not None graph = make_graph(nsegs, outsegs, one_to_many=False) monotonic = [] for s in nsegs: seg_sequence = find_path(graph.copy(), s)[:-1] # last number is 0 for outlet monotonic.append(np.all(np.diff(np.array(seg_sequence)) > 0)) monotonic = np.all(monotonic) return consecutive_and_onebased & monotonic else: return consecutive_and_onebased
def routing(self): """Dictionary of routing connections from ids (keys) to to_ids (values). """ if self._routing is None or self._routing_changed(): toid = self.df.toid.values # check whether or not routing is # many-to-one or one-to-one (no diversions) # squeeze it down to_one = False # if below == True, all toids are scalar or length 1 lists if len(toid) > 1: to_one = is_to_one(toid) # if not, try converting any scalars to lists if not to_one: toid = [[l] if np.isscalar(l) else l for l in toid] to_one = is_to_one(toid) toid = np.squeeze(list(toid)) routing = make_graph(self.df.id.values, toid, one_to_many=not to_one) if not to_one: routing = pick_toids(routing, self.elevup) else: routing = {self.df.id.values[0]: 0} self._routing = routing return self._routing
def test_get_upsegs(sfr_test_numbering): rd, sd = sfr_test_numbering graph = dict(zip(sd.nseg, sd.outseg)) graph_r = make_graph(list(graph.values()), list(graph.keys())) upsegs = [] for s in sd.nseg: upsegs.append(get_upsegs(graph_r, s)) assert upsegs[1] == {1, 3, 4, 5, 6, 7, 8, 9}
def arbolate_sum(segment, lengths, routing, starting_asums=None): """Compute the total length of all tributaries upstream from segment, including that segment, using the supplied lengths and routing connections. Parameters ---------- segment : int or list of ints Segment or node number that is also a key in the lengths and routing dictionaries. lengths : dict Dictionary of lengths keyed by segment (node) numbers, including those in segment. routing : dict Dictionary describing routing connections between segments (nodes); values represent downstream connections. starting_asums : dict Option to supply starting arbolate sum values for any of the segments. By default, None. Returns ------- asum : float or dict Arbolate sums for each segment. """ if np.isscalar(segment): segment = [segment] graph_r = make_graph(list(routing.values()), list(routing.keys())) asum = {} for s in segment: upsegs = get_upsegs(graph_r, s) lnupsegs = [lengths[us] for us in upsegs] upstream_starting_asums = [0.] segment_starting_asum = 0. if starting_asums is not None: upstream_starting_asums = [ starting_asums.get(us, 0.) for us in upsegs ] segment_starting_asum = starting_asums.get(s, 0.) asum[s] = np.sum(lnupsegs) + lengths[s] + np.sum( upstream_starting_asums) + segment_starting_asum return asum
def routing_is_circular(fromid, toid): """Verify that segments or reaches never route to themselves. Parameters ---------- fromid : list or 1D array e.g. COMIDS, segments, or rnos toid : list or 1D array routing connections """ fromid = np.atleast_1d(fromid) toid = np.atleast_1d(toid) graph = make_graph(fromid, toid, one_to_many=False) paths = {fid: find_path(graph, fid) for fid in graph.keys()} # a fromid should not appear more than once in its sequence for k, v in paths.items(): if v.count(k) > 1: return True return False
def routing(self): if self._routing is None or self._routing_changed(): toid = self.df.toid.values # check whether or not routing is # many-to-one or one-to-one (no diversions) # squeeze it down to_one = False # if below == True, all toids are scalar or length 1 lists to_one = np.isscalar(np.squeeze(toid)[0]) # if not, try converting any scalars to lists if not to_one: toid = [[l] if np.isscalar(l) else l for l in toid] to_one = np.isscalar(np.squeeze(toid)[0]) toid = np.squeeze(toid) routing = make_graph(self.df.id.values, toid, one_to_many=not to_one) if not to_one: routing = pick_toids(routing, self.elevup) self._routing = routing return self._routing
def smooth_elevations(fromids, toids, elevations, start_elevations=None): # elevup, elevdn): """ Parameters ---------- fromids : sequence of hashables toids : sequence of hashables Downstream connections of fromids elevations : sequence of floats Elevation for each edge (line) in a stream network, or if start_elevations are specified, the end elevation for each edge. start_elevations : sequence of floats, optional Start elevation for edge (line) in a stream network. By default, None. Returns ------- Elevations : dict or tuple Dictionary of smoothed edge elevations, or smoothed end elevations, start elevations """ # make forward and reverse dictionaries with routing info graph = dict(zip(fromids, toids)) assert 0 in set(graph.values()), 'No outlets in routing network!' graph_r = make_graph(toids, fromids) # make dictionaries of segment end elevations elevations = dict(zip(fromids, elevations)) if start_elevations is not None: elevmax = dict(zip(fromids, start_elevations)) def get_upseg_levels(seg): """Traverse routing network, returning a list of segments at each level upstream from the outlets. (level 0 route to seg; segments in level 1 route to a segment in level 0, etc.) Parameters: ----------- seg : int Starting segment number Returns ------- all_upsegs : list List with list of segments at each level """ upsegs = graph_r[seg].copy() all_upsegs = [upsegs] for i in range(len(fromids)): upsegs = get_nextupsegs(graph_r, upsegs) if len(upsegs) > 0: all_upsegs.append(upsegs) else: break return all_upsegs def reset_elevations(seg): """Reset segment elevations above (upsegs) and below (outseg) a node. """ oseg = graph[seg] all_upsegs = np.array(list(get_upsegs(graph_r, seg)) + [seg]) # all segments upstream of node elevmin_s = np.min([elevations[s] for s in all_upsegs ]) # minimum current elevation upstream of node oldmin_s = elevations[seg] elevs = [elevmin_s, oldmin_s] if oseg > 0: # if segment is not an outlet, if start_elevations is not None: elevs.append( elevmax[oseg]) # outseg start elevation (already updated) # set segment end elevation as min of # upstream elevations, current elevation, outseg start elevation elevations[seg] = np.min(elevs) # if the node is not an outlet, reset the outseg max if the current min is lower if oseg > 0: if start_elevations is not None: next_reach_elev = elevmax[oseg] elevmax[graph[seg]] = np.min([elevmin_s, next_reach_elev]) else: next_reach_elev = elevations[oseg] elevations[graph[seg]] = np.min([elevmin_s, next_reach_elev]) print('\nSmoothing elevations...') ta = time.time() # get list of segments at each level, starting with 0 (outlet) segment_levels = get_upseg_levels(0) # at each level, reset all of the segment elevations as necessary for level in segment_levels: for s in level: if 0 in level: j = 2 reset_elevations(s) print("finished in {:.2f}s".format(time.time() - ta)) if start_elevations is not None: return elevations, elevmax return elevations
def get_inflow_locations_from_parent_model(parent_reach_data, inset_reach_data, inset_grid, active_area=None): """Get places in an inset model SFR network where the parent SFR network crosses the inset model boundary, using common line ID numbers from parent and inset reach datasets. MF2005 or MF6 supported; if either dataset contains only reach numbers (is MODFLOW-6), the reach numbers are used as segment numbers, with each segment only having one reach. Parameters ---------- parent_reach_data : str (filepath) or DataFrame SFR reach data for parent model. Must include columns: line_id : int; unique identifier for hydrography line that each reach is based on rno : int; unique identifier for each reach. Optional if iseg and ireach columns are included. iseg : int; unique identifier for each segment. Optional if rno is included. ireach : int; unique identifier for each reach. Optional if rno is included. geometry : shapely.geometry object representing location of each reach inset_reach_data : str (filepath) or DataFrame SFR reach data for inset model. Same columns as parent_reach_data, except a geometry column isn't needed. line_id values must correspond to same source hydrography as those in parent_reach_data. inset_grid : flopy.discretization.StructuredGrid instance describing model grid Must be in same coordinate system as geometries in parent_reach_data. Required only if active_area is None. active_area : shapely.geometry.Polygon object Describes the area of the inset model where SFR is applied. Used to find inset reaches from parent model. Must be in same coordinate system as geometries in parent_reach_data. Required only if inset_grid is None. Returns ------- locations : DataFrame Columns: parent_segment : parent model segment parent_reach : parent model reach parent_rno : parent model reach number line_id : unique identifier for hydrography line that each reach is based on """ # spatial reference instances defining parent and inset grids if isinstance(inset_grid, str): grid = load_modelgrid(inset_grid) elif isinstance(inset_grid, flopy.discretization.grid.Grid): grid = inset_grid else: raise ValueError('Unrecognized input for inset_grid') if active_area is None: l, r, b, t = grid.extent active_area = box(l, b, r, t) # parent and inset reach data if isinstance(parent_reach_data, str): prd = shp2df(parent_reach_data) elif isinstance(parent_reach_data, pd.DataFrame): prd = parent_reach_data.copy() else: raise ValueError('Unrecognized input for parent_reach_data') if 'rno' in prd.columns and 'iseg' not in prd.columns: prd['iseg'] = prd['rno'] prd['ireach'] = 1 mustinclude_cols = {'line_id', 'rno', 'iseg', 'ireach', 'geometry'} assert len(mustinclude_cols.intersection( prd.columns)) == len(mustinclude_cols) if isinstance(inset_reach_data, str): if inset_reach_data.endswith('.shp'): ird = shp2df(inset_reach_data) else: ird = pd.read_csv(inset_reach_data) elif isinstance(inset_reach_data, pd.DataFrame): ird = inset_reach_data.copy() else: raise ValueError('Unrecognized input for inset_reach_data') if 'rno' in ird.columns and 'iseg' not in ird.columns: ird['iseg'] = ird['rno'] ird['ireach'] = 1 mustinclude_cols = {'line_id', 'rno', 'iseg', 'ireach'} assert len(mustinclude_cols.intersection( ird.columns)) == len(mustinclude_cols) graph = make_graph(ird.rno.values, ird.outreach.values, one_to_many=False) # cull parent reach data to only lines that cross or are just upstream of inset boundary buffered = active_area.buffer(5000, cap_style=2) close = [g.intersects(buffered) for g in prd.geometry] prd = prd.loc[close] prd.index = prd.rno boundary = active_area.exterior inset_line_id_connections = {} # parent rno: inset line_id for i, r in prd.iterrows(): if r.outreach not in prd.index: continue downstream_line = prd.loc[r.outreach, 'geometry'] upstream_line = prd.loc[prd.rno == r.outreach, 'geometry'].values[0] intersects = r.geometry.intersects(boundary) intersects_downstream = downstream_line.within(active_area) # intersects_upstream = upstream_line.within(active_area) in_inset_model = r.geometry.within(active_area) if intersects_downstream: if intersects: # if not intersects_upstream: # exclude lines that originated within the model # # lines that cross route to their counterpart in inset model inset_line_id_connections[r.rno] = r.line_id pass elif not in_inset_model: # lines that route to a line within the inset model # route to that line's inset counterpart inset_line_id_connections[r.rno] = prd.loc[r.outreach, 'line_id'] pass prd = prd.loc[prd.rno.isin(inset_line_id_connections.keys())] # parent rno lookup parent_rno_lookup = {v: k for k, v in inset_line_id_connections.items()} # inlet reaches in inset model ird = ird.loc[ird.ireach == 1] ird = ird.loc[ird.line_id.isin(inset_line_id_connections.values())] # for each reach in ird (potential inset inlets) # check that there isn't another inlet downstream drop_reaches = [] for i, r in ird.iterrows(): path = find_path(graph, r.rno) another_inlet_downstream = len( set(path[1:]).intersection(set(ird.rno))) > 0 if another_inlet_downstream: drop_reaches.append(r.rno) ird = ird.loc[~ird.rno.isin(drop_reaches)] # cull parent flows to outlet reaches iseg_ireach = zip(prd.iseg, prd.ireach) parent_outlet_iseg_ireach = dict(zip(prd.rno, iseg_ireach)) df = ird[['line_id', 'name', 'rno', 'iseg', 'ireach']].copy() df['parent_rno'] = [parent_rno_lookup[lid] for lid in df['line_id']] df['parent_iseg'] = [ parent_outlet_iseg_ireach[rno][0] for rno in df['parent_rno'] ] df['parent_ireach'] = [ parent_outlet_iseg_ireach[rno][1] for rno in df['parent_rno'] ] return df.reset_index(drop=True)
def smooth_elevations(fromids, toids, elevations): # elevup, elevdn): # make forward and reverse dictionaries with routing info graph = dict(zip(fromids, toids)) assert 0 in set(graph.values()), 'No outlets in routing network!' graph_r = make_graph(toids, fromids) # make dictionaries of segment end elevations elevations = dict(zip(fromids, elevations)) # elevmax = dict(zip(fromids, elevup)) def get_upseg_levels(seg): """Traverse routing network, returning a list of segments at each level upstream from the outlets. (level 0 route to seg; segments in level 1 route to a segment in level 0, etc.) Parameters: ----------- seg : int Starting segment number Returns ------- all_upsegs : list List with list of segments at each level """ upsegs = graph_r[seg].copy() all_upsegs = [upsegs] for i in range(len(fromids)): upsegs = get_nextupsegs(graph_r, upsegs) if len(upsegs) > 0: all_upsegs.append(upsegs) else: break return all_upsegs def reset_elevations(seg): """Reset segment elevations above (upsegs) and below (outseg) a node. """ oseg = graph[seg] all_upsegs = np.array(list(get_upsegs(graph_r, seg)) + [seg]) # all segments upstream of node elevmin_s = np.min([elevations[s] for s in all_upsegs ]) # minimum current elevation upstream of node oldmin_s = elevations[seg] elevs = [elevmin_s, oldmin_s] if oseg > 0: # if segment is not an outlet, pass # elevs.append(elevmax[oseg]) # outseg start elevation (already updated) # set segment end elevation as min of # upstream elevations, current elevation, outseg start elevation elevations[seg] = np.min(elevs) # if the node is not an outlet, reset the outseg max if the current min is lower if oseg > 0: # outseg_max = elevmax[oseg] next_reach_elev = elevations[oseg] elevations[graph[seg]] = np.min([elevmin_s, next_reach_elev]) print('\nSmoothing elevations...') ta = time.time() # get list of segments at each level, starting with 0 (outlet) segment_levels = get_upseg_levels(0) # at each level, reset all of the segment elevations as necessary for level in segment_levels: [reset_elevations(s) for s in level] print("finished in {:.2f}s".format(time.time() - ta)) return elevations