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

Last change on this file since 433 was 428, checked in by duncan, 20 years ago

fixed bug in writing .tsh file

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