Example #1
0
 def makeRidgeRaster(self):
     """Make raster  to show ridges."""
     ridgeFile = QSWATUtils.join(self._gv.demDir,
                                 'ridge' + str(self.branchThresh) + '.tif')
     ridgeRaster = Raster(ridgeFile, self._gv, canWrite=True, isInt=True)
     res = ridgeRaster.open(self.chunkCount,
                            numRows=self.numRows,
                            numCols=self.numCols,
                            transform=self.demTransform,
                            projection=self.demProjection,
                            noData=self.noData)
     if not res:
         return
     for row in range(self.numRows):
         for col in range(self.numCols):
             if self.demRaster.read(row, col) != self.demNoData:
                 val, _ = self.ridgePoints.get((row, col), (-1, -1))
                 ridgeRaster.write(row, col, 0 if val == -1 else 1)
     ridgeRaster.close()
Example #2
0
class Floodplain(QObject):
    """
    Calculate floodplain raster  from DEM, stream threshold, ridge threshold, floodplain threshold,
    (optionally) D8 flow direction grid, flow accumulation grid, outlets shapefile.
     
    The slope position value is, for each point, (e - v) / (r - v) where e is the elevation, 
    v the nearest valley floor elevation, and r the nearest ridge elevation.  Nearest means along D8 flow path.
    Valley floor is points with flow accumulation above stream generation threshold.
    Ridge is similar, obtained by inverting DEM, then calculating D8 flow direction and accumulation.
    The floodplain raster  is zero for points with slope position value at most the floodplain threshold, else 1.
    """
    def __init__(self, gv, progress, chunkCount):
        """Initialise class variables."""
        QObject.__init__(self)
        self._gv = gv
        self._progress = progress
        ## raster  chunk count
        self.chunkCount = chunkCount
        ## ridge accumulation threshold
        self.ridgeThresh = 0
        ## floodplain accumulation threshold
        self.floodThresh = 0
        ## branch length threshold
        self.branchThresh = 0
        ## dem raster  dataset
        self.demDs = None
        ## dem raster  transform
        self.demTransform = None
        ## dem raster
        self.demRaster = None
        ## dem raster  no data value
        self.demNoData = -1
        ## dem raster  projection
        self.demProjection = None
        ## no data value for output rasters; must be negative
        self.noData = -1
        ## number of rows in input clipped DEM raster
        self.numRows = 0
        ## number of columns in input clipped DEM raster
        self.numCols = 0
        ## depths of valley floor below points
        self.valleyDepthsRaster = None
        ## file path for ridgeHeightsRaster
        self.ridgeHeightsFile = ''
        ## heights of ridge above points
        self.ridgeHeightsRaster = None
        ## distances of ridge from points
        self.ridgeDistancesRaster = None
        ## floodplain raster
        self.floodplainRaster = None
        ## flag to indicate if using inversion or branch length method
        self.useInversion = True
        ## map of (row, col) to (elevation, branch length) for ridge points
        self.ridgePoints = None

    def run(self, demRaster, valleyDepthsFile, ridgeThresh, floodThresh,
            branchThresh, subbasins, distances, slopeDir, flowAcc, ridgep,
            ridges, noData, mustRun):
        """
        Generate floodplain raster , with floodplain value 0 and upland value 1.
        
        valleyDepthsRaster is already calculated and stored in valleyDepthsFile
        ridgeThresh is the threshold in pixel counts for ridges (inverse accumulation).
        floodThresh is the critical ratio of relative depth of valley floor against height of ridge above valley floor.
        branchThresh is the threshold in metres for branch lengths.
        dem is clipped filled DEM
        The groups {subbasins, distances, slopeDir, flowAcc} and {ridgep, ridges} are either all None or all existing files,
        assumed to be clipped and with same pixel size as DEM (so (row, col|) coordinates may be shared across all of them).
        The second group is used to calculate ridges by DEM inversion if subbasins is None, else the first is used to 
        calculate ridges by branch lengths.
        """
        self.demRaster = demRaster
        self.ridgeThresh = ridgeThresh
        self.floodThresh = floodThresh
        self.branchThresh = branchThresh
        gdal.AllRegister()
        ds = self.demRaster.ds
        self.numRows = ds.RasterYSize
        self.numCols = ds.RasterXSize
        self.demTransform = ds.GetGeoTransform()
        self.demNoData = self.demRaster.band.GetNoDataValue()
        self.demProjection = ds.GetProjection()
        self.noData = noData
        self._progress('Ridge heights ...')
        root = QgsProject.instance().layerTreeRoot()
        if subbasins is None:
            # use inverted DEM method for ridges
            self.useInversion = True
            time1 = time.process_time()
            if not self.calcRidgeHeightsByInversion(ridgep, ridges, root,
                                                    mustRun):
                return
            time2 = time.process_time()
            QSWATUtils.loginfo(
                'Ridge heights by inversion took {0} seconds'.format(
                    int(time2 - time1)))
        else:
            self.useInversion = False
            time1 = time.process_time()
            if not self.calRidgeHeghtsByBranchLength(
                    subbasins, distances, slopeDir, flowAcc, valleyDepthsFile,
                    root, mustRun):
                return
            time2 = time.process_time()
            QSWATUtils.loginfo(
                'Ridge heights by branch length took {0} seconds'.format(
                    int(time2 - time1)))
        time1 = time.process_time()
        self.writeFloodPlain(valleyDepthsFile, mustRun)
        time2 = time.process_time()
        QSWATUtils.loginfo('Floodplain creation took {0} seconds'.format(
            int(time2 - time1)))
        self._progress('')

    def calcRidgeHeightsByInversion(self, ridgep, ridges, root, mustRun):
        """
        Create the ridgeHeightsRaster with differences between the elevation at the point and 
        the elevation of the nearest ridge cell.
        
        ridgep is the D8 flow directions and ridges the flow accumulation raster , both calculated from an inverted DEM.
        """
        self.ridgeHeightsFile = QSWATUtils.join(self._gv.demDir,
                                                'invheights.tif')
        if mustRun or not QSWATUtils.isUpToDate(
                ridgep, self.ridgeHeightsFile) or not QSWATUtils.isUpToDate(
                    ridges, self.ridgeHeightsFile):
            self._gv.clearOpenRasters()
            completed = False
            while not completed:
                try:
                    completed = True  # only gets set on MemoryError exception
                    if os.path.exists(self.ridgeHeightsFile):
                        QSWATUtils.tryRemoveLayerAndFiles(
                            self.ridgeHeightsFile, root)
                    self.ridgeHeightsRaster = Raster(self.ridgeHeightsFile,
                                                     self._gv,
                                                     canWrite=True,
                                                     isInt=False)
                    res = self.ridgeHeightsRaster.open(
                        self.chunkCount,
                        numRows=self.numRows,
                        numCols=self.numCols,
                        transform=self.demTransform,
                        projection=self.demProjection,
                        noData=self.noData)
                    if not res:
                        return False
                    # ridgep obtained by inversion has float values, although these are in the range 1 to 8
                    ridgepRaster = Raster(ridgep,
                                          self._gv,
                                          canWrite=False,
                                          isInt=False)
                    res = ridgepRaster.open(self.chunkCount)
                    if not res:
                        self._gv.closeOpenRasters()
                        return False
                    ridgesRaster = Raster(ridges,
                                          self._gv,
                                          canWrite=False,
                                          isInt=True)
                    res = ridgesRaster.open(self.chunkCount)
                    if not res:
                        self._gv.closeOpenRasters()
                        return False
                    for row in range(self.numRows):
                        for col in range(self.numCols):
                            if self.demRaster.read(row, col) != self.demNoData:
                                (val, path) = self.valueAtNearest(
                                    row, col, ridgepRaster, ridgesRaster,
                                    self.ridgeThresh)
                                if path is not None:
                                    self.propagate(val, path)
                    ridgepRaster.close()
                    ridgesRaster.close()
                    self.ridgeHeightsRaster.close()
                    return True
                except MemoryError:
                    QSWATUtils.loginfo(
                        'Out of memory for ridge heights by inversion with chunk count {0}'
                        .format(self.chunkCount))
                    self._gv.closeOpenRasters()
                    completed = False
                    self.chunkCount += 1
                    if not self.demRaster.open(self.chunkCount):
                        return False
        else:
            return True

    def calRidgeHeghtsByBranchLength(self, subbasins, distances, slopeDir,
                                     flowAcc, valleyDepthsFile, root, mustRun):
        """
        Create the ridgeHeightsRaster with differences between the elevation at the point and 
        the elevation of the nearest ridge cell.
        
        subbasins is the subbasins raster, distances is the distances to outlet raster, 
        slopeDir is the D8 slope directions raster, flowAcc the flow accumulation raster, all four clipped 
        the same as the DEM.
        valleyDepthsFile is the existing raster  giving the depth of the valley flow below each point (as a positive number of metres).
        """
        self.ridgeHeightsFile = QSWATUtils.join(self._gv.demDir,
                                                'branchheights.tif')
        distancesFile = QSWATUtils.join(self._gv.demDir,
                                        'branchdistancess.tif')
        if mustRun or not QSWATUtils.isUpToDate(subbasins, self.ridgeHeightsFile) \
            or not QSWATUtils.isUpToDate(distances, self.ridgeHeightsFile) \
            or not QSWATUtils.isUpToDate(slopeDir, self.ridgeHeightsFile) \
            or not QSWATUtils.isUpToDate(flowAcc, self.ridgeHeightsFile):
            self._gv.clearOpenRasters()
            completed = False
            while not completed:
                try:
                    completed = True  # only gets set on MemoryError exception
                    if os.path.exists(self.ridgeHeightsFile):
                        QSWATUtils.tryRemoveLayerAndFiles(
                            self.ridgeHeightsFile, root)
                    if os.path.exists(distancesFile):
                        QSWATUtils.tryRemoveLayerAndFiles(distancesFile, root)
                    self.ridgeHeightsRaster = Raster(self.ridgeHeightsFile,
                                                     self._gv,
                                                     canWrite=True,
                                                     isInt=False)
                    res = self.ridgeHeightsRaster.open(
                        self.chunkCount,
                        numRows=self.numRows,
                        numCols=self.numCols,
                        transform=self.demTransform,
                        projection=self.demProjection,
                        noData=self.noData)
                    if not res:
                        return False
                    self.ridgeDistancesRaster = Raster(distancesFile,
                                                       self._gv,
                                                       canWrite=True,
                                                       isInt=False)
                    res = self.ridgeDistancesRaster.open(
                        self.chunkCount,
                        numRows=self.numRows,
                        numCols=self.numCols,
                        transform=self.demTransform,
                        projection=self.demProjection,
                        noData=self.noData)
                    if not res:
                        self._gv.closeOpenRasters()
                        return False
                    subbasinsRaster = Raster(subbasins,
                                             self._gv,
                                             canWrite=False,
                                             isInt=True)
                    res = subbasinsRaster.open(self.chunkCount)
                    if not res:
                        self._gv.closeOpenRasters()
                        return False
                    distRaster = Raster(distances,
                                        self._gv,
                                        canWrite=False,
                                        isInt=False)
                    res = distRaster.open(self.chunkCount)
                    if not res:
                        self._gv.closeOpenRasters()
                        return False
                    self.findRidges(subbasinsRaster, distRaster, root)
                    subbasinsRaster.close()
                    distRaster.close()
                    accRaster = Raster(flowAcc,
                                       self._gv,
                                       canWrite=False,
                                       isInt=True)
                    res = accRaster.open(self.chunkCount)
                    if not res:
                        self._gv.closeOpenRasters()
                        return False
                    dirRaster = Raster(slopeDir,
                                       self._gv,
                                       canWrite=False,
                                       isInt=True)
                    res = dirRaster.open(self.chunkCount)
                    if not res:
                        self._gv.closeOpenRasters()
                        return False
                    self.propagateFromRidges(valleyDepthsFile, dirRaster,
                                             accRaster)
                    dirRaster.close()
                    accRaster.close()
                    self.ridgeHeightsRaster.close()
                    self.ridgeDistancesRaster.close()
                    return True
                except MemoryError:
                    QSWATUtils.loginfo(
                        'Out of memory for ridge heights by branch length with chunk count {0}'
                        .format(self.chunkCount))
                    self._gv.closeOpenRasters()
                    completed = False
                    self.chunkCount += 1
                    if not self.demRaster.open(self.chunkCount):
                        return False
        else:
            return True

    def valueAtNearest(self, row, col, d8Raster, accRaster, threshold):
        """
        Return the positive elevation of the nearest point in accRaster where it is at least threshold,
        plus path along d8Raster to that point.
        
        If a value is found in result, point already processed, return result - elevation for that point.
        We are working upslope, final height is ridge height,
        ridgeHeights contains ridge - elev, i.e. positive height of ridge above point, 
        so if result already exists, ridge is recovered as result + elev
        """
        path = []
        accNoData = accRaster.band.GetNoDataValue()
        while True:
            res = self.ridgeHeightsRaster.read(row, col)
            if res >= 0:  # already done this point
                # eg result is 60, this point's elevation is 100, return value (ridge elevation) is 160
                return (self.demRaster.read(row, col) + res, path)
            acc = accRaster.read(row, col)
            if acc >= threshold or acc == accNoData:  # found nearest stopping point
                path.append((row, col))
                return (self.demRaster.read(row, col), path)
            if (row, col) in path:
                (startRow, startCol) = path[0]
                startX, startY = QSWATTopology.cellToProj(
                    startCol, startRow, self.demTransform)
                x, y = QSWATTopology.cellToProj(col, row, self.demTransform)
                QSWATUtils.error(
                    'Loop to ({0}, {1}) from ({2}, {3})'.format(
                        x, y, startX, startY), self._gv.isBatch)
                return (0, None)
            path.append((row, col))
            pt, _ = self.alongDirPoint(row, col, d8Raster)
            if pt is None:
                # hit edge of clipped area = must be a ridge
                return (self.demRaster.read(row, col), path)
            (row, col) = pt

    def propagate(self, val, path):
        """
        For each point on path, set result to val (ridge elevation) - point elevation."""
        for (row, col) in path:
            self.ridgeHeightsRaster.write(row, col,
                                          val - self.demRaster.read(row, col))

    def writeFloodPlain(self, valleyDepthsFile, mustRun):
        """Calculate slope positions from valleyDepths and ridgeHeights, and write floodplain raster ."""
        method = 'inv' if self.useInversion else 'branch'
        thresh = '{0:.2F}'.format(self.floodThresh).replace('.', '_')
        flood = QSWATUtils.join(self._gv.floodDir,
                                method + 'flood' + thresh + '.tif')
        root = QgsProject.instance().layerTreeRoot()
        if mustRun or \
            not QSWATUtils.isUpToDate(valleyDepthsFile, flood) or \
            not QSWATUtils.isUpToDate(self.ridgeHeightsFile, flood):
            if os.path.exists(flood):
                QSWATUtils.tryRemoveLayerAndFiles(flood, root)
            self._gv.clearOpenRasters()
            completed = False
            while not completed:
                try:
                    completed = True  # only gets set on MemoryError exception
                    self.ridgeHeightsRaster = Raster(self.ridgeHeightsFile,
                                                     self._gv,
                                                     canWrite=False,
                                                     isInt=False)
                    res = self.ridgeHeightsRaster.open(self.chunkCount)
                    if not res:
                        self._gv.closeOpenRasters()
                        return
                    if self.valleyDepthsRaster is None:
                        # may have been opened when calculating ridge heights by branch length
                        self.valleyDepthsRaster = Raster(valleyDepthsFile,
                                                         self._gv,
                                                         canWrite=False,
                                                         isInt=False)
                        res = self.valleyDepthsRaster.open(self.chunkCount)
                        if not res:
                            self._gv.closeOpenRasters()
                            return
                    self._progress('Flood plain...')
                    self.floodplainRaster = Raster(flood,
                                                   self._gv,
                                                   canWrite=True,
                                                   isInt=True)
                    OK = self.floodplainRaster.open(
                        self.chunkCount,
                        numRows=self.numRows,
                        numCols=self.numCols,
                        transform=self.demTransform,
                        projection=self.demProjection,
                        noData=self.noData)
                    if OK:
                        self.calcFloodPlain1()
                        self.floodplainRaster.close()
                        ## fixing aux.xml file seems unnecessary in QGIS 2.16
                        #                         # now fix maximum value to 1 instead of zero in aux.xml file
                        #                         # else if loaded has legend 0 to nan and display is all black
                        #                         xmlFile = self.floodplainRaster.fileName + '.aux.xml'
                        #                         ok, err = QSWATUtils.setXMLValue(xmlFile, u'MDI', u'key', u'STATISTICS_MAXIMUM', u'1')
                        #                         if not ok:
                        #                             QSWATUtils.error(err, self._gv.isBatch)
                        QSWATUtils.copyPrj(self.demRaster.fileName,
                                           self.floodplainRaster.fileName)
                        # load flood above DEM
                        layers = root.findLayers()
                        demLayer = QSWATUtils.getLayerByLegend(
                            FileTypes.legend(FileTypes._DEM), layers)
                        ft = FileTypes._INVFLOOD if self.useInversion else FileTypes._BRANCHFLOOD
                        floodLayer, _ = QSWATUtils.getLayerByFilename(
                            layers, self.floodplainRaster.fileName, ft,
                            self._gv, demLayer,
                            QSWATUtils._WATERSHED_GROUP_NAME)
                        if floodLayer is None:
                            QSWATUtils.error('Failed to load floodplain raster {0}' \
                                             .format(self.floodplainRaster.fileName), self._gv.isBatch)
                    self.valleyDepthsRaster.close()
                    self.ridgeHeightsRaster.close()
                    self._progress('Flood plain done')
                except MemoryError:
                    QSWATUtils.loginfo(
                        'Out of memory for flood plain with chunk count {0}'.
                        format(self.chunkCount))
                    self._gv.closeOpenRasters()
                    completed = False
                    self.chunkCount += 1
        else:  # already have uptodate flood file: make sure it is loaded
            # load flood above DEM
            layers = root.findLayers()
            demLayer = QSWATUtils.getLayerByLegend(
                FileTypes.legend(FileTypes._DEM), layers)
            ft = FileTypes._INVFLOOD if self.useInversion else FileTypes._BRANCHFLOOD
            floodLayer, _ = QSWATUtils.getLayerByFilename(
                layers, flood, ft, self._gv, demLayer,
                QSWATUtils._WATERSHED_GROUP_NAME)
            if floodLayer is None:
                QSWATUtils.error('Failed to load floodplain raster {0}' \
                                 .format(self.floodplainRaster.fileName), self._gv.isBatch)

    def calcFloodPlain1(self):
        """Calculate the floodplain array elementwise."""
        old_settings = numpy.seterr(divide='raise')
        valleyTransform = self.valleyDepthsRaster.ds.GetGeoTransform()
        for row in range(self.numRows):
            for col in range(self.numCols):
                rh = self.ridgeHeightsRaster.read(row, col)
                if rh != self.noData:
                    vd = self.valleyDepthsRaster.read(row, col)
                    if vd != self.noData:
                        try:
                            sp = 0 if int(rh + vd) == 0 else vd / (rh + vd)
                        except Exception:
                            QSWATUtils.information('Problem in calculating slope position at {0}: numerator {1}; denominator {2}: {3}. Set to 0' \
                                               .format(QSWATTopology.cellToProj(col, row, valleyTransform), vd, rh+vd, traceback.format_exc()),
                                             self._gv.isBatch)
                            sp = 0
                        self.floodplainRaster.write(
                            row, col,
                            1 if sp <= self.floodThresh else self.noData)
        numpy.seterr(divide=old_settings['divide'])

    # TODO: deal with array access
    #===========================================================================
    # def calcFloodPlain2(self):
    #     """Calculate the floodplain array using numpy array methods."""
    #     for i in xrange(self.chunkCount):
    #         # if there is only one chunk we can avoid reading it again
    #         if i != self.ridgeHeightsRaster.currentIndex:
    #             heightsChunk = self.ridgeHeightsRaster.chunks[i]
    #             self.ridgeHeightsRaster.array = self.ridgeHeightsRaster.band.ReadAsArray(0, heightsChunk.rowOffset, self.ridgeHeightsRaster.numCols, heightsChunk.numRows)
    #             self.ridgeHeightsRaster.currentIndex = i
    #         if i != self.valleyDepthsRaster.currentIndex:
    #             depthsChunk = self.valleyDepthsRaster.chunks[i]
    #             self.valleyDepthsRaster.array = self.valleyDepthsRaster.band.ReadAsArray(0, depthsChunk.rowOffset, self.valleyDepthsRaster.numCols, depthsChunk.numRows)
    #             self.valleyDepthsRaster.currentIndex = i
    #         msk = (self.ridgeHeightsRaster.array != self.noData) & (self.valleyDepthsRaster.array() != self.noData)
    #         # start with array of right size for floodplain (since current size will be last initial chunk which may be small)
    #         # note also we have to initialize it to type float and correct to int later
    #         floodplainChunk = self.floodplainRaster.chunks[i]
    #         self.floodplainRaster.array = np.core.full((floodplainChunk.numRows, self.numCols), self.noData, np.float_)
    #         # the next line uses just one write but does not work
    #         #self.floodplainRaster.array[msk] = 1 if (self.valleyDepthsRaster.array[msk] / (self.valleyDepthsRaster.array[msk] + self.ridgeHeightsRaster.array[msk])) <= self.floodThresh else 0
    #         self.floodplainRaster.array[msk] = self.valleyDepthsRaster.array[msk] / (self.valleyDepthsRaster.array[msk] + self.ridgeHeightsRaster.array[msk])
    #         self.floodplainRaster.array[(self.floodplainRaster.array > self.floodThresh) & (self.ridgeHeightsRaster.array != self.noData) & (self.valleyDepthsRaster.array != self.noData)] = 1
    #         self.floodplainRaster.array[(self.floodplainRaster.array <= self.floodThresh) & (self.ridgeHeightsRaster.array != self.noData) & (self.valleyDepthsRaster.array != self.noData)] = 2
    #         self.floodplainRaster.array[msk] = self.floodplainRaster.array[msk] - 1
    #         self.floodplainRaster.array = self.floodplainRaster.array.astype(np.int_)
    #         self.floodplainRaster.array[self.floodplainRaster.array < 0] = self.noData
    #         self.floodplainRaster.band.WriteArray(self.floodplainRaster.array, 0, floodplainChunk.rowOffset)
    #===========================================================================

    def findRidges(self, subbasinsRaster, distRaster, root):
        """
        Create ridge points dictionary (row, col) -> (elevation, maximum branch length) for all cells on 
        subbasin boundaries.
        
        A cell is on a boundary if it has one or more adjacent cells in a different subbasin,
        or if it has an adjacent cell with nodata for its subbasin, ie it is on watershed boundary,
        when its branch length is set to the branch threshold.
        The maximum branch length is the maximum of the branch lengths for adjacent cells 
        that are in a different subbasin or outside the watershed.
        Assumes subbasins, outlet distance and dem rasters have same extents and cell sizes.
        """
        # check assumption about rasters:
        assert self.demTransform == subbasinsRaster.ds.GetGeoTransform(
        ), 'DEM and subbasins rasters not compatible'
        assert self.demTransform == distRaster.ds.GetGeoTransform(
        ), 'DEM and distance to outlet rasters not compatible'
        if len(self._gv.topo.subbasinToStream) == 0:
            # need to calculate some topology
            if self._gv.useGridModel:
                if os.path.exists(self._gv.delinStreamFile):
                    streamLayer = QgsVectorLayer(self._gv.delinStreamFile,
                                                 'Delineated streams', 'ogr')
                else:
                    QSWATUtils.error(
                        'Cannot use branch length algorithm without a delineated stream shapefile',
                        self._gv.isBatch)
                    return
            else:
                streamLayer = QSWATUtils.getLayerByFilename(
                    root.findLayers(), self._gv.streamFile, FileTypes._STREAMS,
                    None, None, None)[0]
                if streamLayer is None:
                    QSWATUtils.error(
                        'Streams layer not found: have you run TauDEM?',
                        self._gv.isBatch)
                    return
            if not self._gv.topo.setUp1(streamLayer):
                return
        self.makeRidges(subbasinsRaster, distRaster)
        time1 = time.process_time()
        self.makeRidgeRaster()
        time2 = time.process_time()
        QSWATUtils.loginfo('Writing ridge raster  took {0} seconds'.format(
            int(time2 - time1)))

    def makeRidges(self, subbasinsRaster, distRaster):
        """
        Set values of ridge points.
        """
        time1 = time.process_time()
        self.ridgePoints = dict()
        subbasinsNoData = subbasinsRaster.band.GetNoDataValue()
        for row in range(self.numRows):
            for col in range(self.numCols):
                subbasin = subbasinsRaster.read(row, col)
                if subbasin == subbasinsNoData:
                    continue
                maxBranchLength = 0
                finished = False
                for dy in [-1, 0, 1]:
                    row1 = row + dy
                    dxs = [-1, 1] if dy == 0 else [-1, 0, 1]
                    for dx in dxs:
                        col1 = col + dx
                        if self.pointInMap(row1, col1):
                            subbasin1 = subbasinsRaster.read(
                                row + dy, col + dx)
                        else:
                            subbasin1 = subbasinsNoData
                        if subbasin1 == subbasinsNoData:
                            maxBranchLength = self.branchThresh
                            finished = True
                            break
                        elif subbasin1 != subbasin:
                            distanceToJoin = self._gv.topo.getDistanceToJoin(
                                subbasin1, subbasin)
                            branchLength = distRaster.read(
                                row1, col1) + distanceToJoin
                            maxBranchLength = max(branchLength,
                                                  maxBranchLength)
                    if finished:
                        break
                if maxBranchLength >= self.branchThresh:
                    elevation = self.demRaster.read(row, col)
                    if elevation != self.demNoData:
                        self.ridgePoints[(row, col)] = (elevation,
                                                        maxBranchLength)
        time2 = time.process_time()
        QSWATUtils.loginfo('Making ridge points took {0} seconds'.format(
            int(time2 - time1)))

    def getRidgeElevation(self, row, col, reportFailure):
        """
        Find nearest ridge point to (row, col) and return its elevation and distance from (row, col).
        
        Distance is cartesian distance, in number of pixels (assumed square).
        Algorithm looks for first acceptable ridge  point on increasing square perimeter around original point,
        as an approximation to the nearest.
        Slope position algorithm for floodplain identification only depends on elevations, not distances, 
        so we don't worry too much about distances to ridge points, and interpret 'nearest' loosely for efficient execution.
        """
        # use view so that changes may be used later in iteration, and avoid repeated searches for adjacent unacceptable points
        n = 0
        while True:
            #=============version where square used has N-s and E-W sides=======
            # for dy in xrange(0-n, n+1):
            #     for dx in xrange(0-n, n+1):
            #         if abs(dy) == n or abs(dx) == n: # on boundary of square
            #             (e, l) = self.ridgePoints.get((row+dy, col+dx), (-1, -1))
            #             if l >= 0:
            #                 return e, math.sqrt(dx * dx + dy * dy)
            #===================================================================
            # version with diagonal sides to square - a little closer to circular maybe?
            if n == 0:
                (e, l) = self.ridgePoints.get((row, col), (-1, -1))
                if l >= 0:
                    return e, 0
            else:
                for dx in range(n + 1):
                    dy = n - dx
                    (e, l) = self.ridgePoints.get((row + dy, col + dx),
                                                  (-1, -1))
                    if l >= 0:
                        return e, math.sqrt(dx * dx + dy * dy)
                    if dx != 0:
                        (e, l) = self.ridgePoints.get((row + dy, col - dx),
                                                      (-1, -1))
                        if l >= 0:
                            return e, math.sqrt(dx * dx + dy * dy)
                    if dy != 0:
                        (e, l) = self.ridgePoints.get((row - dy, col + dx),
                                                      (-1, -1))
                        if l >= 0:
                            return e, math.sqrt(dx * dx + dy * dy)
                        if dx != 0:
                            (e, l) = self.ridgePoints.get((row - dy, col - dx),
                                                          (-1, -1))
                            if l >= 0:
                                return e, math.sqrt(dx * dx + dy * dy)
            n += 1
            if n > 1000:
                if reportFailure:
                    x, y = QSWATTopology.cellToProj(row, col,
                                                    self.demTransform)
                    QSWATUtils.information(
                        'No ridge point found within 1000 pixels of ({0}, {1}).  Is the branch threshold too high?'
                        .format(x, y), self._gv.isBatch)
                return 0, -1

    def propagateFromRidges(self, valleyDepthsFile, dirRaster, accRaster):
        """
        Propagate relative heights of nearest ridge points along flow paths, starting from points with accumulation 1.
        
        Heights are put into ridgeHeightsRaster.  
        Distances from ridge are put into ridgeDistancesRaster  If a point already has a height, 
        but the new path is shorter, the new height overwrites the old.
        """
        time1 = time.process_time()
        self.valleyDepthsRaster = Raster(valleyDepthsFile,
                                         self._gv,
                                         canWrite=False,
                                         isInt=False)
        res = self.valleyDepthsRaster.open(self.chunkCount)
        if not res:
            return
        reportFailure = True
        pathLength = 0  # path length counted in pixels (horizontal and vertical assumed same, so 1)
        diag = math.sqrt(2)
        for row in range(self.numRows):
            for col in range(self.numCols):
                if accRaster.read(row, col) == 1:
                    elevation, pathLength = self.getRidgeElevation(
                        row, col, reportFailure)
                    if pathLength < 0:
                        if reportFailure:
                            reportFailure = False
                        elevation = self.demRaster.read(row, col)
                        pathLength = 0
                    nextRow, nextCol = row, col
                    while True:
                        nextElev = self.demRaster.read(nextRow, nextCol)
                        if nextElev == self.demNoData:
                            break
                        currentPathLength = self.ridgeDistancesRaster.read(
                            nextRow, nextCol)
                        if currentPathLength >= 0:
                            # have a previously stored value
                            if pathLength < currentPathLength:
                                # new path length from ridge is shorter: update heights raster
                                self.ridgeHeightsRaster.write(
                                    nextRow, nextCol, elevation - nextElev)
                                self.ridgeDistancesRaster.write(
                                    nextRow, nextCol, pathLength)
                            else:  # already had shorter path from ridge - no point in continuing down flow path
                                break
                        else:  # no value stored yet
                            self.ridgeHeightsRaster.write(
                                nextRow, nextCol, elevation - nextElev)
                            self.ridgeDistancesRaster.write(
                                nextRow, nextCol, pathLength)
                        pt, isDiag = self.alongDirPoint(
                            nextRow, nextCol, dirRaster)
                        if pt is None:
                            break
                        pathLength += diag if isDiag else 1
                        nextRow, nextCol = pt
        time2 = time.process_time()
        QSWATUtils.loginfo('Propagating ridge points took {0} seconds'.format(
            int(time2 - time1)))

    def makeRidgeRaster(self):
        """Make raster  to show ridges."""
        ridgeFile = QSWATUtils.join(self._gv.demDir,
                                    'ridge' + str(self.branchThresh) + '.tif')
        ridgeRaster = Raster(ridgeFile, self._gv, canWrite=True, isInt=True)
        res = ridgeRaster.open(self.chunkCount,
                               numRows=self.numRows,
                               numCols=self.numCols,
                               transform=self.demTransform,
                               projection=self.demProjection,
                               noData=self.noData)
        if not res:
            return
        for row in range(self.numRows):
            for col in range(self.numCols):
                if self.demRaster.read(row, col) != self.demNoData:
                    val, _ = self.ridgePoints.get((row, col), (-1, -1))
                    ridgeRaster.write(row, col, 0 if val == -1 else 1)
        ridgeRaster.close()
        #QSWATUtils.getLayerByFilename(self._iface.legendInterface().layers(), ridgeFile, FileTypes._OTHER,
        #                                self._gv, None, QSWATUtils._WATERSHED_GROUP_NAME)

    def alongDirPoint(self, row, col, d8Raster):
        """Return next point (as pair or None) along d8Raster direction from (row, col) that has a data value in demRaster."""
        # d8Raster obtained by inverting the d8 flow directions raster has float values, so must cooerce to int
        dir0 = int(d8Raster.read(row, col) - 1)
        if 0 <= dir0 < 8:
            row1 = row + QSWATUtils._dY[dir0]
            col1 = col + QSWATUtils._dX[dir0]
            if self.pointInMap(row1, col1) and self.demRaster.read(
                    row1, col1) != self.demNoData:
                isDiag = (dir0 // 2 == 1)
                return (row1, col1), isDiag
            else:
                return None, False
        else:
            return None, False

    def pointInMap(self, row, col):
        """Return true if row and col are in the limits for the DEM array."""
        return 0 <= row < self.numRows and 0 <= col < self.numCols