{"action":"created","comment":{"url":"https://api.github.com/repos/UW-Hydro/RVIC/pulls/comments/31013520","id":31013520,"diff_hunk":"@@ -0,0 +1,689 @@\n+import numpy as np\n+import pandas as pd\n+import sys\n+import ast\n+\n+class flow_grid(object):        \n+    \"\"\"\n+    Container class for holding and manipulating gridded VIC routing data.\n+    Can be instantiated with optional keyword arguments. These keyword\n+    arguments will be used to add a dataset (dem, flowdir, accumulation)\n+    to the flow_grid instance.\n+\n+    Parameters\n+    ----------\n+    data : string or numpy ndarray (optional)\n+           Data to be read. Can either be a file name or an array.\n+           If data is from a file, 'input_type' should be set to the\n+           appropriate value ('ascii' or 'raster').\n+    data_type : 'dem', 'dir', 'acc' (optional)\n+                 How to interpret the input data:\n+                     'dem' : digital elevation data\n+                     'dir' : flow direction data\n+                     'acc' : flow accumulation (upstream area) data\n+                \n+    input_type : 'raster', 'ascii' or 'array' (optional)\n+                 Type of input data.\n+    band : int (optional)\n+           For raster data, the band number to read.\n+    nodata : int or float (optional)\n+             Value indicating no data.\n+\n+    Attributes (Optional)\n+    ---------------------\n+    dem : digital elevation grid\n+    dir : flow direction grid\n+    acc : flow accumulation grid\n+    catch : Catchment delineated from 'dir' and a given pour point\n+    frac : fractional contributing area grid\n+    bbox : The geographical bounding box of the gridded dataset\n+           (xmin, ymin, xmax, ymax)\n+    shape : The shape of the gridded data (nrows, ncolumns)\n+    cellsize : The length/width of each grid cell (assumed to be square).\n+    nodata : The value to use for gridcells with no data.\n+\n+    Methods\n+    -------\n+    read_input : add a gridded dataset (dem, flowdir, accumulation) \n+                 to flow_grid instance.\n+    nearest_cell : Returns the index (column, row) of the cell closest\n+                   to a given geographical coordinate (x, y).\n+    flowdir : Generate a flow direction grid from a given digital elevation\n+              dataset (dem).\n+    catchment : Delineate the watershed for a given pour point (x, y)\n+                or (column, row).\n+    fraction : Generate the fractional contributing area for a coarse\n+               scale flow direction grid based on a fine-scale flow\n+               direction grid.\n+    \"\"\"\n+\n+    def __init__(self, conform_data=True, **kwargs):\n+        self.conform_data = conform_data\n+        self.nodata = {}\n+        self.gridlist = []\n+        if 'data' in kwargs:\n+            self.read_input(**kwargs)\n+        else:\n+            pass\n+\n+    def read_input(self, data, data_type='dir', input_type='ascii', band=1,\n+                   nodata=0, bbox=None, crs=None, **kwargs):\n+        \"\"\"\n+        Reads data into a named attribute of flow_grid\n+        (name of attribute determined by 'data_type').\n+\n+        Parameters\n+        ----------\n+        data : File name (string) or numpy ndarray\n+               If data is from a file, 'input_type' should be set to the\n+               appropriate value ('ascii' or 'raster').\n+        data_type : 'dem', 'dir', 'acc' or other string\n+                     Name of dataset. Will determine the name of the attribute\n+                     representing the gridded data. Default values are used\n+                     internally by some class methods:\n+                         'dem' : digital elevation data\n+                         'dir' : flow direction data\n+                         'acc' : flow accumulation (upstream area) data\n+                    \n+        input_type : 'raster', 'ascii' or 'array'\n+                     Type of input data.\n+        band : int\n+               For raster data, the band number to read.\n+        nodata : int or float\n+                 Value indicating no data.\n+        bbox : tuple or list\n+               Bounding box, if none provided.\n+\n+        \"\"\"\n+\n+        # read ascii file\n+        if input_type == 'ascii':\n+            with open(data) as header:\n+                ncols = int(header.readline().split()[1])\n+                nrows = int(header.readline().split()[1])\n+                xll = ast.literal_eval(header.readline().split()[1])\n+                yll = ast.literal_eval(header.readline().split()[1])\n+                cellsize = ast.literal_eval(header.readline().split()[1])\n+                nodata = ast.literal_eval(header.readline().split()[1])\n+                shape = (nrows, ncols)\n+                bbox = (xll, yll, xll + ncols*cellsize, yll + nrows*cellsize)\n+            data = np.loadtxt(data, skiprows=6, **kwargs)\n+            nodata = data.dtype.type(nodata)\n+\n+        # read raster file\n+        if input_type == 'raster':\n+            import rasterio\n+            f = rasterio.open(data)\n+            crs = f.crs\n+            bbox = tuple(f.bounds)\n+            shape = f.shape\n+            cellsize = f.affine[0]\n+            nodata = f.nodatavals[0]\n+            if len(f.indexes) > 1:\n+                data = np.ma.filled(f.read_band(band))\n+            else:\n+                data = np.ma.filled(f.read())\n+                f.close()\n+                data = data.reshape(shape)\n+            nodata = data.dtype.type(nodata)\n+\n+        # read numpy array\n+        if input_type == 'array':\n+            shape = data.shape\n+\n+        # if there are no datasets, initialize bbox, shape,\n+        # cellsize and crs based on incoming data\n+        if len(self.gridlist) < 1:\n+            self.bbox = bbox\n+            self.shape = shape\n+            self.cellsize = cellsize\n+            self.crs = crs\n+        # if there are existing datasets, conform incoming\n+        # data to bbox\n+        else:\n+            if self.conform_data == True:\n+                np.testing.assert_almost_equal(cellsize, self.cellsize)\n+                try:\n+                    np.testing.assert_almost_equal(bbox, self.bbox)\n+                except AssertionError:\n+                    data = self.conform(data, bbox, fill=nodata)\n+                    shape = data.shape\n+                assert(shape == self.shape)\n+\n+        self.shape_min = np.min_scalar_type(max(self.shape))\n+        self.size_min = np.min_scalar_type(data.size)\n+\n+        # assign new data to attribute; record nodata value\n+        self.gridlist.append(data_type)\n+        self.nodata.update({data_type : nodata})\n+        setattr(self, data_type, data)\n+\n+    def nearest_cell(self, x, y):\n+        \"\"\"\n+        Returns the index of the cell (column, row) closest\n+        to a given geographical coordinate.\n+\n+        Parameters\n+        ----------\n+        x : int or float\n+            x coordinate.\n+        y : int or float\n+            y coordinate.\n+        \"\"\"\n+\n+        # create coordinate grid of cell centroids based on bbox\n+        coords = np.meshgrid(\n+            np.linspace(self.bbox[0] + self.cellsize/2.0,\n+                        self.bbox[2] + self.cellsize/2.0,\n+                        self.shape[1], endpoint=False),\n+            np.linspace(self.bbox[1] + self.cellsize/2.0,\n+                        self.bbox[3] + self.cellsize/2.0,\n+                        self.shape[0], endpoint=False)[::-1])\n+\n+        # select nearest cell based on euclidian distance\n+        nearest = np.unravel_index(np.argmin(np.sqrt((\n+                                   coords[0] - x)**2 + (coords[1] - y)**2)),\n+                                   self.shape)\n+        return nearest[1], nearest[0]\n+\n+    def flowdir(self, data=None, include_edges=True, dirmap=(1,2,3,4,5,6,7,8),\n+                nodata=0, flat=-1, inplace=True):\n+        \"\"\"\n+        Generates a flow direction grid from a DEM grid.\n+\n+        Parameters\n+        ----------\n+        data : numpy ndarray\n+               Array representing DEM grid\n+        include_edges : bool\n+                        Whether to include outer rim of grid.\n+        dirmap : list or tuple\n+                 List of integer values representing the following\n+                 cardinal and intercardinal directions (in order):\n+                 [N, NE, E, SE, S, SW, W, NW]\n+        \"\"\"\n+\n+        # if data not provided, use self.dem\n+        if data is None:\n+            if hasattr(self, 'dem'):\n+                data = self.dem\n+\n+        # generate grid of indices\n+        indices = np.indices(self.shape, dtype=self.shape_min)\n+\n+        # handle nodata values in dem\n+        dem_nodata = self.nodata['dem']\n+        dem_mask = (data == dem_nodata)        \n+        np.place(data, dem_mask, np.iinfo(data.dtype.type).max)\n+\n+        # initialize indices of corners\n+        corner = {\n+        'nw' : {'k' : tuple(indices[:,0,0]),\n+                'v' : [[0,1,1], [1,1,0]],\n+                'pad': np.array([3,4,5])},\n+        'ne' : {'k' : tuple(indices[:,0,-1]),\n+                'v' : [[1,1,0], [-1,-2,-2]],\n+                'pad': np.array([5,6,7])},\n+        'sw' : {'k' : tuple(indices[:,-1,0]),\n+                'v' : [[-2,-2,-1], [0,1,1]],\n+                'pad': np.array([1,2,3])},\n+        'se' : {'k' : tuple(indices[:,-1,-1]),\n+                'v' : [[-1,-2,-2], [-2,-2,-1]],\n+                'pad': np.array([7,8,1])}\n+        }\n+    \n+        # initialize indices of edges\n+        edge = {\n+        'n' : {'k' : tuple(indices[:,0,1:-1]),\n+               'pad' : np.array([3,4,5,6,7])},\n+        'w' : {'k' : tuple(indices[:,1:-1,0]),\n+               'pad' : np.array([1,2,3,4,5])},\n+        'e' : {'k' : tuple(indices[:,1:-1,-1]),\n+               'pad' : np.array([1,5,6,7,8])},\n+        's' : {'k' : tuple(indices[:,-1,1:-1]),\n+               'pad' : np.array([1,2,3,7,8])}\n+        }\n+    \n+        # initialize indices of body (all cells except edges and corners)\n+        body = indices[:, 1:-1, 1:-1]\n+    \n+        # initialize output array\n+        outmap = np.full(self.shape, nodata, dtype=np.int8)\n+    \n+        # select the eight cells surrounding a cell\n+        def select_surround(i, j):\n+            return ([i-1, i-1, i+0, i+1, i+1, i+1, i+0, i-1],\n+                   [j+0, j+1, j+1, j+1, j+0, j-1, j-1, j-1])\n+    \n+        # select the five cells surrounding an edge cell\n+        def select_edge_sur(k):\n+            i,j = edge[k]['k']\n+            if k == 'n':\n+                return [i+0, i+1, i+1, i+1, i+0], [j+1, j+1, j+0, j-1, j-1]\n+            elif k =='e':\n+                return [i-1, i+1, i+1, i+0, i-1], [j+0, j+0, j-1, j-1, j-1]\n+            elif k =='s':\n+                return [i-1, i-1, i+0, i+0, i-1], [j+0, j+1, j+1, j-1, j-1]\n+            elif k == 'w':\n+                return [i-1, i-1, i+0, i+1, i+1], [j+0, j+1, j+1, j+1, j+0]\n+     \n+        # for each entry in \"body\" determine flow direction based\n+        # on steepest neighboring slope\n+        for i, j in np.nditer(tuple(body), flags=['external_loop']):\n+            dat = data[i,j]\n+            sur = data[select_surround(i,j)]\n+            a = ((dat - sur) > 0).any(axis=0)\n+            b = np.argmax((dat - sur), axis=0) + 1\n+            c = flat\n+            outmap[i,j] = np.where(a,b,c)\n+\n+        # determine flow direction for edges and corners, if desired\n+        if include_edges == True:\n+\n+            # fill corners\n+            for i in corner.keys():\n+                dat = data[corner[i]['k']]\n+                sur = data[corner[i]['v']]\n+                if ((dat - sur) > 0).any():\n+                    outmap[corner[i]['k']] = corner[i]['pad'][np.argmax(dat - sur)]\n+                else:\n+                    outmap[corner[i]['k']] = flat\n+\n+            # fill edges\n+            for x in edge.keys():\n+                dat = data[edge[x]['k']]\n+                sur = data[select_edge_sur(x)]\n+                a = ((dat - sur) > 0).any(axis=0)\n+                b = edge[x]['pad'][np.argmax((dat - sur), axis=0)]\n+                c = flat\n+                outmap[edge[x]['k']] = np.where(a,b,c)\n+    \n+        # If direction numbering isn't default, convert values of output array.\n+        if dirmap != (1,2,3,4,5,6,7,8):\n+            dir_d = dict(zip((1,2,3,4,5,6,7,8), dirmap))\n+            outmap = pd.DataFrame(outmap).apply(lambda x: x.map(dir_d), axis=1).values\n+\n+        np.place(outmap, dem_mask, nodata)\n+        np.place(data, dem_mask, dem_nodata)        \n+        \n+        if inplace == True:\n+            self.dir = outmap\n+        else:\n+            return outmap\n+\n+    def catchment(self, x, y, pour_value=None, dirmap=(1,2,3,4,5,6,7,8),\n+                  nodata=0, xytype='index', recursionlimit=15000, inplace=True):\n+        \"\"\"\n+        Delineates a watershed from a given pour point (x, y).\n+        Returns a grid \n+\n+        Parameters\n+        ----------\n+        x : int or float\n+            x coordinate of pour point\n+        y : int or float\n+            y coordinate of pour point\n+        pour_value : int or None\n+                     If not None, value to represent pour point in catchment\n+                     grid (required by some programs).\n+        dirmap : list or tuple\n+                 List of integer values representing the following\n+                 cardinal directions (in order):\n+                 [N, NE, E, SE, S, SW, W, NW] \n+        xytype : 'index' or 'label'\n+                 How to interpret parameters 'x' and 'y'.\n+                     'index' : x and y represent the column and row \n+                               indices of the pour point.\n+                     'label' : x and y represent geographic coordinates\n+                               (will be passed to self.nearest_cell).\n+        recursionlimit : int\n+                         Recursion limit--may need to be raised if\n+                         recursion limit is reached.\n+        inplace : bool\n+                  If True, catchment will be written to attribute 'catch'.\n+        \"\"\"\n+\n+        # if xytype is 'label', delineate catchment based on cell nearest\n+        # to given geographic coordinate\n+        if xytype == 'label':\n+            x, y = self.nearest_cell(x, y)\n+\n+        # set recursion limit (needed for large datasets)\n+        sys.setrecursionlimit(recursionlimit)\n+\n+        # initialize array to collect catchment cells\n+        self.collect = np.array([], dtype=int)\n+\n+        # pad the flow direction grid with a rim of 'nodata' cells\n+        # easy way to prevent catchment search from going out of bounds\n+        try:\n+            self.cdir = np.pad(self.dir, 1, mode='constant',\n+                               constant_values=np.asscalar(self.nodata['dir']))\n+        except ValueError:\n+            self.cdir = np.pad(self.dir, 1, mode='constant')\n+\n+        # get shape of padded flow direction array, then flatten\n+        padshape = self.cdir.shape\n+        self.cdir = self.cdir.ravel()\n+        \n+        # get the flattened index of the pour point\n+        pour_point = np.ravel_multi_index(np.array([y+1, x+1]), padshape)\n+\n+        # reorder direction mapping to work with select_surround_ravel()\n+        dirmap = np.array(dirmap)[[4,5,6,7,0,1,2,3]].tolist()\n+\n+        # select cells surrounding a given cell for a flattened array\n+        # used by catchment_search()\n+        def select_surround_ravel(i):\n+            return np.array([i + 0 - padshape[1],\n+                             i + 1 - padshape[1],\n+                             i + 1 + 0,\n+                             i + 1 + padshape[1],\n+                             i + 0 + padshape[1],\n+                             i - 1 + padshape[1],\n+                             i - 1 + 0,\n+                             i - 1 - padshape[1]]).T\n+\n+        # for each cell j, recursively search through grid to determine\n+        # if surrounding cells are in the contributing area, then add\n+        # flattened indices to self.collect\n+        def catchment_search(j):\n+            self.collect = np.append(self.collect, j)\n+            selection = select_surround_ravel(j)\n+            next_idx = selection[np.where(self.cdir[selection] == dirmap)]\n+            if next_idx.any():\n+                return catchment_search(next_idx)\n+\n+        # call catchment search starting at the pour point\n+        catchment_search(pour_point)\n+\n+        # initialize output array\n+        outcatch = np.zeros(padshape, dtype=int)\n+\n+        # if nodata is not 0, replace 0 with nodata value in output array\n+        if nodata != 0:\n+            np.place(outcatch, outcatch == 0, nodata)\n+\n+        # set values of output array based on 'collected' cells\n+        outcatch.flat[self.collect] = self.cdir[self.collect]\n+\n+        # remove outer rim, delete temporary arrays\n+        outcatch = outcatch[1:-1, 1:-1]\n+        del self.cdir\n+        del self.collect\n+\n+        # if pour point needs to be a special value, set it\n+        if pour_value is not None:\n+            outcatch[y,x] = pour_value \n+\n+        # if inplace is True, update attributes\n+        if inplace == True:\n+            self.catch = outcatch\n+            self.nodata.update({'catch' : nodata})\n+            self.gridlist.append('catch')\n+        else:\n+            return outcatch\n+\n+    def fraction(self, other, nodata=0, inplace=True):\n+        \"\"\"\n+        Generates a grid representing the fractional contributing area for a\n+        coarse-scale flow direction grid.\n+\n+        Parameters\n+        ----------\n+        other : flow_grid instance\n+                Another flow_grid instance containing fine-scale flow direction\n+                data. The ratio of self.cellsize/other.cellsize must be a\n+                positive integer. Grid cell boundaries must have some overlap.\n+                Must have attributes 'dir' and 'catch' (i.e. must have a flow\n+                direction grid, along with a delineated catchment).\n+                \n+        inplace : bool (optional)\n+                  If True, appends fraction grid to attribute 'frac'.\n+        \"\"\"\n+\n+        # check for required attributes in self and other\n+        assert hasattr(self, 'dir')\n+        assert hasattr(other, 'dir')\n+        assert hasattr(other, 'catch')\n+\n+        # set scale ratio\n+        cell_ratio = int(self.cellsize/other.cellsize)\n+\n+        # create DataFrames for self and other with geographic coordinates\n+        # as row and column labels. entries in selfdf represent cell indices.\n+        selfdf = pd.DataFrame(\n+                np.arange(self.dir.size).reshape(self.shape),\n+                index=np.linspace(self.bbox[1], self.bbox[3],\n+                                  self.shape[0], endpoint=False)[::-1],\n+                columns=np.linspace(self.bbox[0], self.bbox[2],\n+                                    self.shape[1], endpoint=False)\n+                )\n+        otherdf = pd.DataFrame(\n+                other.dir,\n+                index=np.linspace(other.bbox[1], other.bbox[3],\n+                                  other.shape[0], endpoint=False)[::-1],\n+                columns=np.linspace(other.bbox[0], other.bbox[2],\n+                                    other.shape[1], endpoint=False)\n+                )\n+\n+        # reindex self to other based on column labels and fill nulls with\n+        # nearest neighbor\n+        result = (selfdf.reindex(otherdf.index, method='nearest')\n+                  .reindex_axis(otherdf.columns, axis=1, method='nearest'))\n+\n+        # mask cells not in catchment of 'other'\n+        result = result.values[np.where(other.catch != 0, True, False)]\n+\n+        # count remaining indices and divide by the original number of indices\n+        result = ((np.bincount(result, minlength=selfdf.size)\n+                  .astype(float)/(cell_ratio**2)).reshape(selfdf.shape))\n+\n+        # replace 0 with nodata value\n+        if nodata != 0:\n+            np.place(result, result == 0, nodata)\n+\n+        # if inplace is True, set class attributes\n+        if inplace == True:\n+            self.frac = result\n+            self.nodata.update({'frac' : nodata})\n+            self.gridlist.append('frac')\n+        else:\n+            return result\n+\n+    def conform(self, data, bbox, precision=7, fillna=True, fill=0):\n+        \"\"\"\n+        Conform data to existing bbox.\n+\n+        Parameters\n+        ----------\n+        data : numpy ndarray\n+               Data to be conformed.\n+        bbox : tuple\n+               bbox of new data\n+        precision : int\n+                    Precision to use when matching geographic coordinates.\n+        fillna : bool\n+                 Whether to fill nulls.\n+        fill : int or float\n+               Fill value to use.\n+        \"\"\"\n+\n+        # create arrays representing coordinates of existing grids\n+        selfrows = np.around(np.linspace(self.bbox[1], self.bbox[3],\n+                   self.shape[0], endpoint=False)[::-1], precision)\n+        selfcols = np.around(np.linspace(self.bbox[0], self.bbox[2],\n+                   self.shape[1], endpoint=False), precision)\n+\n+        # create arrays representing coordinates of new grid\n+        rows = np.around(np.linspace(bbox[1], bbox[3], data.shape[0],\n+                                     endpoint=False)[::-1], precision)\n+        cols = np.around(np.linspace(bbox[0], bbox[2], data.shape[1],\n+                                     endpoint=False), precision)\n+\n+        # reindex new grid to existing grids\n+        data = pd.DataFrame(data, index=rows, columns=cols)\n+        data = data.reindex(selfrows).reindex_axis(selfcols, axis=1)\n+\n+        # if there is area with no overlap, fill nulls\n+        if fillna == True:\n+            return data.fillna(fill).values\n+        else:\n+            return data.values\n+\n+    def clip_nodata(self, data_name, inplace=True, precision=7, **kwargs):\n+        \"\"\"\n+        Clip grid to bbox representing the smallest area that contains all\n+        non-null data. If inplace is True, will also set self.bbox to this\n+        value and clip all other grids to the same extent.\n+\n+        Parameters\n+        ----------\n+        data_name : numpy ndarray\n+                    Name of attribute to base the clip on.\n+        precision : int\n+                    Precision to use when matching geographic coordinates.\n+        \"\"\"\n+\n+        # get class attributes\n+        data = getattr(self, data_name)\n+        nodata = self.nodata[data_name]\n+\n+        # get bbox of nonzero entries\n+        nz = np.nonzero(data != nodata)\n+        nz_ix = (nz[0].min(), nz[0].max(), nz[1].min(), nz[1].max())\n+\n+        # if inplace is True, clip all grids to new bbox and set self.bbox\n+        if inplace == True:\n+            selfrows = np.around(np.linspace(self.bbox[1], self.bbox[3],\n+                       self.shape[0], endpoint=False)[::-1],precision)\n+            selfcols = np.around(np.linspace(self.bbox[0], self.bbox[2],\n+                       self.shape[1], endpoint=False), precision)\n+            new_bbox = (selfcols[nz_ix[2]], selfrows[nz_ix[1]],\n+                        selfcols[nz_ix[3]], selfrows[nz_ix[0]])\n+            # set self.bbox to clipped bbox\n+            self.set_bbox(new_bbox, **kwargs)\n+        else:\n+            # if inplace is False, return the clipped data\n+            return data[nz_ix[0]:nz_ix[1], nz_ix[2]:nz_ix[3]]\n+\n+    def set_bbox(self, new_bbox, precision=7): \n+        \"\"\"\n+        Set the bounding box of the class instance (self.bbox) and clip\n+        all grids to the new bbox.\n+\n+        Parameters\n+        ----------\n+        new_bbox : tuple\n+                   New bbox to use (xmin, ymin, xmax, ymax)\n+        precision : int\n+                    Precision to use when matching geographic coordinates.\n+        fillna : bool\n+                 Whether to fill nulls.\n+        fill : int or float\n+               Fill value to use.\n+        \"\"\"\n+        # round new bbox to proper precision\n+        new_bbox = np.around(new_bbox, precision)\n+\n+        # construct arrays representing coordinates of existing grids\n+        selfrows = np.around(np.linspace(self.bbox[1], self.bbox[3],\n+                   self.shape[0], endpoint=False)[::-1], precision)\n+        selfcols = np.around(np.linspace(self.bbox[0], self.bbox[2],\n+                   self.shape[1], endpoint=False), precision)\n+\n+        # construct arrays representing coordinates of new grid\n+        rows = (pd.Series(selfrows, index=selfrows)\n+                .loc[new_bbox[3]:new_bbox[1]].values)\n+        cols = (pd.Series(selfcols, index=selfcols)\n+                .loc[new_bbox[0]:new_bbox[2]].values)\n+\n+        # clip existing grids to new bbox\n+        for i in self.gridlist:\n+            data = pd.DataFrame(getattr(self, i),\n+                                index=selfrows, columns=selfcols)\n+            data = (data.reindex(rows).reindex_axis(cols, axis=1)\n+                        .fillna(self.nodata[i]).values)\n+            setattr(self, i, data)\n+        \n+        # set class attributes\n+        self.bbox = tuple(new_bbox)\n+        self.shape = tuple([len(rows), len(cols)])\n+\n+    def set_nodata(self, data_name, new_nodata, old_nodata=None):\n+        \"\"\"\n+        Change nodata value of dataset.\n+\n+        Parameters\n+        ----------\n+        data_name : string\n+                    Attribute name of dataset to change\n+        new_nodata : int or float\n+                     New nodata value to use\n+        old_nodata : int or float (optional)\n+                     If none provided, defaults to self.nodata[data_name]\n+        \"\"\"\n+\n+        if old_nodata is None:\n+            old_nodata = self.nodata[data_name]\n+        data = getattr(self, data_name)\n+        np.place(data, data==old_nodata, new_nodata)\n+        self.nodata.update({data_name : new_nodata})\n+\n+    def catchment_mask(self, to_mask, mask_source='catch'):\n+        \"\"\"\n+        Masks grid cells not included in catchment.\n+\n+        Parameters\n+        ----------\n+        to_mask : string\n+                  name of dataset to mask\n+        mask_source : string (optional)\n+                      dataset on which mask is based (defaults to 'catch')\n+        \"\"\"\n+\n+        mask = (getattr(self, mask_source) == self.nodata[mask_source])\n+        if isinstance(to_mask, str):\n+            np.place(getattr(self, to_mask), mask, self.nodata[to_mask])\n+        elif isinstance(to_mask, (list, tuple, np.ndarray)):\n+            for i in to_mask:\n+                np.place(getattr(self, i), mask, self.nodata[i])\n+\n+    def to_ascii(self, data_name=None, file_name=None, delimiter=' ', **kwargs):\n+        \"\"\"\n+        Writes grid data to ascii grid files.\n+\n+        Parameters\n+        ----------\n+        data_name : string or list-like (optional)\n+                    Attribute name(s) of datasets to write. Defaults to all\n+                    grid dataset names.\n+        file_name : string or list-like\n+                    Name(s) of file(s) to write to\n+        delimiter : string\n+                    Delimiter to use\n+        \"\"\"\n+        if data_name is None:\n+            data_name = self.gridlist\n+        if file_name is None:\n+            file_name = self.gridlist\n+\n+        if isinstance(data_name, str):\n+            data_name = [data_name]\n+        if isinstance(file_name, str):\n+            file_name = [file_name]\n+\n+        for i, j in zip(data_name, file_name):","path":"tools/flow_grid.py","position":676,"original_position":676,"commit_id":"aca64f0c2ed271f9af946558788ff3cc536992f7","original_commit_id":"aca64f0c2ed271f9af946558788ff3cc536992f7","user":{"login":"jhamman","id":2443309,"avatar_url":"https://avatars.githubusercontent.com/u/2443309?v=3","gravatar_id":"","url":"https://api.github.com/users/jhamman","html_url":"https://github.com/jhamman","followers_url":"https://api.github.com/users/jhamman/followers","following_url":"https://api.github.com/users/jhamman/following{/other_user}","gists_url":"https://api.github.com/users/jhamman/gists{/gist_id}","starred_url":"https://api.github.com/users/jhamman/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jhamman/subscriptions","organizations_url":"https://api.github.com/users/jhamman/orgs","repos_url":"https://api.github.com/users/jhamman/repos","events_url":"https://api.github.com/users/jhamman/events{/privacy}","received_events_url":"https://api.github.com/users/jhamman/received_events","type":"User","site_admin":false},"body":"change variable names.  `i` and `j` are usually indices and here they are confusingly used as variable/file names.","created_at":"2015-05-26T08:31:03Z","updated_at":"2015-05-26T08:31:03Z","html_url":"https://github.com/UW-Hydro/RVIC/pull/65#discussion_r31013520","pull_request_url":"https://api.github.com/repos/UW-Hydro/RVIC/pulls/65","_links":{"self":{"href":"https://api.github.com/repos/UW-Hydro/RVIC/pulls/comments/31013520"},"html":{"href":"https://github.com/UW-Hydro/RVIC/pull/65#discussion_r31013520"},"pull_request":{"href":"https://api.github.com/repos/UW-Hydro/RVIC/pulls/65"}}},"pull_request":{"url":"https://api.github.com/repos/UW-Hydro/RVIC/pulls/65","id":36166761,"html_url":"https://github.com/UW-Hydro/RVIC/pull/65","diff_url":"https://github.com/UW-Hydro/RVIC/pull/65.diff","patch_url":"https://github.com/UW-Hydro/RVIC/pull/65.patch","issue_url":"https://api.github.com/repos/UW-Hydro/RVIC/issues/65","number":65,"state":"open","locked":false,"title":"Pre-processing tools for gridded data","user":{"login":"mdbartos","id":8175317,"avatar_url":"https://avatars.githubusercontent.com/u/8175317?v=3","gravatar_id":"","url":"https://api.github.com/users/mdbartos","html_url":"https://github.com/mdbartos","followers_url":"https://api.github.com/users/mdbartos/followers","following_url":"https://api.github.com/users/mdbartos/following{/other_user}","gists_url":"https://api.github.com/users/mdbartos/gists{/gist_id}","starred_url":"https://api.github.com/users/mdbartos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mdbartos/subscriptions","organizations_url":"https://api.github.com/users/mdbartos/orgs","repos_url":"https://api.github.com/users/mdbartos/repos","events_url":"https://api.github.com/users/mdbartos/events{/privacy}","received_events_url":"https://api.github.com/users/mdbartos/received_events","type":"User","site_admin":false},"body":"Greetings!\r\n\r\nThe following pull request contains pre-processing utilities for the VIC routing model. These utilities are wrapped up in a class object called `'flow_grid'` (I kind of named things as I went along, so name suggestions would be appreciated).\r\n\r\nFor info on how to use this module, see the following ipython notebook:\r\nhttp://nbviewer.ipython.org/github/mdbartos/misc/blob/master/notebooks/flow_grid_example.ipynb\r\n\r\nMy main motivation was to create a streamlined process for creating \"fraction files\" (i.e. ascii grids representing the fractional contributing area at each grid cell). However, the module I ended up developing can do a number of different things as well:\r\n\r\n+ Delineate a catchment from a flow direction grid\r\n\r\n+ Create a flow direction grid from a dem grid (can't handle \"flats\" yet -- still working on this).\r\n\r\n+ Change values within a gridded dataset (e.g. change the NODATA value, change flow direction mappings from ESRI to VIC format, etc.)\r\n\r\n+ Clip and conform gridded data to common spatial extents, mask gridcells that lie outside the catchment, and write ascii files that are spatially compatible with each other\r\n\r\nI would like to write some tests for this new module, but I'm not sure what files I should be running my tests on. <i>Are there any particular test datasets that you would like to use (e.g. a particular catchment)?</i> I've been using the HydroSHEDS datasets, but I think they might be a bit too bulky for Travis testing.\r\n\r\nDependencies include numpy and pandas. Rasterio is an optional dependency for reading raster datasets.\r\n\r\nThanks,\r\nMDB","created_at":"2015-05-26T03:11:55Z","updated_at":"2015-05-26T08:31:03Z","closed_at":null,"merged_at":null,"merge_commit_sha":"a5d67c4a2c76cbb0e7662ca0d50e7ce95cddb449","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/UW-Hydro/RVIC/pulls/65/commits","review_comments_url":"https://api.github.com/repos/UW-Hydro/RVIC/pulls/65/comments","review_comment_url":"https://api.github.com/repos/UW-Hydro/RVIC/pulls/comments{/number}","comments_url":"https://api.github.com/repos/UW-Hydro/RVIC/issues/65/comments","statuses_url":"https://api.github.com/repos/UW-Hydro/RVIC/statuses/aca64f0c2ed271f9af946558788ff3cc536992f7","head":{"label":"mdbartos:develop","ref":"develop","sha":"aca64f0c2ed271f9af946558788ff3cc536992f7","user":{"login":"mdbartos","id":8175317,"avatar_url":"https://avatars.githubusercontent.com/u/8175317?v=3","gravatar_id":"","url":"https://api.github.com/users/mdbartos","html_url":"https://github.com/mdbartos","followers_url":"https://api.github.com/users/mdbartos/followers","following_url":"https://api.github.com/users/mdbartos/following{/other_user}","gists_url":"https://api.github.com/users/mdbartos/gists{/gist_id}","starred_url":"https://api.github.com/users/mdbartos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mdbartos/subscriptions","organizations_url":"https://api.github.com/users/mdbartos/orgs","repos_url":"https://api.github.com/users/mdbartos/repos","events_url":"https://api.github.com/users/mdbartos/events{/privacy}","received_events_url":"https://api.github.com/users/mdbartos/received_events","type":"User","site_admin":false},"repo":{"id":34509542,"name":"RVIC","full_name":"mdbartos/RVIC","owner":{"login":"mdbartos","id":8175317,"avatar_url":"https://avatars.githubusercontent.com/u/8175317?v=3","gravatar_id":"","url":"https://api.github.com/users/mdbartos","html_url":"https://github.com/mdbartos","followers_url":"https://api.github.com/users/mdbartos/followers","following_url":"https://api.github.com/users/mdbartos/following{/other_user}","gists_url":"https://api.github.com/users/mdbartos/gists{/gist_id}","starred_url":"https://api.github.com/users/mdbartos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mdbartos/subscriptions","organizations_url":"https://api.github.com/users/mdbartos/orgs","repos_url":"https://api.github.com/users/mdbartos/repos","events_url":"https://api.github.com/users/mdbartos/events{/privacy}","received_events_url":"https://api.github.com/users/mdbartos/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/mdbartos/RVIC","description":"RVIC Streamflow Routing Model","fork":true,"url":"https://api.github.com/repos/mdbartos/RVIC","forks_url":"https://api.github.com/repos/mdbartos/RVIC/forks","keys_url":"https://api.github.com/repos/mdbartos/RVIC/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mdbartos/RVIC/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mdbartos/RVIC/teams","hooks_url":"https://api.github.com/repos/mdbartos/RVIC/hooks","issue_events_url":"https://api.github.com/repos/mdbartos/RVIC/issues/events{/number}","events_url":"https://api.github.com/repos/mdbartos/RVIC/events","assignees_url":"https://api.github.com/repos/mdbartos/RVIC/assignees{/user}","branches_url":"https://api.github.com/repos/mdbartos/RVIC/branches{/branch}","tags_url":"https://api.github.com/repos/mdbartos/RVIC/tags","blobs_url":"https://api.github.com/repos/mdbartos/RVIC/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mdbartos/RVIC/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mdbartos/RVIC/git/refs{/sha}","trees_url":"https://api.github.com/repos/mdbartos/RVIC/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mdbartos/RVIC/statuses/{sha}","languages_url":"https://api.github.com/repos/mdbartos/RVIC/languages","stargazers_url":"https://api.github.com/repos/mdbartos/RVIC/stargazers","contributors_url":"https://api.github.com/repos/mdbartos/RVIC/contributors","subscribers_url":"https://api.github.com/repos/mdbartos/RVIC/subscribers","subscription_url":"https://api.github.com/repos/mdbartos/RVIC/subscription","commits_url":"https://api.github.com/repos/mdbartos/RVIC/commits{/sha}","git_commits_url":"https://api.github.com/repos/mdbartos/RVIC/git/commits{/sha}","comments_url":"https://api.github.com/repos/mdbartos/RVIC/comments{/number}","issue_comment_url":"https://api.github.com/repos/mdbartos/RVIC/issues/comments{/number}","contents_url":"https://api.github.com/repos/mdbartos/RVIC/contents/{+path}","compare_url":"https://api.github.com/repos/mdbartos/RVIC/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mdbartos/RVIC/merges","archive_url":"https://api.github.com/repos/mdbartos/RVIC/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mdbartos/RVIC/downloads","issues_url":"https://api.github.com/repos/mdbartos/RVIC/issues{/number}","pulls_url":"https://api.github.com/repos/mdbartos/RVIC/pulls{/number}","milestones_url":"https://api.github.com/repos/mdbartos/RVIC/milestones{/number}","notifications_url":"https://api.github.com/repos/mdbartos/RVIC/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mdbartos/RVIC/labels{/name}","releases_url":"https://api.github.com/repos/mdbartos/RVIC/releases{/id}","created_at":"2015-04-24T09:19:33Z","updated_at":"2015-04-24T09:19:34Z","pushed_at":"2015-05-26T06:31:58Z","git_url":"git://github.com/mdbartos/RVIC.git","ssh_url":"git@github.com:mdbartos/RVIC.git","clone_url":"https://github.com/mdbartos/RVIC.git","svn_url":"https://github.com/mdbartos/RVIC","homepage":"http://rvic.readthedocs.org/en/latest/","size":1719,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"UW-Hydro:develop","ref":"develop","sha":"3046e370021d0b0542d91bd8ac9944e00280932d","user":{"login":"UW-Hydro","id":3475662,"avatar_url":"https://avatars.githubusercontent.com/u/3475662?v=3","gravatar_id":"","url":"https://api.github.com/users/UW-Hydro","html_url":"https://github.com/UW-Hydro","followers_url":"https://api.github.com/users/UW-Hydro/followers","following_url":"https://api.github.com/users/UW-Hydro/following{/other_user}","gists_url":"https://api.github.com/users/UW-Hydro/gists{/gist_id}","starred_url":"https://api.github.com/users/UW-Hydro/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/UW-Hydro/subscriptions","organizations_url":"https://api.github.com/users/UW-Hydro/orgs","repos_url":"https://api.github.com/users/UW-Hydro/repos","events_url":"https://api.github.com/users/UW-Hydro/events{/privacy}","received_events_url":"https://api.github.com/users/UW-Hydro/received_events","type":"Organization","site_admin":false},"repo":{"id":11590212,"name":"RVIC","full_name":"UW-Hydro/RVIC","owner":{"login":"UW-Hydro","id":3475662,"avatar_url":"https://avatars.githubusercontent.com/u/3475662?v=3","gravatar_id":"","url":"https://api.github.com/users/UW-Hydro","html_url":"https://github.com/UW-Hydro","followers_url":"https://api.github.com/users/UW-Hydro/followers","following_url":"https://api.github.com/users/UW-Hydro/following{/other_user}","gists_url":"https://api.github.com/users/UW-Hydro/gists{/gist_id}","starred_url":"https://api.github.com/users/UW-Hydro/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/UW-Hydro/subscriptions","organizations_url":"https://api.github.com/users/UW-Hydro/orgs","repos_url":"https://api.github.com/users/UW-Hydro/repos","events_url":"https://api.github.com/users/UW-Hydro/events{/privacy}","received_events_url":"https://api.github.com/users/UW-Hydro/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/UW-Hydro/RVIC","description":"RVIC Streamflow Routing Model","fork":false,"url":"https://api.github.com/repos/UW-Hydro/RVIC","forks_url":"https://api.github.com/repos/UW-Hydro/RVIC/forks","keys_url":"https://api.github.com/repos/UW-Hydro/RVIC/keys{/key_id}","collaborators_url":"https://api.github.com/repos/UW-Hydro/RVIC/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/UW-Hydro/RVIC/teams","hooks_url":"https://api.github.com/repos/UW-Hydro/RVIC/hooks","issue_events_url":"https://api.github.com/repos/UW-Hydro/RVIC/issues/events{/number}","events_url":"https://api.github.com/repos/UW-Hydro/RVIC/events","assignees_url":"https://api.github.com/repos/UW-Hydro/RVIC/assignees{/user}","branches_url":"https://api.github.com/repos/UW-Hydro/RVIC/branches{/branch}","tags_url":"https://api.github.com/repos/UW-Hydro/RVIC/tags","blobs_url":"https://api.github.com/repos/UW-Hydro/RVIC/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/UW-Hydro/RVIC/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/UW-Hydro/RVIC/git/refs{/sha}","trees_url":"https://api.github.com/repos/UW-Hydro/RVIC/git/trees{/sha}","statuses_url":"https://api.github.com/repos/UW-Hydro/RVIC/statuses/{sha}","languages_url":"https://api.github.com/repos/UW-Hydro/RVIC/languages","stargazers_url":"https://api.github.com/repos/UW-Hydro/RVIC/stargazers","contributors_url":"https://api.github.com/repos/UW-Hydro/RVIC/contributors","subscribers_url":"https://api.github.com/repos/UW-Hydro/RVIC/subscribers","subscription_url":"https://api.github.com/repos/UW-Hydro/RVIC/subscription","commits_url":"https://api.github.com/repos/UW-Hydro/RVIC/commits{/sha}","git_commits_url":"https://api.github.com/repos/UW-Hydro/RVIC/git/commits{/sha}","comments_url":"https://api.github.com/repos/UW-Hydro/RVIC/comments{/number}","issue_comment_url":"https://api.github.com/repos/UW-Hydro/RVIC/issues/comments{/number}","contents_url":"https://api.github.com/repos/UW-Hydro/RVIC/contents/{+path}","compare_url":"https://api.github.com/repos/UW-Hydro/RVIC/compare/{base}...{head}","merges_url":"https://api.github.com/repos/UW-Hydro/RVIC/merges","archive_url":"https://api.github.com/repos/UW-Hydro/RVIC/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/UW-Hydro/RVIC/downloads","issues_url":"https://api.github.com/repos/UW-Hydro/RVIC/issues{/number}","pulls_url":"https://api.github.com/repos/UW-Hydro/RVIC/pulls{/number}","milestones_url":"https://api.github.com/repos/UW-Hydro/RVIC/milestones{/number}","notifications_url":"https://api.github.com/repos/UW-Hydro/RVIC/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/UW-Hydro/RVIC/labels{/name}","releases_url":"https://api.github.com/repos/UW-Hydro/RVIC/releases{/id}","created_at":"2013-07-22T19:24:00Z","updated_at":"2015-04-17T10:53:45Z","pushed_at":"2015-05-26T06:31:59Z","git_url":"git://github.com/UW-Hydro/RVIC.git","ssh_url":"git@github.com:UW-Hydro/RVIC.git","clone_url":"https://github.com/UW-Hydro/RVIC.git","svn_url":"https://github.com/UW-Hydro/RVIC","homepage":"http://rvic.readthedocs.org/en/latest/","size":1788,"stargazers_count":1,"watchers_count":1,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":10,"mirror_url":null,"open_issues_count":10,"forks":10,"open_issues":10,"watchers":1,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/UW-Hydro/RVIC/pulls/65"},"html":{"href":"https://github.com/UW-Hydro/RVIC/pull/65"},"issue":{"href":"https://api.github.com/repos/UW-Hydro/RVIC/issues/65"},"comments":{"href":"https://api.github.com/repos/UW-Hydro/RVIC/issues/65/comments"},"review_comments":{"href":"https://api.github.com/repos/UW-Hydro/RVIC/pulls/65/comments"},"review_comment":{"href":"https://api.github.com/repos/UW-Hydro/RVIC/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/UW-Hydro/RVIC/pulls/65/commits"},"statuses":{"href":"https://api.github.com/repos/UW-Hydro/RVIC/statuses/aca64f0c2ed271f9af946558788ff3cc536992f7"}}}}