source: branches/numpy/anuga/abstract_2d_finite_volumes/general_mesh.py @ 6833

Last change on this file since 6833 was 6833, checked in by rwilson, 16 years ago

Changes getting Patong validation to run.

File size: 20.3 KB
Line 
1import copy
2import numpy as num
3
4from anuga.coordinate_transforms.geo_reference import Geo_reference
5
6class General_mesh:
7    """Collection of 2D triangular elements
8
9    A triangular element is defined in terms of three vertex ids,
10    ordered counter clock-wise, each corresponding to a given node
11    which is represented as a coordinate set (x,y).
12    Vertices from different triangles can point to the same node.
13    The nodes are implemented as an Nx2 numeric array containing the
14    x and y coordinates.
15
16
17    To instantiate:
18       Mesh(nodes, triangles)
19
20    where
21
22      nodes is either a list of 2-tuples or an Nx2 numeric array of
23      floats representing all x, y coordinates in the mesh.
24
25      triangles is either a list of 3-tuples or an Mx3 numeric array of
26      integers representing indices of all vertices in the mesh.
27      Each vertex is identified by its index i in [0, N-1].
28
29
30    Example:
31
32        a = [0.0, 0.0]
33        b = [0.0, 2.0]
34        c = [2.0,0.0]
35        e = [2.0, 2.0]
36
37        nodes = [a, b, c, e]
38        triangles = [ [1,0,2], [1,2,3] ]   # bac, bce
39
40        # Create mesh with two triangles: bac and bce
41        mesh = Mesh(nodes, triangles)
42
43
44
45    Other:
46
47      In addition mesh computes an Mx6 array called vertex_coordinates.
48      This structure is derived from coordinates and contains for each
49      triangle the three x,y coordinates at the vertices.
50
51      See neighbourmesh.py for a specialisation of the general mesh class
52      which includes information about neighbours and the mesh boundary.
53
54      The mesh object is purely geometrical and contains no information
55      about quantities defined on the mesh.
56
57    """
58
59    # FIXME: It would be a good idea to use geospatial data as an alternative
60    #        input
61    def __init__(self,
62                 nodes,
63                 triangles,
64                 geo_reference=None,
65                 number_of_full_nodes=None,
66                 number_of_full_triangles=None,
67                 verbose=False):
68        """Build triangular 2d mesh from nodes and triangle information
69
70        Input:
71
72          nodes: x,y coordinates represented as a sequence of 2-tuples or
73                 a Nx2 numeric array of floats.
74
75          triangles: sequence of 3-tuples or Mx3 numeric array of
76                     non-negative integers representing indices into
77                     the nodes array.
78
79          georeference (optional): If specified coordinates are
80          assumed to be relative to this origin.
81
82
83        number_of_full_nodes and number_of_full_triangles relate to
84        parallelism when each mesh has an extra layer of ghost points and
85        ghost triangles attached to the end of the two arrays.
86        In this case it is usefull to specify the number of real (called full)
87        nodes and triangles. If omitted they will default to all.
88
89        """
90
91        if verbose: print 'General_mesh: Building basic mesh structure in ANUGA domain'
92
93        self.triangles = num.array(triangles, num.int)
94        self.nodes = num.array(nodes, num.float)
95
96        # Register number of elements and nodes
97        self.number_of_triangles = N = self.triangles.shape[0]
98        self.number_of_nodes = self.nodes.shape[0]
99
100        if number_of_full_nodes is None:
101            self.number_of_full_nodes = self.number_of_nodes
102        else:
103            assert int(number_of_full_nodes)
104            self.number_of_full_nodes = number_of_full_nodes
105
106        if number_of_full_triangles is None:
107            self.number_of_full_triangles = self.number_of_triangles
108        else:
109            assert int(number_of_full_triangles)
110            self.number_of_full_triangles = number_of_full_triangles
111
112        # FIXME: this stores a geo_reference, but when coords are returned
113        # This geo_ref is not taken into account!
114        if geo_reference is None:
115            self.geo_reference = Geo_reference()    # Use defaults
116        else:
117            self.geo_reference = geo_reference
118
119        # Input checks
120        msg = ('Triangles must an Mx3 numeric array or a sequence of 3-tuples. '
121               'The supplied array has the shape: %s'
122               % str(self.triangles.shape))
123        assert len(self.triangles.shape) == 2, msg
124
125        msg = ('Nodes must an Nx2 numeric array or a sequence of 2-tuples'
126               'The supplied array has the shape: %s' % str(self.nodes.shape))
127        assert len(self.nodes.shape) == 2, msg
128
129        msg = 'Vertex indices reference non-existing coordinate sets'
130        assert num.max(self.triangles) < self.nodes.shape[0], msg
131
132        # FIXME: Maybe move to statistics?
133        # Or use with get_extent
134        xy_extent = [min(self.nodes[:,0]), min(self.nodes[:,1]),
135                     max(self.nodes[:,0]), max(self.nodes[:,1])]
136
137        self.xy_extent = num.array(xy_extent, num.float)
138
139        # Allocate space for geometric quantities
140        self.normals = num.zeros((N, 6), num.float)
141        self.areas = num.zeros(N, num.float)
142        self.edgelengths = num.zeros((N, 3), num.float)
143
144        # Get x,y coordinates for all triangles and store
145        self.vertex_coordinates = V = self.compute_vertex_coordinates()
146
147        # Initialise each triangle
148        if verbose:
149            print 'General_mesh: Computing areas, normals and edgelengths'
150
151        for i in range(N):
152            if verbose and i % ((N+10)/10) == 0: print '(%d/%d)' % (i, N)
153
154            x0, y0 = V[3*i, :]
155            x1, y1 = V[3*i+1, :]
156            x2, y2 = V[3*i+2, :]
157
158            # Area
159            self.areas[i] = abs((x1*y0-x0*y1) + (x2*y1-x1*y2) + (x0*y2-x2*y0))/2
160
161            msg = 'Triangle (%f,%f), (%f,%f), (%f, %f)' % (x0,y0,x1,y1,x2,y2)
162            msg += ' is degenerate:  area == %f' % self.areas[i]
163            assert self.areas[i] > 0.0, msg
164
165            # Normals
166            # The normal vectors
167            #   - point outward from each edge
168            #   - are orthogonal to the edge
169            #   - have unit length
170            #   - Are enumerated according to the opposite corner:
171            #     (First normal is associated with the edge opposite
172            #     the first vertex, etc)
173            #   - Stored as six floats n0x,n0y,n1x,n1y,n2x,n2y per triangle
174            n0 = num.array([x2-x1, y2-y1], num.float)
175            l0 = num.sqrt(num.sum(n0**2))
176
177            n1 = num.array([x0-x2, y0-y2], num.float)
178            l1 = num.sqrt(num.sum(n1**2))
179
180            n2 = num.array([x1-x0, y1-y0], num.float)
181            l2 = num.sqrt(num.sum(n2**2))
182
183            # Normalise
184            n0 /= l0
185            n1 /= l1
186            n2 /= l2
187
188            # Compute and store
189            self.normals[i, :] = [n0[1], -n0[0],
190                                  n1[1], -n1[0],
191                                  n2[1], -n2[0]]
192
193            # Edgelengths
194            self.edgelengths[i, :] = [l0, l1, l2]
195
196        # Build structure listing which trianglse belong to which node.
197        if verbose: print 'Building inverted triangle structure'
198        self.build_inverted_triangle_structure()
199
200    def __len__(self):
201        return self.number_of_triangles
202
203    def __repr__(self):
204        return ('Mesh: %d vertices, %d triangles'
205                % (self.nodes.shape[0], len(self)))
206
207    def get_normals(self):
208        """Return all normal vectors.
209
210        Return normal vectors for all triangles as an Nx6 array
211        (ordered as x0, y0, x1, y1, x2, y2 for each triangle)
212        """
213
214        return self.normals
215
216    def get_normal(self, i, j):
217        """Return normal vector j of the i'th triangle.
218
219        Return value is the numeric array slice [x, y]
220        """
221
222        return self.normals[i, 2*j:2*j+2]
223       
224    def get_edgelength(self, i, j):
225        """Return length of j'th edge of the i'th triangle.
226        Return value is the numeric array slice [x, y]
227        """
228        return self.edgelengths[i, j]
229               
230
231    def get_number_of_nodes(self):
232        return self.number_of_nodes
233
234    def get_nodes(self, absolute=False):
235        """Return all nodes in mesh.
236
237        The nodes are ordered in an Nx2 array where N is the number of nodes.
238        This is the same format they were provided in the constructor
239        i.e. without any duplication.
240
241        Boolean keyword argument absolute determines whether coordinates
242        are to be made absolute by taking georeference into account
243        Default is False as many parts of ANUGA expects relative coordinates.
244        (To see which, switch to default absolute=True and run tests).
245        """
246
247        N = self.number_of_full_nodes
248        V = self.nodes[:N,:]
249        if absolute is True:
250            if not self.geo_reference.is_absolute():
251                V = self.geo_reference.get_absolute(V)
252
253        return V
254
255    def get_node(self, i, absolute=False):
256        """Return node coordinates for triangle i.
257
258        Boolean keyword argument absolute determines whether coordinates
259        are to be made absolute by taking georeference into account
260        Default is False as many parts of ANUGA expects relative coordinates.
261        (To see which, switch to default absolute=True and run tests).
262
263        Note: This method returns a modified _copy_ of the nodes slice if
264              absolute is True.  If absolute is False, just return the slice.
265              This is related to the ensure_numeric() returning a copy problem.
266        """
267
268        V = self.nodes[i,:]
269        print 'V.dtype=%s' % str(V.dtype())
270        if absolute is True:
271            if not self.geo_reference.is_absolute():
272                # get a copy so as not to modify the internal self.nodes array
273                V = copy.copy(V)
274                print ('self.geo_reference.get_xllcorner()=%s'
275                       % self.geo_reference.get_xllcorner())
276                print ('self.geo_reference.get_yllcorner()=%s'
277                       % self.geo_reference.get_yllcorner())
278                V += num.array([self.geo_reference.get_xllcorner(),
279                                self.geo_reference.get_yllcorner()], num.float)
280        return V
281
282    def get_vertex_coordinates(self, triangle_id=None, absolute=False):
283        """Return vertex coordinates for all triangles.
284
285        Return all vertex coordinates for all triangles as a 3*M x 2 array
286        where the jth vertex of the ith triangle is located in row 3*i+j and
287        M the number of triangles in the mesh.
288
289        if triangle_id is specified (an integer) the 3 vertex coordinates
290        for triangle_id are returned.
291
292        Boolean keyword argument absolute determines whether coordinates
293        are to be made absolute by taking georeference into account
294        Default is False as many parts of ANUGA expects relative coordinates.
295        """
296
297        V = self.vertex_coordinates
298
299        if triangle_id is None:
300            if absolute is True:
301                if not self.geo_reference.is_absolute():
302                    V = self.geo_reference.get_absolute(V)
303            return V
304        else:
305            i = triangle_id
306            msg = 'triangle_id must be an integer'
307            assert int(i) == i, msg
308            assert 0 <= i < self.number_of_triangles
309
310            i3 = 3*i
311            if absolute is True and not self.geo_reference.is_absolute():
312                offset=num.array([self.geo_reference.get_xllcorner(),
313                                  self.geo_reference.get_yllcorner()], num.float)
314                return num.array([V[i3,:]+offset,
315                                  V[i3+1,:]+offset,
316                                  V[i3+2,:]+offset], num.float)
317            else:
318                return num.array([V[i3,:], V[i3+1,:], V[i3+2,:]], num.float)
319
320    def get_vertex_coordinate(self, i, j, absolute=False):
321        """Return coordinates for vertex j of the i'th triangle.
322        Return value is the numeric array slice [x, y]
323        """
324
325        msg = 'vertex id j must be an integer in [0,1,2]'
326        assert j in [0,1,2], msg
327
328        V = self.get_vertex_coordinates(triangle_id=i, absolute=absolute)
329        return V[j,:]
330
331    def compute_vertex_coordinates(self):
332        """Return all vertex coordinates for all triangles as a 3*M x 2 array
333        where the jth vertex of the ith triangle is located in row 3*i+j.
334
335        This function is used to precompute this important structure. Use
336        get_vertex coordinates to retrieve the points.
337        """
338
339        M = self.number_of_triangles
340        vertex_coordinates = num.zeros((3*M, 2), num.float)
341
342        for i in range(M):
343            for j in range(3):
344                k = self.triangles[i,j] # Index of vertex j in triangle i
345                vertex_coordinates[3*i+j,:] = self.nodes[k]
346
347        return vertex_coordinates
348
349    def get_triangles(self, indices=None):
350        """Get mesh triangles.
351
352        Return Mx3 integer array where M is the number of triangles.
353        Each row corresponds to one triangle and the three entries are
354        indices into the mesh nodes which can be obtained using the method
355        get_nodes()
356
357        Optional argument, indices is the set of triangle ids of interest.
358        """
359
360        M = self.number_of_full_triangles
361
362        if indices is None:
363            return self.triangles
364
365        return num.take(self.triangles, indices, axis=0)
366
367    def get_disconnected_triangles(self):
368        """Get mesh based on nodes obtained from get_vertex_coordinates.
369
370        Return array Mx3 array of integers where each row corresponds to
371        a triangle. A triangle is a triplet of indices into
372        point coordinates obtained from get_vertex_coordinates and each
373        index appears only once
374
375        This provides a mesh where no triangles share nodes
376        (hence the name disconnected triangles) and different
377        nodes may have the same coordinates.
378
379        This version of the mesh is useful for storing meshes with
380        discontinuities at each node and is e.g. used for storing
381        data in sww files.
382
383        The triangles created will have the format
384        [[0,1,2],
385         [3,4,5],
386         [6,7,8],
387         ...
388         [3*M-3 3*M-2 3*M-1]]
389        """
390
391        M = len(self) # Number of triangles
392        K = 3*M       # Total number of unique vertices
393        return num.reshape(num.arange(K, dtype=num.int), (M,3))
394
395    def get_unique_vertices(self, indices=None):
396        """FIXME(Ole): This function needs a docstring"""
397
398        triangles = self.get_triangles(indices=indices)
399        unique_verts = {}
400        for triangle in triangles:
401            unique_verts[triangle[0]] = 0
402            unique_verts[triangle[1]] = 0
403            unique_verts[triangle[2]] = 0
404        return unique_verts.keys()
405
406    def get_triangles_and_vertices_per_node(self, node=None):
407        """Get triangles associated with given node.
408
409        Return list of triangle_ids, vertex_ids for specified node.
410        If node in None or absent, this information will be returned
411        for all (full) nodes in a list L where L[v] is the triangle
412        list for node v.
413        """
414
415        triangle_list = []
416        if node is not None:
417            # Get index for this node
418            first = num.sum(self.number_of_triangles_per_node[:node])
419
420            # Get number of triangles for this node
421            count = self.number_of_triangles_per_node[node]
422
423            for i in range(count):
424                index = self.vertex_value_indices[first+i]
425
426                volume_id = index / 3
427                vertex_id = index % 3
428
429                triangle_list.append( (volume_id, vertex_id) )
430
431            triangle_list = num.array(triangle_list, num.int)    #array default#
432        else:
433            # Get info for all nodes recursively.
434            # If need be, we can speed this up by
435            # working directly with the inverted triangle structure
436            for i in range(self.number_of_full_nodes):
437                L = self.get_triangles_and_vertices_per_node(node=i)
438                triangle_list.append(L)
439
440        return triangle_list
441
442    def build_inverted_triangle_structure(self):
443        """Build structure listing triangles belonging to each node
444
445        Two arrays are created and store as mesh attributes
446
447        number_of_triangles_per_node: An integer array of length N
448        listing for each node how many triangles use it. N is the number of
449        nodes in mesh.
450
451        vertex_value_indices: An array of length M listing indices into
452        triangles ordered by node number. The (triangle_id, vertex_id)
453        pairs are obtained from each index as (index/3, index%3) or each
454        index can be used directly into a flattened triangles array. This
455        is for example the case in the quantity.c where this structure is
456        used to average vertex values efficiently.
457
458        Example:
459        a = [0.0, 0.0] # node 0
460        b = [0.0, 2.0] # node 1
461        c = [2.0, 0.0] # node 2
462        d = [0.0, 4.0] # node 3
463        e = [2.0, 2.0] # node 4
464        f = [4.0, 0.0] # node 5
465        nodes = array([a, b, c, d, e, f])
466
467        #                    bac,     bce,     ecf,     dbe
468        triangles = array([[1,0,2], [1,2,4], [4,2,5], [3,1,4]])
469
470        For this structure:
471        number_of_triangles_per_node = [1 3 3 1 3 1]
472        which means that node a has 1 triangle associated with it, node b
473        has 3, node has 3 and so on.
474
475        vertex_value_indices = [ 1  0  3 10  2  4  7  9  5  6 11  8]
476        which reflects the fact that
477        node 0 is used by triangle 0, vertex 1 (index = 1)
478        node 1 is used by triangle 0, vertex 0 (index = 0)
479                   and by triangle 1, vertex 0 (index = 3)
480                   and by triangle 3, vertex 1 (index = 10)
481        node 2 is used by triangle 0, vertex 2 (index = 2)
482                   and by triangle 1, vertex 1 (index = 4)
483                   and by triangle 2, vertex 1 (index = 7)
484        node 3 is used by triangle 3, vertex 0 (index = 9)
485        node 4 is used by triangle 1, vertex 2 (index = 5)
486                   and by triangle 2, vertex 0 (index = 6)
487                   and by triangle 3, vertex 2 (index = 11)
488        node 5 is used by triangle 2, vertex 2 (index = 8)
489
490        Preconditions:
491          self.nodes and self.triangles are defined
492
493        Postcondition:
494          self.number_of_triangles_per_node is built
495          self.vertex_value_indices is built
496        """
497
498        # Count number of triangles per node
499        number_of_triangles_per_node = num.zeros(self.number_of_full_nodes,
500                                                 num.int)       #array default#
501        for volume_id, triangle in enumerate(self.get_triangles()):
502            for vertex_id in triangle:
503                number_of_triangles_per_node[vertex_id] += 1
504
505        # Allocate space for inverted structure
506        number_of_entries = num.sum(number_of_triangles_per_node)
507        vertex_value_indices = num.zeros(number_of_entries, num.int) #array default#
508
509        # Register (triangle, vertex) indices for each node
510        vertexlist = [None] * self.number_of_full_nodes
511        for volume_id in range(self.number_of_full_triangles):
512            a = self.triangles[volume_id, 0]
513            b = self.triangles[volume_id, 1]
514            c = self.triangles[volume_id, 2]
515
516            for vertex_id, node_id in enumerate([a, b, c]):
517                if vertexlist[node_id] is None:
518                    vertexlist[node_id] = []
519                vertexlist[node_id].append((volume_id, vertex_id))
520
521        # Build inverted triangle index array
522        k = 0
523        for vertices in vertexlist:
524            if vertices is not None:
525                for volume_id, vertex_id in vertices:
526                    vertex_value_indices[k] = 3*volume_id + vertex_id
527                    k += 1
528
529        # Save structure
530        self.number_of_triangles_per_node = number_of_triangles_per_node
531        self.vertex_value_indices = vertex_value_indices
532
533    def get_extent(self, absolute=False):
534        """Return min and max of all x and y coordinates
535
536        Boolean keyword argument absolute determines whether coordinates
537        are to be made absolute by taking georeference into account
538        """
539
540        C = self.get_vertex_coordinates(absolute=absolute)
541        X = C[:,0:6:2].copy()
542        Y = C[:,1:6:2].copy()
543
544        xmin = num.min(X)
545        xmax = num.max(X)
546        ymin = num.min(Y)
547        ymax = num.max(Y)
548
549        return xmin, xmax, ymin, ymax
550
551    def get_areas(self):
552        """Get areas of all individual triangles."""
553
554        return self.areas
555
556    def get_area(self):
557        """Return total area of mesh"""
558
559        return num.sum(self.areas)
560
561    def set_georeference(self, g):
562        self.geo_reference = g
563
564    def get_georeference(self):
565        return self.geo_reference
566
Note: See TracBrowser for help on using the repository browser.