Example #1
0
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
Example #2
0
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
Example #3
0
 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
Example #4
0
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}
Example #5
0
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
Example #6
0
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
Example #7
0
 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
Example #8
0
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
Example #9
0
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)
Example #10
0
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