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

Last change on this file since 596 was 595, checked in by duncan, 20 years ago

loads space delimited .xya files as well

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