source: inundation/fit_interpolate/interpolate.py @ 2659

Last change on this file since 2659 was 2659, checked in by duncan, 18 years ago

Added an extra test, changed interpolate function to handle points outside the mesh

File size: 26.2 KB
Line 
1"""Least squares interpolation.
2
3   Implements a least-squares interpolation.
4
5   Ole Nielsen, Stephen Roberts, Duncan Gray, Christopher Zoppou
6   Geoscience Australia, 2004.
7
8DESIGN ISSUES
9* what variables should be global?
10- if there are no global vars functions can be moved around alot easier
11
12* What will be the public interface to this class?
13
14TO DO
15* remove points outside the mesh ?(in interpolate_block)?
16* geo-ref (in interpolate_block)
17* add functional interpolate interface - in mesh and points, out interp data
18"""
19
20import time
21import os
22from warnings import warn
23
24from Numeric import zeros, array, Float, Int, dot, transpose, concatenate, \
25     ArrayType, allclose, take, NewAxis, arange
26
27from pyvolution.mesh import Mesh
28from utilities.sparse import Sparse, Sparse_CSR
29from utilities.cg_solve import conjugate_gradient, VectorShapeError
30from coordinate_transforms.geo_reference import Geo_reference
31from pyvolution.quad import build_quadtree
32from utilities.numerical_tools import ensure_numeric, mean, INF
33from utilities.polygon import in_and_outside_polygon
34from geospatial_data.geospatial_data import Geospatial_data
35
36from search_functions import search_tree_of_vertices
37
38class Interpolate:
39   
40    def __init__(self,
41                 vertex_coordinates,
42                 triangles,
43                 mesh_origin = None,
44                 verbose=False,
45                 max_vertices_per_cell=30):
46
47
48        """ Build interpolation matrix mapping from
49        function values at vertices to function values at data points
50
51        Inputs:
52
53          vertex_coordinates: List of coordinate pairs [xi, eta] of
54              points constituting a mesh (or an m x 2 Numeric array)
55              Points may appear multiple times
56              (e.g. if vertices have discontinuities)
57
58          triangles: List of 3-tuples (or a Numeric array) of
59              integers representing indices of all vertices in the mesh.
60
61          mesh_origin: 3-tuples consisting of
62              UTM zone, easting and northing.
63              If specified vertex coordinates are assumed to be
64              relative to their respective origins.
65
66          max_vertices_per_cell: Number of vertices in a quad tree cell
67          at which the cell is split into 4.
68
69        """
70
71        # why no alpha in parameter list?
72       
73        # Initialise variabels
74        self._A_can_be_reused = False
75        self._point_coordinates = None
76       
77        #Convert input to Numeric arrays
78        triangles = ensure_numeric(triangles, Int)
79        vertex_coordinates = ensure_numeric(vertex_coordinates, Float)
80        #Build underlying mesh
81        if verbose: print 'Building mesh'
82        #self.mesh = General_mesh(vertex_coordinates, triangles,
83        #FIXME: Trying the normal mesh while testing precrop,
84        #       The functionality of boundary_polygon is needed for that
85
86        #FIXME: geo_ref can also be a geo_ref object
87        #FIXME: move this to interpolate_block
88
89        #Note: not so keen to use geospacial to represent the mesh,
90        # since the data structure is
91        # points, geo-ref and triangles, rather than just points and geo-ref
92        # this info will usually be coming from a domain instance.
93        if mesh_origin is None:
94            geo = None
95        else:
96            geo = Geo_reference(mesh_origin[0],mesh_origin[1],mesh_origin[2])
97        self.mesh = Mesh(vertex_coordinates, triangles,
98                         geo_reference = geo)
99       
100        self.mesh.check_integrity()
101        self.root = build_quadtree(self.mesh,
102                              max_points_per_cell = max_vertices_per_cell)
103        #print "self.root",self.root.show()
104       
105       
106    def _build_interpolation_matrix_A(self,
107                                     point_coordinates,
108                                     verbose = False):
109        """Build n x m interpolation matrix, where
110        n is the number of data points and
111        m is the number of basis functions phi_k (one per vertex)
112
113        This algorithm uses a quad tree data structure for fast binning
114        of data points
115        origin is a 3-tuple consisting of UTM zone, easting and northing.
116        If specified coordinates are assumed to be relative to this origin.
117
118        This one will override any data_origin that may be specified in
119        instance interpolation
120
121        Preconditions
122        Point_coordindates and mesh vertices have the same origin.
123        """
124
125
126       
127        #Convert point_coordinates to Numeric arrays, in case it was a list.
128        point_coordinates = ensure_numeric(point_coordinates, Float)
129       
130        if verbose: print 'Getting indices inside mesh boundary'
131        self.inside_poly_indices, self.outside_poly_indices  = \
132                     in_and_outside_polygon(point_coordinates,
133                                            self.mesh.get_boundary_polygon(),
134                                            closed = True, verbose = verbose)
135       
136        #Build n x m interpolation matrix
137        m = self.mesh.coordinates.shape[0] #Nbr of basis functions (1/vertex)
138        n = point_coordinates.shape[0]     #Nbr of data points
139
140        if verbose: print 'Number of datapoints: %d' %n
141        if verbose: print 'Number of basis functions: %d' %m
142
143        A = Sparse(n,m)
144
145        #Compute matrix elements for points inside the mesh
146        for i in self.inside_poly_indices:
147            #For each data_coordinate point
148            if verbose and i%((n+10)/10)==0: print 'Doing %d of %d' %(i, n)
149            x = point_coordinates[i]
150            element_found, sigma0, sigma1, sigma2, k = \
151                           search_tree_of_vertices(self.root, self.mesh, x)
152            #Update interpolation matrix A if necessary
153            if element_found is True:
154                #Assign values to matrix A
155
156                j0 = self.mesh.triangles[k,0] #Global vertex id for sigma0
157                j1 = self.mesh.triangles[k,1] #Global vertex id for sigma1
158                j2 = self.mesh.triangles[k,2] #Global vertex id for sigma2
159
160                sigmas = {j0:sigma0, j1:sigma1, j2:sigma2}
161                js     = [j0,j1,j2]
162
163                for j in js:
164                    A[i,j] = sigmas[j]
165            else:
166                msg = 'Could not find triangle for point', x
167                raise Exception(msg)
168        return A
169
170
171     # FIXME: What is a good start_blocking_len value?
172    def interpolate(self, f, point_coordinates = None,
173                    start_blocking_len = 500000, verbose=False):
174        """Interpolate mesh data f to determine values, z, at points.
175
176        f is the data on the mesh vertices.
177
178        The mesh values representing a smooth surface are
179        assumed to be specified in f.
180
181        Inputs:
182          f: Vector or array of data at the mesh vertices.
183              If f is an array, interpolation will be done for each column as
184              per underlying matrix-matrix multiplication
185          point_coordinates: Interpolate mesh data to these positions.
186              List of coordinate pairs [x, y] of
187              data points or an nx2 Numeric array or a Geospatial_data object
188             
189              If point_coordinates is absent, the points inputted last time
190              this method was called are used, if possible.
191          start_blocking_len: If the # of points is more or greater than this,
192              start blocking
193
194        Output:
195          Interpolated values at inputted points (z).
196        """
197        #print "point_coordinates interpolate.interpolate",point_coordinates
198        # Can I interpolate, based on previous point_coordinates?
199        if isinstance(point_coordinates,Geospatial_data):
200            point_coordinates = point_coordinates.get_data_points( \
201                absolute = True)
202       
203        if point_coordinates is None:
204            if self._A_can_be_reused is True and \
205            len(self._point_coordinates) < start_blocking_len: 
206                z = self._get_point_data_z(f,
207                                           verbose=verbose)
208            elif self._point_coordinates is not None:
209                #     if verbose, give warning
210                if verbose:
211                    print 'WARNING: Recalculating A matrix, due to blocking.'
212                point_coordinates = self._point_coordinates
213            else:
214                #There are no good point_coordinates. import sys; sys.exit()
215                msg = 'ERROR (interpolate.py): No point_coordinates inputted'
216                raise Exception(msg)
217           
218           
219        if point_coordinates is not None:
220            self._point_coordinates = point_coordinates
221            if len(point_coordinates) < start_blocking_len or \
222                   start_blocking_len == 0:
223                self._A_can_be_reused = True
224                z = self.interpolate_block(f, point_coordinates,
225                                           verbose=verbose)
226            else:
227                #Handle blocking
228                self._A_can_be_reused = False
229                start=0
230                # creating a dummy array to concatenate to.
231                z = zeros((0,2)) # 2 for points in 2D space
232                for end in range(start_blocking_len
233                                 ,len(point_coordinates)
234                                 ,start_blocking_len):
235                    t = self.interpolate_block(f, point_coordinates[start:end],
236                                               verbose=verbose)
237                    z = concatenate((z,t))
238                    start = end
239                end = len(point_coordinates)
240                t = self.interpolate_block(f, point_coordinates[start:end],
241                                           verbose=verbose)
242                z = concatenate((z,t))
243        return z
244
245    def interpolate_block(self, f, point_coordinates = None, verbose=False):
246        """
247        Call this if you want to control the blocking or make sure blocking
248        doesn't occur.
249
250        Return the point data, z.
251       
252        See interpolate for doc info.
253        """
254        if point_coordinates is not None:
255            self._A =self._build_interpolation_matrix_A(point_coordinates,
256                                                       verbose=verbose)
257        return self._get_point_data_z(f)
258
259    def _get_point_data_z(self, f, verbose=False):
260        """
261        Return the point data, z.
262       
263        Precondition,
264        The _A matrix has been created
265        """
266        z = self._A * f
267        # Taking into account points outside the mesh.
268        #print "self.outside_poly_indices", self.outside_poly_indices
269        #print "self.inside_poly_indices", self.inside_poly_indices
270        #print "z", z
271        #print "z.size", z.size
272        for i in self.outside_poly_indices: 
273            z[i] = INF
274        return z
275
276
277class Interpolation_function:
278    """Interpolation_interface - creates callable object f(t, id) or f(t,x,y)
279    which is interpolated from time series defined at vertices of
280    triangular mesh (such as those stored in sww files)
281
282    Let m be the number of vertices, n the number of triangles
283    and p the number of timesteps.
284
285    Mandatory input
286        time:               px1 array of monotonously increasing times (Float)
287        quantities:         Dictionary of arrays or 1 array (Float)
288                            The arrays must either have dimensions pxm or mx1.
289                            The resulting function will be time dependent in
290                            the former case while it will be constan with
291                            respect to time in the latter case.
292       
293    Optional input:
294        quantity_names:     List of keys into the quantities dictionary
295        vertex_coordinates: mx2 array of coordinates (Float)
296        triangles:          nx3 array of indices into vertex_coordinates (Int)
297        interpolation_points: Nx2 array of coordinates to be interpolated to
298        verbose:            Level of reporting
299   
300   
301    The quantities returned by the callable object are specified by
302    the list quantities which must contain the names of the
303    quantities to be returned and also reflect the order, e.g. for
304    the shallow water wave equation, on would have
305    quantities = ['stage', 'xmomentum', 'ymomentum']
306
307    The parameter interpolation_points decides at which points interpolated
308    quantities are to be computed whenever object is called.
309    If None, return average value
310    """
311
312   
313   
314    def __init__(self,
315                 time,
316                 quantities,
317                 quantity_names = None, 
318                 vertex_coordinates = None,
319                 triangles = None,
320                 interpolation_points = None,
321                 verbose = False):
322        """Initialise object and build spatial interpolation if required
323        """
324
325        from Numeric import array, zeros, Float, alltrue, concatenate,\
326             reshape, ArrayType
327
328
329        #from util import mean, ensure_numeric
330        from config import time_format
331        import types
332
333
334        #Check temporal info
335        time = ensure_numeric(time)       
336        msg = 'Time must be a monotonuosly '
337        msg += 'increasing sequence %s' %time
338        assert alltrue(time[1:] - time[:-1] >= 0 ), msg
339
340
341        #Check if quantities is a single array only
342        if type(quantities) != types.DictType:
343            quantities = ensure_numeric(quantities)
344            quantity_names = ['Attribute']
345
346            #Make it a dictionary
347            quantities = {quantity_names[0]: quantities}
348
349
350        #Use keys if no names are specified
351        if quantity_names is None:
352            quantity_names = quantities.keys()
353
354
355        #Check spatial info
356        if vertex_coordinates is None:
357            self.spatial = False
358        else:   
359            vertex_coordinates = ensure_numeric(vertex_coordinates)
360
361            assert triangles is not None, 'Triangles array must be specified'
362            triangles = ensure_numeric(triangles)
363            self.spatial = True           
364
365             
366        #Save for use with statistics
367        self.quantity_names = quantity_names       
368        self.quantities = quantities       
369        self.vertex_coordinates = vertex_coordinates
370        self.interpolation_points = interpolation_points
371        self.T = time[:]  # Time assumed to be relative to starttime
372        self.index = 0    # Initial time index
373        self.precomputed_values = {}
374           
375        #Precomputed spatial interpolation if requested
376        if interpolation_points is not None:
377            if self.spatial is False:
378                raise 'Triangles and vertex_coordinates must be specified'
379           
380            try:
381                self.interpolation_points = ensure_numeric(interpolation_points)
382            except:
383                msg = 'Interpolation points must be an N x 2 Numeric array '+\
384                      'or a list of points\n'
385                msg += 'I got: %s.' %(str(self.interpolation_points)[:60] +\
386                                      '...')
387                raise msg
388
389
390            m = len(self.interpolation_points)
391            p = len(self.T)
392           
393            for name in quantity_names:
394                self.precomputed_values[name] = zeros((p, m), Float)
395
396            #Build interpolator
397            interpol = Interpolate(vertex_coordinates,
398                                     triangles,
399                                     #point_coordinates = \
400                                     #self.interpolation_points,
401                                     #alpha = 0,
402                                     verbose = verbose)
403
404            if verbose: print 'Interpolate'
405            for i, t in enumerate(self.T):
406                #Interpolate quantities at this timestep
407                if verbose and i%((p+10)/10)==0:
408                    print ' time step %d of %d' %(i, p)
409                   
410                for name in quantity_names:
411                    if len(quantities[name].shape) == 2:
412                        result = interpol.interpolate(quantities[name][i,:],
413                                     point_coordinates = \
414                                     self.interpolation_points)
415                    else:
416                       #Assume no time dependency
417                       result = interpol.interpolate(quantities[name][:],
418                                     point_coordinates = \
419                                     self.interpolation_points)
420                       
421                    self.precomputed_values[name][i, :] = result
422                   
423            #Report
424            if verbose:
425                print self.statistics()
426                #self.print_statistics()
427           
428        else:
429            #Store quantitites as is
430            for name in quantity_names:
431                self.precomputed_values[name] = quantities[name]
432
433
434    def __repr__(self):
435        #return 'Interpolation function (spatio-temporal)'
436        return self.statistics()
437   
438
439    def __call__(self, t, point_id = None, x = None, y = None):
440        """Evaluate f(t), f(t, point_id) or f(t, x, y)
441
442        Inputs:
443          t: time - Model time. Must lie within existing timesteps
444          point_id: index of one of the preprocessed points.
445          x, y:     Overrides location, point_id ignored
446         
447          If spatial info is present and all of x,y,point_id
448          are None an exception is raised
449                   
450          If no spatial info is present, point_id and x,y arguments are ignored
451          making f a function of time only.
452
453         
454          FIXME: point_id could also be a slice
455          FIXME: What if x and y are vectors?
456          FIXME: What about f(x,y) without t?
457        """
458
459        from math import pi, cos, sin, sqrt
460        from Numeric import zeros, Float
461        from util import mean       
462
463        if self.spatial is True:
464            if point_id is None:
465                if x is None or y is None:
466                    msg = 'Either point_id or x and y must be specified'
467                    raise Exception(msg)
468            else:
469                if self.interpolation_points is None:
470                    msg = 'Interpolation_function must be instantiated ' +\
471                          'with a list of interpolation points before parameter ' +\
472                          'point_id can be used'
473                    raise Exception(msg)
474
475
476        msg = 'Time interval [%s:%s]' %(self.T[0], self.T[1])
477        msg += ' does not match model time: %s\n' %t
478        if t < self.T[0]: raise Exception(msg)
479        if t > self.T[-1]: raise Exception(msg)
480
481        oldindex = self.index #Time index
482
483        #Find current time slot
484        while t > self.T[self.index]: self.index += 1
485        while t < self.T[self.index]: self.index -= 1
486
487        if t == self.T[self.index]:
488            #Protect against case where t == T[-1] (last time)
489            # - also works in general when t == T[i]
490            ratio = 0
491        else:
492            #t is now between index and index+1
493            ratio = (t - self.T[self.index])/\
494                    (self.T[self.index+1] - self.T[self.index])
495
496        #Compute interpolated values
497        q = zeros(len(self.quantity_names), Float)
498
499        for i, name in enumerate(self.quantity_names):
500            Q = self.precomputed_values[name]
501
502            if self.spatial is False:
503                #If there is no spatial info               
504                assert len(Q.shape) == 1
505
506                Q0 = Q[self.index]
507                if ratio > 0: Q1 = Q[self.index+1]
508
509            else:
510                if x is not None and y is not None:
511                    #Interpolate to x, y
512                   
513                    raise 'x,y interpolation not yet implemented'
514                else:
515                    #Use precomputed point
516                    Q0 = Q[self.index, point_id]
517                    if ratio > 0:
518                        Q1 = Q[self.index+1, point_id]
519           
520            #Linear temporal interpolation   
521            if ratio > 0:
522                if Q0 == INF and Q1 == INF:
523                    q[i]  = Q0
524                else:
525                    q[i] = Q0 + ratio*(Q1 - Q0)
526            else:
527                q[i] = Q0
528
529
530        #Return vector of interpolated values
531        #if len(q) == 1:
532        #    return q[0]
533        #else:
534        #    return q
535
536
537        #Return vector of interpolated values
538        #FIXME:
539        if self.spatial is True:
540            return q
541        else:
542            #Replicate q according to x and y
543            #This is e.g used for Wind_stress
544            if x is None or y is None: 
545                return q
546            else:
547                try:
548                    N = len(x)
549                except:
550                    return q
551                else:
552                    from Numeric import ones, Float
553                    #x is a vector - Create one constant column for each value
554                    N = len(x)
555                    assert len(y) == N, 'x and y must have same length'
556                    res = []
557                    for col in q:
558                        res.append(col*ones(N, Float))
559                       
560                return res
561
562
563    def statistics(self):
564        """Output statistics about interpolation_function
565        """
566       
567        vertex_coordinates = self.vertex_coordinates
568        interpolation_points = self.interpolation_points               
569        quantity_names = self.quantity_names
570        quantities = self.quantities
571        precomputed_values = self.precomputed_values                 
572               
573        x = vertex_coordinates[:,0]
574        y = vertex_coordinates[:,1]               
575
576        str =  '------------------------------------------------\n'
577        str += 'Interpolation_function (spatio-temporal) statistics:\n'
578        str += '  Extent:\n'
579        str += '    x in [%f, %f], len(x) == %d\n'\
580               %(min(x), max(x), len(x))
581        str += '    y in [%f, %f], len(y) == %d\n'\
582               %(min(y), max(y), len(y))
583        str += '    t in [%f, %f], len(t) == %d\n'\
584               %(min(self.T), max(self.T), len(self.T))
585        str += '  Quantities:\n'
586        for name in quantity_names:
587            q = quantities[name][:].flat
588            str += '    %s in [%f, %f]\n' %(name, min(q), max(q))
589
590        if interpolation_points is not None:   
591            str += '  Interpolation points (xi, eta):'\
592                   ' number of points == %d\n' %interpolation_points.shape[0]
593            str += '    xi in [%f, %f]\n' %(min(interpolation_points[:,0]),
594                                            max(interpolation_points[:,0]))
595            str += '    eta in [%f, %f]\n' %(min(interpolation_points[:,1]),
596                                             max(interpolation_points[:,1]))
597            str += '  Interpolated quantities (over all timesteps):\n'
598       
599            for name in quantity_names:
600                q = precomputed_values[name][:].flat
601                str += '    %s at interpolation points in [%f, %f]\n'\
602                       %(name, min(q), max(q))
603        str += '------------------------------------------------\n'
604
605        return str
606
607def interpolate_sww(sww_file, time, interpolation_points,
608                    quantity_names = None, verbose = False):
609    """
610    obsolete.
611    use file_function in utils
612    """
613    #open sww file
614    x, y, volumes, time, quantities = read_sww(sww_file)
615    print "x",x
616    print "y",y
617   
618    print "time", time
619    print "quantities", quantities
620
621    #Add the x and y together
622    vertex_coordinates = concatenate((x[:,NewAxis], y[:,NewAxis]),axis=1)
623
624    #Will return the quantity values at the specified times and locations
625    interp =  Interpolation_interface(
626                 time,
627                 quantities,
628                 quantity_names = quantity_names, 
629                 vertex_coordinates = vertex_coordinates,
630                 triangles = volumes,
631                 interpolation_points = interpolation_points,
632                 verbose = verbose)
633
634
635def read_sww(file_name):
636    """
637    obsolete - Nothing should be calling this
638   
639    Read in an sww file.
640   
641    Input;
642    file_name - the sww file
643   
644    Output;
645    x - Vector of x values
646    y - Vector of y values
647    z - Vector of bed elevation
648    volumes - Array.  Each row has 3 values, representing
649    the vertices that define the volume
650    time - Vector of the times where there is stage information
651    stage - array with respect to time and vertices (x,y)
652    """
653   
654    #FIXME Have this reader as part of data_manager?
655   
656    from Scientific.IO.NetCDF import NetCDFFile     
657    import tempfile
658    import sys
659    import os
660   
661    #Check contents
662    #Get NetCDF
663   
664    # see if the file is there.  Throw a QUIET IO error if it isn't
665    # I don't think I could implement the above
666   
667    #throws prints to screen if file not present
668    junk = tempfile.mktemp(".txt")
669    fd = open(junk,'w')
670    stdout = sys.stdout
671    sys.stdout = fd
672    fid = NetCDFFile(file_name, 'r') 
673    sys.stdout = stdout
674    fd.close()
675    #clean up
676    os.remove(junk)     
677     
678    # Get the variables
679    x = fid.variables['x'][:]
680    y = fid.variables['y'][:]
681    volumes = fid.variables['volumes'][:] 
682    time = fid.variables['time'][:]
683
684    keys = fid.variables.keys()
685    keys.remove("x")
686    keys.remove("y")
687    keys.remove("volumes")
688    keys.remove("time")
689     #Turn NetCDF objects into Numeric arrays
690    quantities = {}
691    for name in keys:
692        quantities[name] = fid.variables[name][:]
693           
694    fid.close()
695    return x, y, volumes, time, quantities
696
697#-------------------------------------------------------------
698if __name__ == "__main__":
699    a = [0.0, 0.0]
700    b = [0.0, 2.0]
701    c = [2.0,0.0]
702    points = [a, b, c]
703    vertices = [ [1,0,2] ]   #bac
704   
705    data = [ [2.0/3, 2.0/3] ] #Use centroid as one data point
706   
707    interp = Interpolate(points, vertices) #, data)
708    A  = interp._build_interpolation_matrix_A(data, verbose=True)
709    A = A.todense()
710    print "A",A
711    assert allclose(A, [[1./3, 1./3, 1./3]])
712    print "finished"
Note: See TracBrowser for help on using the repository browser.