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

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

Got rid of output files

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