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

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

Comments

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
31from 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        #FIXME (Ole):  We should use CSR here since matrix-matrix mult is OK.
230        self.A = Sparse(n,m)
231        self.AtA = Sparse(m,m)
232
233        #Build quad tree of vertices (FIXME: Is this the right spot for that?)
234        root = build_quadtree(self.mesh)
235
236        #Compute matrix elements
237        for i in range(n):
238            #For each data_coordinate point
239
240            #print 'Doing %d of %d' %(i, n)
241
242            x = point_coordinates[i]
243
244            #Find vertices near x
245            candidate_vertices = root.search(x[0], x[1])
246
247            is_more_elements = True
248            if candidate_vertices == []:
249                # The point isn't even within the root cell!
250                is_more_elements = False
251                element_found = False
252            else:
253                element_found, sigma0, sigma1, sigma2, k = \
254                    self.search_triangles_of_vertices(candidate_vertices, x)
255
256            while not element_found and is_more_elements: 
257                candidate_vertices = root.expand_search()
258                if candidate_vertices == []:
259                    # All the triangles have been searched.
260                    is_more_elements = False
261                else:
262                    element_found, sigma0, sigma1, sigma2, k = \
263                      self.search_triangles_of_vertices(candidate_vertices, x)
264                   
265               
266           
267            #Update interpolation matrix A if necessary     
268            if element_found is True:       
269                #Assign values to matrix A
270
271                j0 = self.mesh.triangles[k,0] #Global vertex id
272                #self.A[i, j0] = sigma0
273
274                j1 = self.mesh.triangles[k,1] #Global vertex id
275                #self.A[i, j1] = sigma1
276
277                j2 = self.mesh.triangles[k,2] #Global vertex id
278                #self.A[i, j2] = sigma2
279
280                sigmas = {j0:sigma0, j1:sigma1, j2:sigma2}
281                js     = [j0,j1,j2]
282
283                for j in js:
284                    self.A[i,j] = sigmas[j]
285                    for k in js:
286                        self.AtA[j,k] += sigmas[j]*sigmas[k]
287            else:
288                pass
289                #Ok if there is no triangle for datapoint
290                #(as in brute force version)
291                #raise 'Could not find triangle for point', x
292
293
294    def search_triangles_of_vertices(self, candidate_vertices, x):
295            #Find triangle containing x:
296            element_found = False
297
298            # This will be returned if element_found = False
299            sigma2 = -10.0
300            sigma0 = -10.0
301            sigma1 = -10.0
302
303            #For all vertices in same cell as point x
304            for v in candidate_vertices:
305           
306                #for each triangle id (k) which has v as a vertex
307                for k, _ in self.mesh.vertexlist[v]:
308                   
309                    #Get the three vertex_points of candidate triangle
310                    xi0 = self.mesh.get_vertex_coordinate(k, 0)
311                    xi1 = self.mesh.get_vertex_coordinate(k, 1)
312                    xi2 = self.mesh.get_vertex_coordinate(k, 2)     
313
314                    #print "PDSG - k", k
315                    #print "PDSG - xi0", xi0
316                    #print "PDSG - xi1", xi1
317                    #print "PDSG - xi2", xi2
318                    #print "PDSG element %i verts((%f, %f),(%f, %f),(%f, %f))" \
319                    #      % (k, xi0[0], xi0[1], xi1[0], xi1[1], xi2[0], xi2[1])
320                   
321                    #Get the three normals
322                    n0 = self.mesh.get_normal(k, 0)
323                    n1 = self.mesh.get_normal(k, 1)
324                    n2 = self.mesh.get_normal(k, 2)               
325
326                   
327
328                    #Compute interpolation
329                    sigma2 = dot((x-xi0), n2)/dot((xi2-xi0), n2)
330                    sigma0 = dot((x-xi1), n0)/dot((xi0-xi1), n0)
331                    sigma1 = dot((x-xi2), n1)/dot((xi1-xi2), n1)
332
333                    #print "PDSG - sigma0", sigma0
334                    #print "PDSG - sigma1", sigma1
335                    #print "PDSG - sigma2", sigma2
336                   
337                    #FIXME: Maybe move out to test or something
338                    epsilon = 1.0e-6
339                    assert abs(sigma0 + sigma1 + sigma2 - 1.0) < epsilon
340                   
341                    #Check that this triangle contains the data point
342                   
343                    #Sigmas can get negative within
344                    #machine precision on some machines (e.g nautilus)
345                    #Hence the small eps                   
346                    eps = 1.0e-15
347                    if sigma0 >= -eps and sigma1 >= -eps and sigma2 >= -eps:
348                        element_found = True
349                        break
350
351                if element_found is True:
352                    #Don't look for any other triangle
353                    break
354            return element_found, sigma0, sigma1, sigma2, k     
355                   
356
357       
358    def build_interpolation_matrix_A_brute(self, point_coordinates):
359        """Build n x m interpolation matrix, where
360        n is the number of data points and
361        m is the number of basis functions phi_k (one per vertex)
362
363        This is the brute force which is too slow for large problems,
364        but could be used for testing
365        """
366
367
368       
369        #Convert input to Numeric arrays
370        point_coordinates = array(point_coordinates).astype(Float)
371       
372        #Build n x m interpolation matrix       
373        m = self.mesh.coordinates.shape[0] #Nbr of basis functions (1/vertex)
374        n = point_coordinates.shape[0]     #Nbr of data points
375       
376        self.A = Sparse(n,m)
377        self.AtA = Sparse(m,m)
378
379        #Compute matrix elements
380        for i in range(n):
381            #For each data_coordinate point
382
383            x = point_coordinates[i]
384            element_found = False
385            k = 0
386            while not element_found and k < len(self.mesh):
387                #For each triangle (brute force)
388                #FIXME: Real algorithm should only visit relevant triangles
389
390                #Get the three vertex_points
391                xi0 = self.mesh.get_vertex_coordinate(k, 0)
392                xi1 = self.mesh.get_vertex_coordinate(k, 1)
393                xi2 = self.mesh.get_vertex_coordinate(k, 2)                 
394
395                #Get the three normals
396                n0 = self.mesh.get_normal(k, 0)
397                n1 = self.mesh.get_normal(k, 1)
398                n2 = self.mesh.get_normal(k, 2)               
399
400                #Compute interpolation
401                sigma2 = dot((x-xi0), n2)/dot((xi2-xi0), n2)
402                sigma0 = dot((x-xi1), n0)/dot((xi0-xi1), n0)
403                sigma1 = dot((x-xi2), n1)/dot((xi1-xi2), n1)
404
405                #FIXME: Maybe move out to test or something
406                epsilon = 1.0e-6
407                assert abs(sigma0 + sigma1 + sigma2 - 1.0) < epsilon
408
409                #Check that this triangle contains data point
410                if sigma0 >= 0 and sigma1 >= 0 and sigma2 >= 0:
411                    element_found = True
412                    #Assign values to matrix A
413
414                    j0 = self.mesh.triangles[k,0] #Global vertex id
415                    #self.A[i, j0] = sigma0
416
417                    j1 = self.mesh.triangles[k,1] #Global vertex id
418                    #self.A[i, j1] = sigma1
419
420                    j2 = self.mesh.triangles[k,2] #Global vertex id
421                    #self.A[i, j2] = sigma2
422
423                    sigmas = {j0:sigma0, j1:sigma1, j2:sigma2}
424                    js     = [j0,j1,j2]
425
426                    for j in js:
427                        self.A[i,j] = sigmas[j]
428                        for k in js:
429                            self.AtA[j,k] += sigmas[j]*sigmas[k]
430                k = k+1
431       
432
433       
434    def get_A(self):
435        return self.A.todense() 
436
437    def get_B(self):
438        return self.B.todense()
439   
440    def get_D(self):
441        return self.D.todense()
442   
443        #FIXME: Remember to re-introduce the 1/n factor in the
444        #interpolation term
445       
446    def build_smoothing_matrix_D(self):
447        """Build m x m smoothing matrix, where
448        m is the number of basis functions phi_k (one per vertex)
449
450        The smoothing matrix is defined as
451
452        D = D1 + D2
453
454        where
455
456        [D1]_{k,l} = \int_\Omega
457           \frac{\partial \phi_k}{\partial x}
458           \frac{\partial \phi_l}{\partial x}\,
459           dx dy
460
461        [D2]_{k,l} = \int_\Omega
462           \frac{\partial \phi_k}{\partial y}
463           \frac{\partial \phi_l}{\partial y}\,
464           dx dy
465
466
467        The derivatives \frac{\partial \phi_k}{\partial x},
468        \frac{\partial \phi_k}{\partial x} for a particular triangle
469        are obtained by computing the gradient a_k, b_k for basis function k
470        """
471
472        #FIXME: algorithm might be optimised by computing local 9x9
473        #"element stiffness matrices:
474
475        m = self.mesh.coordinates.shape[0] #Nbr of basis functions (1/vertex)
476
477        self.D = Sparse(m,m)
478
479        #For each triangle compute contributions to D = D1+D2       
480        for i in range(len(self.mesh)):
481
482            #Get area
483            area = self.mesh.areas[i]
484
485            #Get global vertex indices
486            v0 = self.mesh.triangles[i,0]
487            v1 = self.mesh.triangles[i,1]
488            v2 = self.mesh.triangles[i,2]
489           
490            #Get the three vertex_points
491            xi0 = self.mesh.get_vertex_coordinate(i, 0)
492            xi1 = self.mesh.get_vertex_coordinate(i, 1)
493            xi2 = self.mesh.get_vertex_coordinate(i, 2)                 
494
495            #Compute gradients for each vertex
496            a0, b0 = gradient(xi0[0], xi0[1], xi1[0], xi1[1], xi2[0], xi2[1],
497                              1, 0, 0)
498
499            a1, b1 = gradient(xi0[0], xi0[1], xi1[0], xi1[1], xi2[0], xi2[1],
500                              0, 1, 0)
501
502            a2, b2 = gradient(xi0[0], xi0[1], xi1[0], xi1[1], xi2[0], xi2[1],
503                              0, 0, 1)           
504
505            #Compute diagonal contributions
506            self.D[v0,v0] += (a0*a0 + b0*b0)*area
507            self.D[v1,v1] += (a1*a1 + b1*b1)*area
508            self.D[v2,v2] += (a2*a2 + b2*b2)*area           
509
510            #Compute contributions for basis functions sharing edges
511            e01 = (a0*a1 + b0*b1)*area
512            self.D[v0,v1] += e01
513            self.D[v1,v0] += e01
514
515            e12 = (a1*a2 + b1*b2)*area
516            self.D[v1,v2] += e12
517            self.D[v2,v1] += e12
518
519            e20 = (a2*a0 + b2*b0)*area
520            self.D[v2,v0] += e20
521            self.D[v0,v2] += e20             
522
523           
524    def fit(self, z):
525        """Fit a smooth surface to given 1d array of data points z.
526
527        The smooth surface is computed at each vertex in the underlying
528        mesh using the formula given in the module doc string.
529
530        Pre Condition:
531          self.A, self.At and self.B have been initialised
532         
533        Inputs:
534          z: Single 1d vector or array of data at the point_coordinates.
535        """
536
537        #Convert input to Numeric arrays
538        z = array(z).astype(Float)
539
540
541        if len(z.shape) > 1 :
542            raise VectorShapeError, 'Can only deal with 1d data vector'
543       
544        #Compute right hand side based on data
545        Atz = self.A.trans_mult(z)
546
547       
548        #Check sanity
549        n, m = self.A.shape
550        if n<m and self.alpha == 0.0:
551            msg = 'ERROR (least_squares): Too few data points\n'
552            msg += 'There only %d data points. Need at least %d\n' %(n,m)
553            msg += 'Alternatively, increase smoothing parameter alpha' 
554            raise msg
555
556
557
558        return conjugate_gradient(self.B, Atz, Atz,imax=2*len(Atz) )
559        #FIXME: Should we store the result here for later use? (ON)       
560
561           
562    def fit_points(self, z):
563        """Like fit, but more robust when each point has two or more attributes
564        FIXME (Ole): The name fit_points doesn't carry any meaning
565        for me. How about something like fit_multiple or fit_columns?
566        """
567       
568        try:
569            return self.fit(z)
570        except VectorShapeError, e:
571            # broadcasting is not supported.
572
573            #Convert input to Numeric arrays
574            z = array(z).astype(Float)
575           
576            #Build n x m interpolation matrix       
577            m = self.mesh.coordinates.shape[0] #Number of vertices
578            n = z.shape[1]               #Number of data points         
579
580            f = zeros((m,n), Float) #Resulting columns
581           
582            for i in range(z.shape[1]):
583                f[:,i] = self.fit(z[:,i])
584               
585            return f
586           
587       
588    def interpolate(self, f):
589        """Evaluate smooth surface f at data points implied in self.A.
590
591        The mesh values representing a smooth surface are
592        assumed to be specified in f. This argument could,
593        for example have been obtained from the method self.fit()
594       
595        Pre Condition:
596          self.A has been initialised
597       
598        Inputs:
599          f: Vector or array of data at the mesh vertices.
600          If f is an array, interpolation will be done for each column as
601          per underlying matrix-matrix multiplication
602        """
603
604        return self.A * f
605       
606           
607#-------------------------------------------------------------
608if __name__ == "__main__":
609    """
610    Load in a mesh and data points with attributes.
611    Fit the attributes to the mesh.
612    Save a new mesh file.
613    """
614    import os, sys
615    usage = "usage: %s mesh_input.tsh point.xya mesh_output.tsh alpha"\
616            %os.path.basename(sys.argv[0])
617
618    if len(sys.argv) < 4:
619        print usage
620    else:
621        mesh_file = sys.argv[1]
622        point_file = sys.argv[2]
623        mesh_output_file = sys.argv[3]
624        if len(sys.argv) > 4:
625            alpha = sys.argv[4]
626        else:
627            alpha = DEFAULT_ALPHA
628        fit_to_mesh_file(mesh_file, point_file, mesh_output_file, alpha)
629       
Note: See TracBrowser for help on using the repository browser.