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

Last change on this file since 441 was 441, checked in by steve, 20 years ago

Dealing with 2d data arrays in fit and interpolate

File size: 16.7 KB
Line 
1"""Least squares smooting and interpolation.
2
3   Implements a penalised least-squares fit and associated interpolations.
4
5   The panalty 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#FIXME: Current implementation uses full matrices and a general solver.
21#Later on we may consider using sparse techniques
22
23import exceptions
24class ShapeError(exceptions.Exception): pass
25
26from general_mesh import General_Mesh
27from Numeric import zeros, array, Float, Int, dot, transpose
28from LinearAlgebra import solve_linear_equations
29from scipy import sparse
30from cg_solve import conjugate_gradient, VectorShapeError
31
32try:
33    from util import gradient
34
35except ImportError, e: 
36    #FIXME reduce the dependency of modules in pyvolution
37    # Have util in a dir, working like load_mesh, and get rid of this
38    def gradient(x0, y0, x1, y1, x2, y2, q0, q1, q2):
39        """
40        """
41   
42        det = (y2-y0)*(x1-x0) - (y1-y0)*(x2-x0)           
43        a = (y2-y0)*(q1-q0) - (y1-y0)*(q2-q0)
44        a /= det
45
46        b = (x1-x0)*(q2-q0) - (x2-x0)*(q1-q0)
47        b /= det           
48
49        return a, b
50
51ALPHA_INITIAL = 0.001
52
53def fit_to_mesh_file(mesh_file, point_file, mesh_output_file, alpha= ALPHA_INITIAL):
54    """
55    Given a mesh file (tsh) and a point attribute file (xya), fit
56    point attributes to the mesh and write a mesh file with the
57    results.
58    """
59    from load_mesh.loadASCII import mesh_file_to_mesh_dictionary, \
60                 load_xya_file, export_trianglulation_file
61    # load in the .tsh file
62    mesh_dict = mesh_file_to_mesh_dictionary(mesh_file)
63    vertex_coordinates = mesh_dict['generatedpointlist']
64    triangles = mesh_dict['generatedtrianglelist']
65   
66    old_point_attributes = mesh_dict['generatedpointattributelist'] 
67    old_title_list = mesh_dict['generatedpointattributetitlelist']
68
69   
70    # load in the .xya file
71   
72    point_dict = load_xya_file(point_file)
73    point_coordinates = point_dict['pointlist']
74    point_attributes = point_dict['pointattributelist']
75    title_string = point_dict['title']
76    title_list = title_string.split(',') #iffy! Hard coding title delimiter 
77    for i in range(len(title_list)):
78        title_list[i] = title_list[i].strip() 
79    #print "title_list stripped", title_list   
80    f = fit_to_mesh(vertex_coordinates,
81                    triangles,
82                    point_coordinates,
83                    point_attributes,
84                    alpha = alpha)
85   
86    # convert array to list of lists
87    new_point_attributes = f.tolist()
88
89    #FIXME have this overwrite attributes with the same title - DSG
90    #Put the newer attributes last
91    if old_title_list <> []:
92        old_title_list.extend(title_list)
93        #FIXME can this be done a faster way? - DSG
94        for i in range(len(old_point_attributes)):
95            old_point_attributes[i].extend(new_point_attributes[i])
96        mesh_dict['generatedpointattributelist'] = old_point_attributes
97        mesh_dict['generatedpointattributetitlelist'] = old_title_list
98    else:
99        mesh_dict['generatedpointattributelist'] = new_point_attributes
100        mesh_dict['generatedpointattributetitlelist'] = title_list
101   
102    export_trianglulation_file(mesh_output_file, mesh_dict)
103       
104
105def fit_to_mesh(vertex_coordinates,
106                              triangles,
107                              point_coordinates,
108                              point_attributes,
109                              alpha = ALPHA_INITIAL):
110    """
111    Fit a smooth surface to a trianglulation,
112    given data points with attributes.
113
114         
115        Inputs:
116       
117          vertex_coordinates: List of coordinate pairs [xi, eta] of points
118          constituting mesh (or a an m x 2 Numeric array)
119       
120          triangles: List of 3-tuples (or a Numeric array) of
121          integers representing indices of all vertices in the mesh.
122
123          point_coordinates: List of coordinate pairs [x, y] of data points
124          (or an nx2 Numeric array)
125
126          alpha: Smoothing parameter.
127
128          point_attributes: Vector or array of data at the point_coordinates.
129    """
130    interp = Interpolation(vertex_coordinates,
131                           triangles,
132                           point_coordinates,
133                           alpha = alpha)
134   
135    vertex_attributes = interp.fit_points(point_attributes)
136    return vertex_attributes
137
138
139class Interpolation:
140
141    def __init__(self,
142                 vertex_coordinates,
143                 triangles,
144                 point_coordinates = None,
145                 alpha = ALPHA_INITIAL):
146
147       
148        """ Build interpolation matrix mapping from
149        function values at vertices to function values at data points
150
151        Inputs:
152       
153          vertex_coordinates: List of coordinate pairs [xi, eta] of points
154          constituting mesh (or a an m x 2 Numeric array)
155       
156          triangles: List of 3-tuples (or a Numeric array) of
157          integers representing indices of all vertices in the mesh.
158
159          point_coordinates: List of coordinate pairs [x, y] of data points
160          (or an nx2 Numeric array)
161
162          alpha: Smoothing parameter
163         
164        """
165
166
167        #Convert input to Numeric arrays
168        vertex_coordinates = array(vertex_coordinates).astype(Float)
169        triangles = array(triangles).astype(Int)               
170       
171        #Build underlying mesh
172        self.mesh = General_Mesh(vertex_coordinates, triangles)
173
174        #Smoothing parameter
175        self.alpha = alpha
176
177        #Build coefficient matrices
178        self.build_coefficient_matrix_B(point_coordinates)   
179
180
181       
182    def build_coefficient_matrix_B(self, point_coordinates=None):
183        """Build final coefficient matrix"""
184       
185
186        if self.alpha <> 0:
187            self.build_smoothing_matrix_D()
188       
189        if point_coordinates:
190
191            self.build_interpolation_matrix_A(point_coordinates)
192
193            if self.alpha <> 0:
194                self.B = self.AtA + self.alpha*self.D
195            else:
196                self.B = self.AtA
197
198       
199    def build_interpolation_matrix_A(self, point_coordinates):
200        """Build n x m interpolation matrix, where
201        n is the number of data points and
202        m is the number of basis functions phi_k (one per vertex)"""
203       
204        #Convert input to Numeric arrays
205        point_coordinates = array(point_coordinates).astype(Float)
206       
207        #Build n x m interpolation matrix       
208        m = self.mesh.coordinates.shape[0] #Nbr of basis functions (1/vertex)
209        n = point_coordinates.shape[0]     #Nbr of data points
210       
211        #self.A = zeros((n,m), Float)
212        self.A = sparse.dok_matrix()
213        self.AtA = sparse.dok_matrix()
214
215        #Compute matrix elements
216        for i in range(n):
217            #For each data_coordinate point
218
219            x = point_coordinates[i]
220            for k in range(len(self.mesh)):
221                #For each triangle (brute force)
222                #FIXME: Real algorithm should only visit relevant triangles
223
224                #Get the three vertex_points
225                xi0 = self.mesh.get_vertex_coordinate(k, 0)
226                xi1 = self.mesh.get_vertex_coordinate(k, 1)
227                xi2 = self.mesh.get_vertex_coordinate(k, 2)                 
228
229                #Get the three normals
230                n0 = self.mesh.get_normal(k, 0)
231                n1 = self.mesh.get_normal(k, 1)
232                n2 = self.mesh.get_normal(k, 2)               
233
234                #Compute interpolation
235                sigma2 = dot((x-xi0), n2)/dot((xi2-xi0), n2)
236                sigma0 = dot((x-xi1), n0)/dot((xi0-xi1), n0)
237                sigma1 = dot((x-xi2), n1)/dot((xi1-xi2), n1)
238
239                #FIXME: Maybe move out to test or something
240                epsilon = 1.0e-6
241                assert abs(sigma0 + sigma1 + sigma2 - 1.0) < epsilon
242
243                #Check that this triangle contains data point
244                if sigma0 >= 0 and sigma1 >= 0 and sigma2 >= 0:
245
246                    #Assign values to matrix A
247
248
249                    j0 = self.mesh.triangles[k,0] #Global vertex id
250                    #self.A[i, j0] = sigma0
251
252                    j1 = self.mesh.triangles[k,1] #Global vertex id
253                    #self.A[i, j1] = sigma1
254
255                    j2 = self.mesh.triangles[k,2] #Global vertex id
256                    #self.A[i, j2] = sigma2
257
258                    sigmas = {j0:sigma0, j1:sigma1, j2:sigma2}
259                    js     = [j0,j1,j2]
260
261                    for j in js:
262                        self.A[i,j] = sigmas[j]
263                        for k in js:
264                            self.AtA[j,k] += sigmas[j]*sigmas[k]
265
266       
267        self.A   = (self.A).tocsc()
268        self.AtA = (self.AtA).tocsc()
269        self.At  =  self.A.transp()
270
271    def get_A(self):
272        return self.A.todense() 
273
274    def get_B(self):
275        return self.B.todense()
276   
277    def get_D(self):
278        return self.D.todense()
279   
280        #FIXME: Remember to re-introduce the 1/n factor in the
281        #interpolation term
282       
283    def build_smoothing_matrix_D(self):
284        """Build m x m smoothing matrix, where
285        m is the number of basis functions phi_k (one per vertex)
286
287        The smoothing matrix is defined as
288
289        D = D1 + D2
290
291        where
292
293        [D1]_{k,l} = \int_\Omega
294           \frac{\partial \phi_k}{\partial x}
295           \frac{\partial \phi_l}{\partial x}\,
296           dx dy
297
298        [D2]_{k,l} = \int_\Omega
299           \frac{\partial \phi_k}{\partial y}
300           \frac{\partial \phi_l}{\partial y}\,
301           dx dy
302
303
304        The derivatives \frac{\partial \phi_k}{\partial x},
305        \frac{\partial \phi_k}{\partial x} for a particular triangle
306        are obtained by computing the gradient a_k, b_k for basis function k
307        """
308
309        #FIXME: algorithm might be optimised by computing local 9x9
310        #"element stiffness matrices:
311
312       
313        m = self.mesh.coordinates.shape[0] #Nbr of basis functions (1/vertex)
314
315        #self.D = zeros((m,m), Float)
316        self.D = sparse.dok_matrix()
317
318        #For each triangle compute contributions to D = D1+D2       
319        for i in range(len(self.mesh)):
320
321            #Get area
322            area = self.mesh.areas[i]
323
324            #Get global vertex indices
325            v0 = self.mesh.triangles[i,0]
326            v1 = self.mesh.triangles[i,1]
327            v2 = self.mesh.triangles[i,2]
328           
329            #Get the three vertex_points
330            xi0 = self.mesh.get_vertex_coordinate(i, 0)
331            xi1 = self.mesh.get_vertex_coordinate(i, 1)
332            xi2 = self.mesh.get_vertex_coordinate(i, 2)                 
333
334            #Compute gradients for each vertex
335            a0, b0 = gradient(xi0[0], xi0[1], xi1[0], xi1[1], xi2[0], xi2[1],
336                              1, 0, 0)
337
338            a1, b1 = gradient(xi0[0], xi0[1], xi1[0], xi1[1], xi2[0], xi2[1],
339                              0, 1, 0)
340
341            a2, b2 = gradient(xi0[0], xi0[1], xi1[0], xi1[1], xi2[0], xi2[1],
342                              0, 0, 1)           
343
344            #Compute diagonal contributions
345            self.D[v0,v0] += (a0*a0 + b0*b0)*area
346            self.D[v1,v1] += (a1*a1 + b1*b1)*area
347            self.D[v2,v2] += (a2*a2 + b2*b2)*area           
348
349            #Compute contributions for basis functions sharing edges
350            e01 = (a0*a1 + b0*b1)*area
351            self.D[v0,v1] += e01
352            self.D[v1,v0] += e01
353
354            e12 = (a1*a2 + b1*b2)*area
355            self.D[v1,v2] += e12
356            self.D[v2,v1] += e12
357
358            e20 = (a2*a0 + b2*b0)*area
359            self.D[v2,v0] += e20
360            self.D[v0,v2] += e20             
361
362        self.D = (self.D).tocsc()
363           
364    def fit(self, z):
365        """Fit a smooth surface to given 1d array of data points z.
366
367        The smooth surface is computed at each vertex in the underlying
368        mesh using the formula given in the module doc string.
369
370        Pre Condition:
371          self.A, self.At and self.B have been initialised
372         
373        Inputs:
374          z: Single 1d vector or array of data at the point_coordinates.
375        """
376
377        #Convert input to Numeric arrays
378        z = array(z).astype(Float)
379
380
381        if len(z.shape) > 1 :
382            raise VectorShapeError, 'Can only deal with 1d data vector'
383       
384        #Compute right hand side based on data
385        Atz = self.At * z
386
387        #Check sanity
388        n, m = self.A.shape
389        if n<m and self.alpha == 0.0:
390            msg = 'ERROR (least_squares): Too few data points\n'
391            msg += 'There only %d data points. Need at least %d\n' %(n,m)
392            msg += 'Alternatively, increase smoothing parameter alpha' 
393            raise msg
394
395
396        #Solve and return
397        #return solve_linear_equations(self.get_B(), Atz)
398
399        # caused errors
400        #return sparse.solve(self.B,Atz)
401
402        return conjugate_gradient(self.B, Atz, Atz)
403        #FIXME: Should we store the result here for later use? (ON)       
404   
405    def fit_points(self, z):
406        """
407        Like fit, but more robust when each point has two or more attributes
408        """
409        try:
410            return self.fit(z)
411        except VectorShapeError, e:
412            # broadcasting is not supported.
413
414            #Convert input to Numeric arrays
415            z = array(z).astype(Float)
416           
417            #Build n x m interpolation matrix       
418            m = self.mesh.coordinates.shape[0] #Number of vertices
419            n = z.shape[1]               #Number of data points         
420
421            f = zeros((m,n), Float)
422            #f = sparse.dok_matrix() # even though it wont be sparse?
423           
424            for i in range(z.shape[1]):
425                f[:,i] = self.fit(z[:,i])
426            return f
427           
428       
429    def interpolate(self, f):
430        """Compute predicted values at data points implied in self.A.
431
432        The mesh values representing a smooth surface are
433        assumed to be specified in f. This argument could,
434        for example have been obtained from the method self.fit()
435       
436        Pre Condition:
437          self.A has been initialised
438       
439        Inputs:
440          f: Vector or array of data at the mesh vertices.
441          If f is an array, interpolation will be done for each column
442        """
443
444        try:
445            return self.A * f
446        except ValueError:
447            # We are here, so it probalby means that f is 2 dimensional
448            # and so will do each column separately due to problems in
449            # sparse matrix, 2d array multiplication
450           
451            (N , M) = self.A.shape
452            f = array(f).astype(Float)
453            (m , n) = f.shape
454            if m != M :
455                raise VectorShapeError, 'Mismatch between A and f dimensions'
456           
457            y = zeros( (N,n), Float)
458
459            for i in range(n):
460                y[:,i] = self.A * f[:,i]
461
462            return y
463     
464    #FIXME: We will need a method 'evaluate(self):' that will interpolate
465    #a computed surface living on the mesh onto a collection of
466    #arbitrary data points
467    #
468    #Precondition: self.fit(z) has stored its result in self.f.
469    #
470    #Input: data_points
471    #Algorithm:
472    #  1 Build a new temporary A matrix based on mesh and new data points
473    #  2 Apply it to self.f (return A*self.f)
474    #
475    # ON
476
477
478
479#-------------------------------------------------------------
480if __name__ == "__main__":
481    """
482    Load in a mesh and data points with attributes.
483    Fit the attributes to the mesh.
484    Save a new mesh file.
485    """
486    import os, sys
487    usage = "usage: %s mesh_input.tsh point.xya  mesh_output.tsh alpha" %         os.path.basename(sys.argv[0])
488
489    if len(sys.argv) < 4:
490        print usage
491    else:
492        mesh_file = sys.argv[1]
493        point_file = sys.argv[2]
494        mesh_output_file = sys.argv[3]
495        if len(sys.argv) > 4:
496            alpha = sys.argv[4]
497        else:
498            alpha = ALPHA_INITIAL
499        fit_to_mesh_file(mesh_file, point_file, mesh_output_file, alpha)
500       
Note: See TracBrowser for help on using the repository browser.