source: inundation/ga/storm_surge/pyvolution/least_squares.py @ 641

Last change on this file since 641 was 641, checked in by ole, 20 years ago

Test of interpolation using different points from those used in the fit

File size: 21.2 KB
Line 
1"""Least squares smooting and interpolation.
2
3   Implements a penalised least-squares fit and associated interpolations.
4
5   The penalty term (or smoothing term) is controlled by the smoothing
6   parameter alpha.
7   With a value of alpha=0, the fit function will attempt
8   to interpolate as closely as possible in the least-squares sense.
9   With values alpha > 0, a certain amount of smoothing will be applied.
10   A positive alpha is essential in cases where there are too few
11   data points.
12   A negative alpha is not allowed.
13   A typical value of alpha is 1.0e-6
14         
15
16   Ole Nielsen, Stephen Roberts, Duncan Gray, Christopher Zoppou
17   Geoscience Australia, 2004.   
18"""
19
20
21#FIXME (Ole): Currently datapoints outside the triangular mesh are ignored.
22#             Is there a clean way of including them?
23
24
25
26import exceptions
27class ShapeError(exceptions.Exception): pass
28
29from general_mesh import General_mesh
30from Numeric import zeros, array, Float, Int, dot, transpose
31##from LinearAlgebra import solve_linear_equations
32from sparse import Sparse, Sparse_CSR
33from cg_solve import conjugate_gradient, VectorShapeError
34
35try:
36    from util import gradient
37except ImportError, e: 
38    #FIXME reduce the dependency of modules in pyvolution
39    # Have util in a dir, working like load_mesh, and get rid of this
40    def gradient(x0, y0, x1, y1, x2, y2, q0, q1, q2):
41        """
42        """
43   
44        det = (y2-y0)*(x1-x0) - (y1-y0)*(x2-x0)           
45        a = (y2-y0)*(q1-q0) - (y1-y0)*(q2-q0)
46        a /= det
47
48        b = (x1-x0)*(q2-q0) - (x2-x0)*(q1-q0)
49        b /= det           
50
51        return a, b
52
53
54DEFAULT_ALPHA = 0.001
55   
56def fit_to_mesh_file(mesh_file, point_file, mesh_output_file, alpha=DEFAULT_ALPHA):
57    """
58    Given a mesh file (tsh) and a point attribute file (xya), fit
59    point attributes to the mesh and write a mesh file with the
60    results.
61    """
62    from load_mesh.loadASCII import mesh_file_to_mesh_dictionary, \
63                 load_xya_file, export_trianglulation_file
64    # load in the .tsh file
65    mesh_dict = mesh_file_to_mesh_dictionary(mesh_file)
66    vertex_coordinates = mesh_dict['generatedpointlist']
67    triangles = mesh_dict['generatedtrianglelist']
68   
69    old_point_attributes = mesh_dict['generatedpointattributelist'] 
70    old_title_list = mesh_dict['generatedpointattributetitlelist']
71
72   
73    # load in the .xya file
74    try:
75        point_dict = load_xya_file(point_file)
76    except SyntaxError,e:
77        point_dict = load_xya_file(point_file,delimiter = ' ')
78    point_coordinates = point_dict['pointlist']
79    point_attributes = point_dict['pointattributelist']
80    title_string = point_dict['title']
81    title_list = title_string.split(',') #FIXME iffy! Hard coding title delimiter 
82    for i in range(len(title_list)):
83        title_list[i] = title_list[i].strip() 
84    #print "title_list stripped", title_list   
85    f = fit_to_mesh(vertex_coordinates,
86                    triangles,
87                    point_coordinates,
88                    point_attributes,
89                    alpha = alpha)
90   
91    # convert array to list of lists
92    new_point_attributes = f.tolist()
93
94    #FIXME have this overwrite attributes with the same title - DSG
95    #Put the newer attributes last
96    if old_title_list <> []:
97        old_title_list.extend(title_list)
98        #FIXME can this be done a faster way? - DSG
99        for i in range(len(old_point_attributes)):
100            old_point_attributes[i].extend(new_point_attributes[i])
101        mesh_dict['generatedpointattributelist'] = old_point_attributes
102        mesh_dict['generatedpointattributetitlelist'] = old_title_list
103    else:
104        mesh_dict['generatedpointattributelist'] = new_point_attributes
105        mesh_dict['generatedpointattributetitlelist'] = title_list
106   
107    export_trianglulation_file(mesh_output_file, mesh_dict)
108       
109
110def fit_to_mesh(vertex_coordinates,
111                triangles,
112                point_coordinates,
113                point_attributes,
114                alpha = DEFAULT_ALPHA):
115    """
116    Fit a smooth surface to a trianglulation,
117    given data points with attributes.
118
119         
120        Inputs:
121       
122          vertex_coordinates: List of coordinate pairs [xi, eta] of points
123          constituting mesh (or a an m x 2 Numeric array)
124       
125          triangles: List of 3-tuples (or a Numeric array) of
126          integers representing indices of all vertices in the mesh.
127
128          point_coordinates: List of coordinate pairs [x, y] of data points
129          (or an nx2 Numeric array)
130
131          alpha: Smoothing parameter.
132
133          point_attributes: Vector or array of data at the point_coordinates.
134    """
135    interp = Interpolation(vertex_coordinates,
136                           triangles,
137                           point_coordinates,
138                           alpha = alpha)
139   
140    vertex_attributes = interp.fit_points(point_attributes)
141    return vertex_attributes
142
143
144class Interpolation:
145
146    def __init__(self,
147                 vertex_coordinates,
148                 triangles,
149                 point_coordinates = None,
150                 alpha = DEFAULT_ALPHA):
151
152       
153        """ Build interpolation matrix mapping from
154        function values at vertices to function values at data points
155
156        Inputs:
157       
158          vertex_coordinates: List of coordinate pairs [xi, eta] of
159          points constituting mesh (or a an m x 2 Numeric array)
160       
161          triangles: List of 3-tuples (or a Numeric array) of
162          integers representing indices of all vertices in the mesh.
163
164          point_coordinates: List of coordinate pairs [x, y] of
165          data points (or an nx2 Numeric array)
166          If point_coordinates is absent, only smoothing matrix will
167          be built
168
169          alpha: Smoothing parameter
170         
171        """
172
173
174        #Convert input to Numeric arrays
175        vertex_coordinates = array(vertex_coordinates).astype(Float)
176        triangles = array(triangles).astype(Int)               
177       
178        #Build underlying mesh
179        self.mesh = General_mesh(vertex_coordinates, triangles)
180
181        #Smoothing parameter
182        self.alpha = alpha
183
184        #Build coefficient matrices
185        self.build_coefficient_matrix_B(point_coordinates)   
186
187    def set_point_coordinates(self, point_coordinates):
188        """
189        A public interface to setting the point co-ordinates.
190        """
191        self.build_coefficient_matrix_B(point_coordinates)
192       
193    def build_coefficient_matrix_B(self, point_coordinates=None):
194        """Build final coefficient matrix"""
195       
196
197        if self.alpha <> 0:
198            self.build_smoothing_matrix_D()
199       
200        if point_coordinates:
201
202            self.build_interpolation_matrix_A(point_coordinates)
203
204            if self.alpha <> 0:
205                self.B = self.AtA + self.alpha*self.D
206            else:
207                self.B = self.AtA
208
209            #Convert self.B matrix to CSR format for faster matrix vector
210            self.B = Sparse_CSR(self.B)
211       
212    def build_interpolation_matrix_A(self, point_coordinates):
213        """Build n x m interpolation matrix, where
214        n is the number of data points and
215        m is the number of basis functions phi_k (one per vertex)
216
217        This algorithm uses a quad tree data structure for fast binning of data points
218        """
219
220        from quad import build_quadtree
221       
222        #Convert input to Numeric arrays
223        point_coordinates = array(point_coordinates).astype(Float)
224       
225        #Build n x m interpolation matrix       
226        m = self.mesh.coordinates.shape[0] #Nbr of basis functions (1/vertex)
227        n = point_coordinates.shape[0]     #Nbr of data points
228
229       
230        #FIXME (Ole): We should use CSR here since mat-mat mult is now OK.
231        self.A = Sparse(n,m)
232        self.AtA = Sparse(m,m)
233
234        #Build quad tree of vertices (FIXME: Is this the right spot for that?)
235        root = build_quadtree(self.mesh)
236
237        #Compute matrix elements
238        for i in range(n):
239            #For each data_coordinate point
240
241            #print 'Doing %d of %d' %(i, n)
242
243            x = point_coordinates[i]
244
245            #Find vertices near x
246            candidate_vertices = root.search(x[0], x[1])
247
248            is_more_elements = True
249            if candidate_vertices == []:
250                # The point isn't even within the root cell!
251                is_more_elements = False
252                element_found = False
253            else:
254                element_found, sigma0, sigma1, sigma2, k = \
255                    self.search_triangles_of_vertices(candidate_vertices, x)
256
257            while not element_found and is_more_elements: 
258                candidate_vertices = root.expand_search()
259                if candidate_vertices == []:
260                    # All the triangles have been searched.
261                    is_more_elements = False
262                else:
263                    element_found, sigma0, sigma1, sigma2, k = \
264                      self.search_triangles_of_vertices(candidate_vertices, x)
265                   
266               
267           
268            #Update interpolation matrix A if necessary     
269            if element_found is True:       
270                #Assign values to matrix A
271
272                j0 = self.mesh.triangles[k,0] #Global vertex id
273                #self.A[i, j0] = sigma0
274
275                j1 = self.mesh.triangles[k,1] #Global vertex id
276                #self.A[i, j1] = sigma1
277
278                j2 = self.mesh.triangles[k,2] #Global vertex id
279                #self.A[i, j2] = sigma2
280
281                sigmas = {j0:sigma0, j1:sigma1, j2:sigma2}
282                js     = [j0,j1,j2]
283
284                for j in js:
285                    self.A[i,j] = sigmas[j]
286                    for k in js:
287                        self.AtA[j,k] += sigmas[j]*sigmas[k]
288            else:
289                pass
290                #Ok if there is no triangle for datapoint
291                #(as in brute force version)
292                #raise 'Could not find triangle for point', x
293
294
295    def search_triangles_of_vertices(self, candidate_vertices, x):
296            #Find triangle containing x:
297            element_found = False
298
299            # This will be returned if element_found = False
300            sigma2 = -10.0
301            sigma0 = -10.0
302            sigma1 = -10.0
303
304            #For all vertices in same cell as point x
305            for v in candidate_vertices:
306           
307                #for each triangle id (k) which has v as a vertex
308                for k, _ in self.mesh.vertexlist[v]:
309                   
310                    #Get the three vertex_points of candidate triangle
311                    xi0 = self.mesh.get_vertex_coordinate(k, 0)
312                    xi1 = self.mesh.get_vertex_coordinate(k, 1)
313                    xi2 = self.mesh.get_vertex_coordinate(k, 2)     
314
315                    #print "PDSG - k", k
316                    #print "PDSG - xi0", xi0
317                    #print "PDSG - xi1", xi1
318                    #print "PDSG - xi2", xi2
319                    #print "PDSG element %i verts((%f, %f),(%f, %f),(%f, %f))" \
320                    #      % (k, xi0[0], xi0[1], xi1[0], xi1[1], xi2[0], xi2[1])
321                   
322                    #Get the three normals
323                    n0 = self.mesh.get_normal(k, 0)
324                    n1 = self.mesh.get_normal(k, 1)
325                    n2 = self.mesh.get_normal(k, 2)               
326
327                   
328
329                    #Compute interpolation
330                    sigma2 = dot((x-xi0), n2)/dot((xi2-xi0), n2)
331                    sigma0 = dot((x-xi1), n0)/dot((xi0-xi1), n0)
332                    sigma1 = dot((x-xi2), n1)/dot((xi1-xi2), n1)
333
334                    #print "PDSG - sigma0", sigma0
335                    #print "PDSG - sigma1", sigma1
336                    #print "PDSG - sigma2", sigma2
337                   
338                    #FIXME: Maybe move out to test or something
339                    epsilon = 1.0e-6
340                    assert abs(sigma0 + sigma1 + sigma2 - 1.0) < epsilon
341                   
342                    #Check that this triangle contains the data point
343                   
344                    #Sigmas can get negative within
345                    #machine precision on some machines (e.g nautilus)
346                    #Hence the small eps                   
347                    eps = 1.0e-15
348                    if sigma0 >= -eps and sigma1 >= -eps and sigma2 >= -eps:
349                        element_found = True
350                        break
351
352                if element_found is True:
353                    #Don't look for any other triangle
354                    break
355            return element_found, sigma0, sigma1, sigma2, k     
356                   
357
358       
359    def build_interpolation_matrix_A_brute(self, point_coordinates):
360        """Build n x m interpolation matrix, where
361        n is the number of data points and
362        m is the number of basis functions phi_k (one per vertex)
363
364        This is the brute force which is too slow for large problems,
365        but could be used for testing
366        """
367
368
369       
370        #Convert input to Numeric arrays
371        point_coordinates = array(point_coordinates).astype(Float)
372       
373        #Build n x m interpolation matrix       
374        m = self.mesh.coordinates.shape[0] #Nbr of basis functions (1/vertex)
375        n = point_coordinates.shape[0]     #Nbr of data points
376       
377        self.A = Sparse(n,m)
378        self.AtA = Sparse(m,m)
379
380        #Compute matrix elements
381        for i in range(n):
382            #For each data_coordinate point
383
384            x = point_coordinates[i]
385            element_found = False
386            k = 0
387            while not element_found and k < len(self.mesh):
388                #For each triangle (brute force)
389                #FIXME: Real algorithm should only visit relevant triangles
390
391                #Get the three vertex_points
392                xi0 = self.mesh.get_vertex_coordinate(k, 0)
393                xi1 = self.mesh.get_vertex_coordinate(k, 1)
394                xi2 = self.mesh.get_vertex_coordinate(k, 2)                 
395
396                #Get the three normals
397                n0 = self.mesh.get_normal(k, 0)
398                n1 = self.mesh.get_normal(k, 1)
399                n2 = self.mesh.get_normal(k, 2)               
400
401                #Compute interpolation
402                sigma2 = dot((x-xi0), n2)/dot((xi2-xi0), n2)
403                sigma0 = dot((x-xi1), n0)/dot((xi0-xi1), n0)
404                sigma1 = dot((x-xi2), n1)/dot((xi1-xi2), n1)
405
406                #FIXME: Maybe move out to test or something
407                epsilon = 1.0e-6
408                assert abs(sigma0 + sigma1 + sigma2 - 1.0) < epsilon
409
410                #Check that this triangle contains data point
411                if sigma0 >= 0 and sigma1 >= 0 and sigma2 >= 0:
412                    element_found = True
413                    #Assign values to matrix A
414
415                    j0 = self.mesh.triangles[k,0] #Global vertex id
416                    #self.A[i, j0] = sigma0
417
418                    j1 = self.mesh.triangles[k,1] #Global vertex id
419                    #self.A[i, j1] = sigma1
420
421                    j2 = self.mesh.triangles[k,2] #Global vertex id
422                    #self.A[i, j2] = sigma2
423
424                    sigmas = {j0:sigma0, j1:sigma1, j2:sigma2}
425                    js     = [j0,j1,j2]
426
427                    for j in js:
428                        self.A[i,j] = sigmas[j]
429                        for k in js:
430                            self.AtA[j,k] += sigmas[j]*sigmas[k]
431                k = k+1
432       
433
434       
435    def get_A(self):
436        return self.A.todense() 
437
438    def get_B(self):
439        return self.B.todense()
440   
441    def get_D(self):
442        return self.D.todense()
443   
444        #FIXME: Remember to re-introduce the 1/n factor in the
445        #interpolation term
446       
447    def build_smoothing_matrix_D(self):
448        """Build m x m smoothing matrix, where
449        m is the number of basis functions phi_k (one per vertex)
450
451        The smoothing matrix is defined as
452
453        D = D1 + D2
454
455        where
456
457        [D1]_{k,l} = \int_\Omega
458           \frac{\partial \phi_k}{\partial x}
459           \frac{\partial \phi_l}{\partial x}\,
460           dx dy
461
462        [D2]_{k,l} = \int_\Omega
463           \frac{\partial \phi_k}{\partial y}
464           \frac{\partial \phi_l}{\partial y}\,
465           dx dy
466
467
468        The derivatives \frac{\partial \phi_k}{\partial x},
469        \frac{\partial \phi_k}{\partial x} for a particular triangle
470        are obtained by computing the gradient a_k, b_k for basis function k
471        """
472
473        #FIXME: algorithm might be optimised by computing local 9x9
474        #"element stiffness matrices:
475
476        m = self.mesh.coordinates.shape[0] #Nbr of basis functions (1/vertex)
477
478        self.D = Sparse(m,m)
479
480        #For each triangle compute contributions to D = D1+D2       
481        for i in range(len(self.mesh)):
482
483            #Get area
484            area = self.mesh.areas[i]
485
486            #Get global vertex indices
487            v0 = self.mesh.triangles[i,0]
488            v1 = self.mesh.triangles[i,1]
489            v2 = self.mesh.triangles[i,2]
490           
491            #Get the three vertex_points
492            xi0 = self.mesh.get_vertex_coordinate(i, 0)
493            xi1 = self.mesh.get_vertex_coordinate(i, 1)
494            xi2 = self.mesh.get_vertex_coordinate(i, 2)                 
495
496            #Compute gradients for each vertex
497            a0, b0 = gradient(xi0[0], xi0[1], xi1[0], xi1[1], xi2[0], xi2[1],
498                              1, 0, 0)
499
500            a1, b1 = gradient(xi0[0], xi0[1], xi1[0], xi1[1], xi2[0], xi2[1],
501                              0, 1, 0)
502
503            a2, b2 = gradient(xi0[0], xi0[1], xi1[0], xi1[1], xi2[0], xi2[1],
504                              0, 0, 1)           
505
506            #Compute diagonal contributions
507            self.D[v0,v0] += (a0*a0 + b0*b0)*area
508            self.D[v1,v1] += (a1*a1 + b1*b1)*area
509            self.D[v2,v2] += (a2*a2 + b2*b2)*area           
510
511            #Compute contributions for basis functions sharing edges
512            e01 = (a0*a1 + b0*b1)*area
513            self.D[v0,v1] += e01
514            self.D[v1,v0] += e01
515
516            e12 = (a1*a2 + b1*b2)*area
517            self.D[v1,v2] += e12
518            self.D[v2,v1] += e12
519
520            e20 = (a2*a0 + b2*b0)*area
521            self.D[v2,v0] += e20
522            self.D[v0,v2] += e20             
523
524           
525    def fit(self, z):
526        """Fit a smooth surface to given 1d array of data points z.
527
528        The smooth surface is computed at each vertex in the underlying
529        mesh using the formula given in the module doc string.
530
531        Pre Condition:
532          self.A, self.At and self.B have been initialised
533         
534        Inputs:
535          z: Single 1d vector or array of data at the point_coordinates.
536        """
537
538        #Convert input to Numeric arrays
539        z = array(z).astype(Float)
540
541
542        if len(z.shape) > 1 :
543            raise VectorShapeError, 'Can only deal with 1d data vector'
544       
545        #Compute right hand side based on data
546        Atz = self.A.trans_mult(z)
547
548       
549        #Check sanity
550        n, m = self.A.shape
551        if n<m and self.alpha == 0.0:
552            msg = 'ERROR (least_squares): Too few data points\n'
553            msg += 'There only %d data points. Need at least %d\n' %(n,m)
554            msg += 'Alternatively, increase smoothing parameter alpha' 
555            raise msg
556
557
558
559        return conjugate_gradient(self.B, Atz, Atz,imax=2*len(Atz) )
560        #FIXME: Should we store the result here for later use? (ON)       
561
562           
563    def fit_points(self, z):
564        """Like fit, but more robust when each point has two or more attributes
565        FIXME (Ole): The name fit_points doesn't carry any meaning
566        for me. How about something like fit_multiple or fit_columns?
567        """
568       
569        try:
570            return self.fit(z)
571        except VectorShapeError, e:
572            # broadcasting is not supported.
573
574            #Convert input to Numeric arrays
575            z = array(z).astype(Float)
576           
577            #Build n x m interpolation matrix       
578            m = self.mesh.coordinates.shape[0] #Number of vertices
579            n = z.shape[1]               #Number of data points         
580
581            f = zeros((m,n), Float) #Resulting columns
582           
583            for i in range(z.shape[1]):
584                f[:,i] = self.fit(z[:,i])
585               
586            return f
587           
588       
589    def interpolate(self, f):
590        """Evaluate smooth surface f at data points implied in self.A.
591
592        The mesh values representing a smooth surface are
593        assumed to be specified in f. This argument could,
594        for example have been obtained from the method self.fit()
595       
596        Pre Condition:
597          self.A has been initialised
598       
599        Inputs:
600          f: Vector or array of data at the mesh vertices.
601          If f is an array, interpolation will be done for each column as
602          per underlying matrix-matrix multiplication
603        """
604
605        return self.A * f
606       
607           
608#-------------------------------------------------------------
609if __name__ == "__main__":
610    """
611    Load in a mesh and data points with attributes.
612    Fit the attributes to the mesh.
613    Save a new mesh file.
614    """
615    import os, sys
616    usage = "usage: %s mesh_input.tsh point.xya mesh_output.tsh alpha"\
617            %os.path.basename(sys.argv[0])
618
619    if len(sys.argv) < 4:
620        print usage
621    else:
622        mesh_file = sys.argv[1]
623        point_file = sys.argv[2]
624        mesh_output_file = sys.argv[3]
625        if len(sys.argv) > 4:
626            alpha = sys.argv[4]
627        else:
628            alpha = DEFAULT_ALPHA
629        fit_to_mesh_file(mesh_file, point_file, mesh_output_file, alpha)
630       
Note: See TracBrowser for help on using the repository browser.