Changeset 9513


Ignore:
Timestamp:
Jan 27, 2015, 5:03:44 PM (9 years ago)
Author:
davies
Message:

Adding bilinear raster lookup + nan_interpolation_region_polygons to composite_quantity_setting_functions

Location:
trunk/anuga_core/source/anuga/utilities
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/anuga_core/source/anuga/utilities/quantity_setting_functions.py

    r9511 r9513  
    3232    INPUTS:
    3333        @param quantity_xyValueIn -- A 3 column array with 'x,y, Value'
    34             defining the points used to set the new quantity values
     34            defining the points used to set the new quantity values in
     35            georeferenced coordinates
    3536        @param domain -- The ANUGA domain
    3637        @param k_nearest_neighbors If > 1, then an inverse-distance-weighted average
     
    131132                                        domain,
    132133                                        clip_range = None,
    133                                         nan_treatment = 'exception'):
     134                                        nan_treatment = 'exception',
     135                                        nan_interpolation_region_polygon = None,
     136                                        default_k_nearest_neighbours = 1,
     137                                        default_raster_interpolation = 'pixel',
     138                                        verbose=True):
    134139    """ Make a 'composite function' to set quantities -- applies different
    135140        functions inside different polygon regions.
     
    176181              fi = a .txt or .csv file name containing x, y, z data,
    177182                     with comma separators and an optional header row
    178                      containing letters
     183                     containing letters (nearest neighbour interpolation is used)
    179184              fi = a string rasterFile name (not ending in .txt or .csv)
    180                     which can be passed to quantityRasterFun to make a function,
     185                    which can be passed to quantityRasterFun to make a function
    181186              fi = a numpy array with 3 columns (x,y,Value) in which case
    182187                   nearest-neighbour interpolation is used on the points
     
    212217                poly,fun pairs (in sequence) to set the value.
    213218
     219          @param nan_interpolation_region_polygon = None, or 'All', or a list
     220                of csv or shp filenames containing polygons, or a list of
     221                anuga polygon objects.
     222
     223                If it is not None, then all x,y points which evaluate to nan
     224                on their **first preference** dataset are recorded, and as a
     225                final step, the values at these x,y points
     226                **which are inside the nan_interpolation_region_polygon**
     227                are interpolated from the other x,y,F(x,y) values.
     228   
     229                Nearest neighbour interpolation is used, with
     230                k_nearest_neighbours taken from default_k_nearest_neighbours.
     231
     232                Note that if nan_treatment = 'exception', then nan's will cause
     233                exceptions earlier on in this routine, so you will need
     234                nan_treatment = 'fall_through' to use this option.
     235
     236                Example of why you might want this:
     237                    Say you have 2 elevation datasets (one defining the
     238                    topography above MSL, and the other defining the topography
     239                    below MSL). There might be small nan gaps between them,
     240                    which you would like to fill with interpolation. That
     241                    can be done with this option.
     242               
     243          @param default_k_nearest_neighbours = integer >=1 . The value of
     244                k_nearest_neighbours passed to
     245                make_nearestNeighbour_quantity_function when a 'special_case'
     246                value of fi is passed in (either a point array or a .txt or
     247                .csv point file), or when nan_interpolation_region_polygon is
     248                not None
     249
     250          @param default_raster_interpolation = 'pixel' or 'bilinear'. The value of
     251                'interpolation' passed to quantityRasterFun if a raster filename
     252                is passed as one of the fi.
     253
     254          @param verbose TRUE/FALSE Print more information
     255
    214256        OUTPUT: A function F(x,y) which can be used e.g. to set the quantity
    215257                domain.set_quantity('elevation', F)
     258
    216259    """
    217260    import os
     
    230273                raise Exception('clip_range minima must be less than maxima')
    231274
     275
    232276    def F(x,y):
    233         """This is the function we return
     277        """This is the function returned by composite_quantity_setting_function
    234278           It can be passed to set_quantity
    235279        """
    236         isSet=numpy.zeros(len(x)) # 0/1 - record if each point has been set
    237         quantityVal=x*0 # Function return value
    238         lpf=len(poly_fun_pairs)
    239         if(lpf<=0):
     280        isSet = numpy.zeros(len(x)) # 0/1 - record if each point has been set
     281        quantityVal = x*0 + numpy.nan # Function return value
     282
     283        # Record points which evaluated to nan on their first preference
     284        # dataset.
     285        was_ever_nan = (x*0).astype(int)
     286
     287        lpf = len(poly_fun_pairs)
     288        if(lpf <= 0):
    240289            raise Exception('Must have at least 1 fun-poly-pair')
    241290
    242291        # Make an array of 'transformed' spatial coordinates, for checking
    243292        # polygon inclusion
    244         xll=domain.geo_reference.xllcorner
    245         yll=domain.geo_reference.yllcorner
    246         xy_array_trans=numpy.vstack([x+xll,y+yll]).transpose()
     293        xll = domain.geo_reference.xllcorner
     294        yll = domain.geo_reference.yllcorner
     295        xy_array_trans = numpy.vstack([x+xll,y+yll]).transpose()
    247296
    248297        # Check that none of the pi polygons [except perhaps the last] is 'All'
     
    255304                    raise Exception('Can only have the last polygon = All')
    256305
    257         # MAIN LOOP
     306        # Main Loop
    258307        # Apply the fi inside the pi
    259308        for i in range(lpf):
     
    271320            if(pi == 'All'):
    272321                # Get all unset points
    273                 fInside=(1-isSet)
    274                 fInds=(fInside==1).nonzero()[0]
     322                fInside = (1-isSet)
     323                fInds = (fInside==1).nonzero()[0]
    275324
    276325            else:
     
    289338
    290339                    # Then we get the extent from the raster itself
    291                     pi_path=su.getRasterExtent(fi,asPolygon=True)
     340                    pi_path = su.getRasterExtent(fi,asPolygon=True)
    292341
    293342                elif(type(pi)==str and os.path.isfile(pi) ):
    294343                    # pi is a file
    295                     pi_path=su.read_polygon(pi)
     344                    pi_path = su.read_polygon(pi)
    296345
    297346                else:
    298347                    # pi is the actual polygon data
    299                     pi_path=pi
     348                    pi_path = pi
    300349
    301350                # Get the insides of unset points inside pi_path
    302                 notSet=(isSet==0.).nonzero()[0]
     351                notSet = (isSet==0.).nonzero()[0]
    303352                fInds = inside_polygon(xy_array_trans[notSet,:], pi_path)
    304353                fInds = notSet[fInds]
     
    320369            elif isinstance(fi, (int, long, float)):
    321370                # fi is a numerical constant
    322                 quantityVal[fInds]=fi*1.0
     371                quantityVal[fInds] = fi*1.0
    323372
    324373            elif ( type(fi) is str and os.path.exists(fi)):
     
    330379                    if fi_array.shape[1] is not 3:
    331380                        print 'Treated input file ' + fi +\
    332                               ' as xyz array with '+ \
    333                               str(int(hasLetters)) + ' header row'
     381                              ' as xyz array with an optional header'
    334382                        msg = 'Array should have 3 columns -- x,y,value'
    335383                        raise Exception(msg)
    336384
    337385                    newfi = make_nearestNeighbour_quantity_function(
    338                         fi_array, domain)
     386                        fi_array, domain,
     387                        k_nearest_neighbours = default_k_nearest_neighbours)
    339388                    quantityVal[fInds] = newfi(x[fInds], y[fInds])
    340389
    341390                else:
    342391                    # Treating input file as a raster
    343                     newfi = quantityRasterFun(domain, fi)
     392                    newfi = quantityRasterFun(domain, fi,
     393                        interpolation = default_raster_interpolation)
    344394                    quantityVal[fInds] = newfi(x[fInds], y[fInds])
    345395
     
    348398                    msg = 'Array should have 3 columns -- x,y,value'
    349399                    raise Exception(msg)
    350                 newfi = make_nearestNeighbour_quantity_function(fi, domain)
     400                newfi = make_nearestNeighbour_quantity_function(fi, domain,
     401                    k_nearest_neighbours = default_k_nearest_neighbours)
    351402                quantityVal[fInds] = newfi(x[fInds], y[fInds])
    352403
     
    354405                print 'Error with function from'
    355406                print fi
    356                 msg='Cannot make function from type '+str(type(fi))
     407                msg='Cannot make function from type ' + str(type(fi))
    357408                raise Exception, msg
    358409
     
    361412            ###################################################################
    362413
    363             nan_flag = quantityVal[fInds] != quantityVal[fInds]
     414            nan_flag = (quantityVal[fInds] != quantityVal[fInds])
    364415            nan_inds = nan_flag.nonzero()[0]
     416            was_ever_nan[fInds[nan_inds]] = 1
     417
    365418            if len(nan_inds)>0:
    366419                if nan_treatment == 'exception':
     
    379432                          'in composite_quantity_setting_function. ' + \
    380433                          'They will be passed to later poly_fun_pairs'
    381                     print msg
     434                    if verbose: print msg
    382435                    not_nan_inds = (1-nan_flag).nonzero()[0]
    383436
     
    388441                        msg = '( Actually all the values were nan - ' + \
    389442                              'Are you sure they should be? Possible error?)'
    390                         print msg
     443                        if verbose: print msg
    391444                        continue
     445
    392446                else:
    393447                    msg = 'Found nan values in ' + \
     
    397451
    398452            # Record that the points have been set
    399             isSet[fInds]=1
     453            isSet[fInds] = 1
    400454
    401455            # Enforce clip_range
     
    410464        # End of loop
    411465
     466        # Find points which were nan on their first preference dataset + are
     467        # inside nan_interpolation_region_polygon. Then reinterpolate their
     468        # values from the other x,y, quantityVal points.
     469        if (nan_interpolation_region_polygon is not None) &\
     470           (was_ever_nan.sum() > 0):
     471            if nan_interpolation_region_polygon == 'All':
     472                points_to_reinterpolate = was_ever_nan.nonzero()[0]
     473            else:
     474                # nan_interpolation_region_polygon contains information on 1 or
     475                # more polygons
     476                # Inside those polygons, we need to re-interpolate points which
     477                # first evaluted to na
     478                possible_points_to_reint = was_ever_nan.nonzero()[0]
     479                points_to_reinterpolate = numpy.array([]).astype(int)
     480
     481                for i in range(len(nan_interpolation_region_polygon)):
     482                    nan_pi = nan_interpolation_region_polygon[i]
     483
     484                    # Ensure nan_pi = list of x,y points making a polygon
     485                    if(type(nan_pi) == str):
     486                        nan_pi = su.read_polygon(nan_pi)
     487             
     488                    points_in_nan_pi = inside_polygon(
     489                        xy_array_trans[possible_points_to_reint,:],
     490                        nan_pi)
     491                   
     492                    if len(points_in_nan_pi)>0:
     493                        points_to_reinterpolate = numpy.hstack(
     494                            [points_to_reinterpolate,
     495                             points_in_nan_pi])
     496
     497            if verbose:
     498                print 'Re-interpolating ', len(points_to_reinterpolate),\
     499                      ' points which were nan under their',\
     500                      ' first-preference and are inside the',\
     501                      ' nan_interpolation_region_polygon'
     502
     503            # Find the interpolation points = points not needing reinterpolation
     504            ip = x*0 + 1
     505            ip[points_to_reinterpolate] = 0
     506            number_of_ip = ip.sum()
     507            ip = ip.nonzero()[0]
     508         
     509            if(number_of_ip < default_k_nearest_neighbours):
     510                raise Exception('Too few non-nan points to interpolate from')
     511
     512            # Make function for re-interpolation. Note this requires
     513            # x,y,z in georeferenced coordinates, whereas x,y are ANUGA
     514            # coordinates
     515            reinterp_F = make_nearestNeighbour_quantity_function(
     516                numpy.vstack([xy_array_trans[ip,0], xy_array_trans[ip,1],
     517                              quantityVal[ip]]).transpose(),
     518                domain,
     519                k_nearest_neighbours = default_k_nearest_neighbours)
     520
     521            # re-interpolate
     522            quantityVal[points_to_reinterpolate] = reinterp_F(
     523                x[points_to_reinterpolate], y[points_to_reinterpolate])
     524
     525            isSet[points_to_reinterpolate] = 1
     526           
     527        # Check there are no remaining nan values
    412528        if( min(isSet) != 1):
    413             print 'Some points were not inside any polygon, ',\
    414                   'or evalute to nan over all datasets'
     529            print 'Some points remain as nan, which is not allowed'
    415530            unset_inds = (isSet!=1).nonzero()[0]
    416531            lui = min(5, len(unset_inds))
     
    428543##############################################################################
    429544
    430 def quantityRasterFun(domain, rasterFile):
     545def quantityRasterFun(domain, rasterFile, interpolation='pixel'):
    431546    """
    432547    Make a function whick takes x,y in ANUGA coordinates, and returns the values
    433548    on a raster rasterFile
    434549   
    435     This can be used to set a quantity, and takes care of the manual conversion from
    436     ANUGA coordinates to spatial coordinates.
    437 
    438     INPUTS: domain = ANUGA domain
    439             rasterFile = Filename of the raster to extract point values from
     550    This can be used to set a quantity, and takes care of the manual conversion
     551    from ANUGA coordinates to spatial coordinates.
     552
     553    INPUTS: @param domain = ANUGA domain
     554            @param rasterFile = Filename of the raster to extract point values
     555                    from
     556            @param interpolation = 'pixel' (in which case the point value is
     557                    set from the pixel it is on) or 'bilinear' in which case
     558                    the point value is set from bilinear interpolation of
     559                    pixels.
    440560   
    441561    OUTPUT: Function which takes x,y in ANUGA coordinates, and outputs their
     
    448568        yll=domain.geo_reference.yllcorner
    449569        inDat=scipy.vstack([x+xll,y+yll]).transpose()
    450         return rasterValuesAtPoints(xy=inDat,rasterFile=rasterFile)
     570        return rasterValuesAtPoints(xy=inDat,rasterFile=rasterFile,
     571                                    interpolation=interpolation)
    451572
    452573    return QFun
     
    461582        interpolator is used.
    462583
     584        This has been superceeded by composite_quantity_setting_function
     585
    463586        INPUT:
    464             Pt_Pol_Data = a list with [ [ Polygon_0, Pt_XYZ_0],
    465                                         [ Polygon_1, Pt_XYZ_1],
    466                                         ... ]
     587            @param Pt_Pol_Data = a list with [ [ Polygon_0, Pt_XYZ_0],
     588                                               [ Polygon_1, Pt_XYZ_1],
     589                                               ...
     590                                             ]
    467591                    Here Polygon_i is a polygon in ANUGA format,
    468592                    and Pt_XYZ_i is a 3 column array of x,y,Value points
    469             quantity_raster = A GDAL-compatible quantity raster
    470             domain = ANUGA domain
     593            @param quantity_raster = A GDAL-compatible quantity raster
     594            @param domain = ANUGA domain
    471595    """
    472596
  • trunk/anuga_core/source/anuga/utilities/spatialInputUtil.py

    r9373 r9513  
    696696        rasterFile,
    697697        band=1,
    698         nodata_rel_tol = 1.0e-08):
     698        nodata_rel_tol = 1.0e-08,
     699        interpolation = 'pixel'):
    699700        """
    700701            Get raster values at point locations.
     
    712713                This allows for truncation errors in nodata values which seem
    713714                to be introduced by some file-type conversions
     715
     716            @param interpolation 'pixel' or 'bilinear' determines how the
     717                    raster cell values are used to set the point value
    714718   
    715719            OUTPUT:
     
    730734       
    731735        # Get coordinates in pixel values
    732         px = ((xy[:,0] - xOrigin) / pixelWidth).astype(int) #x
    733         py = ((xy[:,1] - yOrigin) / pixelHeight).astype(int) #y
     736        px = (xy[:,0] - xOrigin) / pixelWidth
     737        py = (xy[:,1] - yOrigin) / pixelHeight
    734738     
    735739        # Hold elevation
    736         elev=px*0.
     740        elev = px*0.
    737741   
    738742        # Get the right character for struct.unpack
     
    753757 
    754758        # Upper bounds for pixel values, so we can fail gracefully
    755         xMax=raster.RasterXSize
    756         yMax=raster.RasterYSize
     759        xMax = raster.RasterXSize
     760        yMax = raster.RasterYSize
    757761        if(px.max()<xMax and px.min()>=0 and py.max()<yMax and py.min()>=0):
    758762            pass
     
    763767        # Get values -- seems we have to loop, but it is efficient enough
    764768        for i in range(len(px)):
    765             xC=int(px[i])
    766             yC=int(py[i])
    767             structval=rasterBand.ReadRaster(xC,yC,1,1,buf_type=rasterBand.DataType)
    768             elev[i] = struct.unpack(CtypeName, structval)[0]
     769
     770            if(interpolation=='pixel'):
     771                # Pixel coordinates
     772                xC=int(numpy.floor(px[i]))
     773                yC=int(numpy.floor(py[i]))
     774
     775                structval = rasterBand.ReadRaster(xC,yC,1,1,
     776                    buf_type=rasterBand.DataType)
     777                elev[i] = struct.unpack(CtypeName, structval)[0]
     778
     779            elif(interpolation=='bilinear'):
     780                # Pixel coordinates
     781                xl = int(numpy.floor(px[i]))
     782                yl = int(numpy.floor(py[i]))
     783
     784                # Find neighbours required for bilinear interpolation
     785                # l = lower, u = upper
     786                if(px[i] - xl > 0.5):
     787                    xu = min(xl + 1, xMax - 1)
     788                else:
     789                    # Swap xl for xu
     790                    xu = xl
     791                    xl = max(xu - 1, 0)
     792
     793                if(py[i] - yl > 0.5):
     794                    yu = min(yl + 1, yMax - 1)
     795                else:
     796                    yu = yl
     797                    yl = max(yu - 1, 0)
     798
     799                # Map x,y to unit square
     800                x = px[i] - (xl + 0.5)
     801                y = py[i] - (yl + 0.5)
     802
     803                if not ( (x>=0.) & (x<=1.)):
     804                    print x, xl, xu, px[i]
     805                    raise Exception('x out of bounds')
     806
     807                if not ( (y>=0.) & (y<=1.)):
     808                    print y, yl, yu, py[i]
     809                    raise Exception('y out of bounds')
     810
     811                # Lower-left
     812                structval = rasterBand.ReadRaster(xl,yl,1,1,
     813                    buf_type=rasterBand.DataType)
     814                r00 = struct.unpack(CtypeName, structval)[0]
     815                # Upper left
     816                structval = rasterBand.ReadRaster(xl,yu,1,1,
     817                    buf_type=rasterBand.DataType)
     818                r01 = struct.unpack(CtypeName, structval)[0]
     819                # Lower-right
     820                structval = rasterBand.ReadRaster(xu,yl,1,1,
     821                    buf_type=rasterBand.DataType)
     822                r10 = struct.unpack(CtypeName, structval)[0]
     823                # Upper right
     824                structval = rasterBand.ReadRaster(xu,yu,1,1,
     825                    buf_type=rasterBand.DataType)
     826                r11 = struct.unpack(CtypeName, structval)[0]
     827
     828                # Bilinear interpolation
     829                elev[i] = r00*(1-x)*(1-y) + r01*(1-x)*y +\
     830                          r10*x*(1-y) + r11*x*y
     831            else:
     832                raise Exception('Unknown value of "interpolation"')           
    769833
    770834        # Deal with nodata
  • trunk/anuga_core/source/anuga/utilities/test/test_quantity_setting_functions.py

    r9512 r9513  
    109109
    110110        # Test that F evaluated in 'ANUGA coordinates' [lower-left = 0,0] is correct
     111        xGet=numpy.array([0., 10., 10., 0., 100.]) + 0.1
     112        yGet=numpy.array([0., 0., 20.,90., 100. ]) + 0.1
     113        expected=numpy.floor(xGet+yGet)
     114        output=F(xGet,yGet)
     115        assert(numpy.allclose(output,expected))
     116
     117
     118        # Test with 3 nearest neighbours at points which are exactly on data points
     119        F = qs.make_nearestNeighbour_quantity_function(inPts, domain,
     120                threshold_distance = 9.0e+100, background_value = 9.0e+100,
     121                k_nearest_neighbours = 3)
     122
    111123        xGet=numpy.array([0., 10., 10., 0., 100.])
    112124        yGet=numpy.array([0., 0., 20.,90., 100. ])
     
    114126        output=F(xGet,yGet)
    115127        assert(numpy.allclose(output,expected))
     128
     129        # Test with 4 nearest neighbours, at points which are not exactly on data points 
     130        F = qs.make_nearestNeighbour_quantity_function(inPts, domain,
     131                threshold_distance = 9.0e+100, background_value = 9.0e+100,
     132                k_nearest_neighbours = 4)
     133        # Test at non-trivial location
     134        xGet=numpy.array([0., 10., 10., 0., 99.]) + 0.5
     135        yGet=numpy.array([0., 0., 20.,90., 99. ]) + 0.5
     136        expected=xGet+yGet
     137        output=F(xGet,yGet)
     138        assert(numpy.allclose(output,expected))
     139
    116140       
    117141        #################################################################################
     
    259283        self.assertRaises(Exception, lambda: should_fail_1())
    260284
     285        ###########################################################################
     286        # This example features a function with some nan return values, and uses
     287        # the nan_interpolation_region_polygon to try to fix it
     288        def f0(x,y):
     289            output = x/10.
     290            output[0] = numpy.nan
     291            return output
     292
     293        F = qs.composite_quantity_setting_function(
     294            [[trenchPoly, f0], ['Extent', 'PointData_ElevTest.tif']],
     295            domain,
     296            nan_treatment = 'fall_through',
     297            nan_interpolation_region_polygon = [trenchPoly],
     298            default_k_nearest_neighbours = 3,
     299            default_raster_interpolation = 'bilinear',
     300            verbose=False)
     301
     302        # Points where we test the function. We deliberately use many points with x=50,
     303        # which happens to ensure that the nan value is replaced with the same
     304        # value it would have had anyway
     305        testPts_X = numpy.array([50.,50.00, 50., 50., 97., 51., 3.])
     306        testPts_Y = numpy.array([1.,    2., 3. , 4  , 20., 30., 60.])
     307        fitted = F(testPts_X,testPts_Y)
     308       
     309        # We should have no nan values
     310        assert(sum(fitted!=fitted) == 0)
     311
     312        # Now the fitted value in the trench should be determined by f0 because
     313        # the re-interpolation of nan values was designed to ensure it
     314        assert(numpy.allclose(fitted[0],50./10.))
     315
    261316        return
    262317
  • trunk/anuga_core/source/anuga/utilities/test/test_spatialInputUtil.py

    r9490 r9513  
    340340        z_predicted=numpy.round(xA)+numpy.round(yA)-tifRange[0]-tifRange[2]-1.0
    341341        InDat=numpy.vstack([xA,yA]).transpose()
     342
    342343        z_fitted=su.rasterValuesAtPoints(InDat, rasterFile='PointData_TestData.tif')
    343344        try:
     
    345346        except:
    346347            raise Exception, 'Error could be in rasterValuesAtPoints or in Make_Geotif'
     348
     349
     350        # Try with bilinear interpolation
     351        z_fitted=su.rasterValuesAtPoints(InDat, rasterFile='PointData_TestData.tif',
     352            interpolation='bilinear')
     353        z_predicted = xA + yA - tifRange[0] - tifRange[2] - 1.0
     354        try:
     355            assert(numpy.allclose(z_fitted,z_predicted))
     356        except:
     357            raise Exception, 'Error could be in rasterValuesAtPoints or in Make_Geotif'
     358
     359
    347360        return
    348361
Note: See TracChangeset for help on using the changeset viewer.