source: inundation/ga/storm_surge/pyvolution/quantity.py @ 506

Last change on this file since 506 was 476, checked in by ole, 20 years ago

Reimplemented unit tests for semi implicit updates and fixed comments.

File size: 22.8 KB
Line 
1"""Class Quantity - Implements values at each triangular element
2
3To create:
4
5   Quantity(domain, vertex_values)
6
7   domain: Associated domain structure. Required.
8   
9   vertex_values: N x 3 array of values at each vertex for each element.
10                  Default None
11
12   If vertex_values are None Create array of zeros compatible with domain.
13   Otherwise check that it is compatible with dimenions of domain.
14   Otherwise raise an exception
15"""
16
17
18class Quantity:
19
20    def __init__(self, domain, vertex_values=None):
21
22        from mesh import Mesh
23        from Numeric import array, zeros, Float
24
25        msg = 'First argument in Quantity.__init__ '
26        msg += 'must be of class Mesh (or a subclass thereof)'
27        assert isinstance(domain, Mesh), msg
28
29        if vertex_values is None:
30            N = domain.number_of_elements           
31            self.vertex_values = zeros((N, 3), Float)
32        else:   
33            self.vertex_values = array(vertex_values).astype(Float)
34
35            N, V = self.vertex_values.shape
36            assert V == 3,\
37                   'Three vertex values per element must be specified'
38           
39
40            msg = 'Number of vertex values (%d) must be consistent with'\
41                  %N
42            msg += 'number of elements in specified domain (%d).'\
43                   %domain.number_of_elements
44           
45            assert N == domain.number_of_elements, msg
46
47        self.domain = domain   
48
49        #Allocate space for other quantities
50        self.centroid_values = zeros(N, Float)
51        self.edge_values = zeros((N, 3), Float)       
52
53        #Intialise centroid and edge_values
54        self.interpolate()
55
56    def __len__(self):
57        return self.centroid_values.shape[0]
58   
59    def interpolate(self):
60        """Compute interpolated values at edges and centroid
61        Pre-condition: vertex_values have been set
62        """
63
64        N = self.vertex_values.shape[0]       
65        for i in range(N):
66            v0 = self.vertex_values[i, 0]
67            v1 = self.vertex_values[i, 1]
68            v2 = self.vertex_values[i, 2]
69           
70            self.centroid_values[i] = (v0 + v1 + v2)/3
71
72        self.interpolate_from_vertices_to_edges()
73
74
75    def interpolate_from_vertices_to_edges(self):
76        #Call correct module function
77        #(either from this module or C-extension)
78        interpolate_from_vertices_to_edges(self)
79           
80           
81    def set_values(self, X, location='vertices', indexes = None):
82        """Set values for quantity
83
84        X: Compatible list, Numeric array (see below), constant or function
85        location: Where values are to be stored.
86                  Permissible options are: vertices, edges, centroid
87                  Default is "vertices"
88
89        In case of location == 'centroid' the dimension values must
90        be a list of a Numerical array of length N, N being the number
91        of elements in the mesh. Otherwise it must be of dimension Nx3
92
93        The values will be stored in elements following their
94        internal ordering.
95
96        If values are described a function, it will be evaluated at specified points
97
98        If selected location is vertices, values for centroid and edges
99        will be assigned interpolated values.
100        In any other case, only values for the specified locations
101        will be assigned and the others will be left undefined.
102        """
103
104        if location not in ['vertices', 'centroids', 'edges']:
105            msg = 'Invalid location: %s' %location
106            raise msg
107
108        if X is None:
109            msg = 'Given values are None'
110            raise msg           
111       
112        import types
113       
114        if callable(X):
115            #Use function specific method
116            self.set_function_values(X, location)           
117        elif type(X) in [types.FloatType, types.IntType, types.LongType]:
118            if location == 'centroids':
119                self.centroid_values[:] = X
120            elif location == 'edges':
121                self.edge_values[:] = X
122            else:
123                self.vertex_values[:] = X               
124
125        else:
126            #Use array specific method
127            self.set_array_values(X, location, indexes = indexes)
128
129        if location == 'vertices':
130            #Intialise centroid and edge_values
131            self.interpolate()
132           
133
134
135    def set_function_values(self, f, location='vertices'):
136        """Set values for quantity using specified function
137
138        f: x, y -> z Function where x, y and z are arrays
139        location: Where values are to be stored.
140                  Permissible options are: vertices, edges, centroid
141                  Default is "vertices"
142        """
143
144        if location == 'centroids':
145            P = self.domain.centroid_coordinates
146            self.set_values(f(P[:,0], P[:,1]), location)
147        elif location == 'edges':
148            raise 'Not implemented: %s' %location
149        else:
150            #Vertices
151            P = self.domain.get_vertex_coordinates()
152            for i in range(3):
153                self.vertex_values[:,i] = f(P[:,2*i], P[:,2*i+1])
154
155           
156    def set_array_values(self, values, location='vertices', indexes = None):
157        """Set values for quantity
158
159        values: Numeric array
160        location: Where values are to be stored.
161                  Permissible options are: vertices, edges, centroid
162                  Default is "vertices"
163       
164        indexes - if this action is carried out on a subset of elements
165        The element indexes are specified here.
166       
167        In case of location == 'centroid' the dimension values must
168        be a list of a Numerical array of length N, N being the number
169        of elements in the mesh. Otherwise it must be of dimension Nx3
170
171        The values will be stored in elements following their
172        internal ordering.
173
174        If selected location is vertices, values for centroid and edges
175        will be assigned interpolated values.
176        In any other case, only values for the specified locations
177        will be assigned and the others will be left undefined.
178        """
179
180        from Numeric import array, Float, Int
181
182        values = array(values).astype(Float)
183
184        if (indexes <> None):
185            indexes = array(indexes).astype(Int)
186            msg = 'Number of values must match number of indexes'
187            assert values.shape[0] == indexes.shape[0], msg
188           
189        N = self.centroid_values.shape[0]
190       
191        if location == 'centroids':
192            assert len(values.shape) == 1, 'Values array must be 1d'
193
194            if indexes == None:
195                msg = 'Number of values must match number of elements'
196                assert values.shape[0] == N, msg
197           
198                self.centroid_values = values
199            else:
200                msg = 'Number of values must match number of indexes'
201                assert values.shape[0] == indexes.shape[0], msg
202               
203                #Brute force
204                for i in range(len(indexes)):
205                    self.centroid_values[indexes[i]] = values[i]
206                   
207        elif location == 'edges':
208            assert len(values.shape) == 2, 'Values array must be 2d'
209
210            msg = 'Number of values must match number of elements'
211            assert values.shape[0] == N, msg
212           
213            msg = 'Array must be N x 3'           
214            assert values.shape[1] == 3, msg
215           
216            self.edge_values = values
217        else:
218            if len(values.shape) == 1:
219
220                if indexes == None:
221                    #Values are being specified once for each unique vertex
222                    msg = 'Number of values must match number of vertices'
223                    assert values.shape[0] == self.domain.coordinates.shape[0], msg
224                    self.set_vertex_values(values)
225                else:
226                    for element_index, value in map(None, indexes, values): 
227                        self.vertex_values[element_index, 0] = value
228                        self.vertex_values[element_index, 1] = value
229                        self.vertex_values[element_index, 2] = value
230            elif len(values.shape) == 2:
231                #Vertex values are given as a triplet for each triangle
232               
233                msg = 'Array must be N x 3'           
234                assert values.shape[1] == 3, msg
235               
236                if indexes == None:
237                    self.vertex_values = values
238                else:
239                    for element_index, value in map(None, indexes, values): 
240                        self.vertex_values[element_index] = value
241            else:   
242                msg = 'Values array must be 1d or 2d'
243                raise msg
244           
245
246    # FIXME have a get_vertex_values as well, so the 'level' quantity can be
247    # set, based on the elevation   
248    def set_vertex_values(self, A):
249        """Set vertex values for all triangles based on input array A
250        which is assumed to have one entry per (unique) vertex, i.e.
251        one value for each row in array self.domain.coordinates.
252        """
253
254        from Numeric import array, Float
255
256        #Assert that A can be converted to a Numeric array of appropriate dim
257        A = array(A, Float)
258
259        assert len(A.shape) == 1
260        assert A.shape[0] == self.domain.coordinates.shape[0]
261
262        N = A.shape[0]
263       
264        #Go through list of unique vertices
265        for k in range(N):
266            L = self.domain.vertexlist[k]
267               
268            if L is None: continue #In case there are unused points
269
270            #Go through all triangle, vertex pairs
271            #touching vertex k and set corresponding vertex value
272            for triangle_id, vertex_id in L:
273                self.vertex_values[triangle_id, vertex_id] = A[k]
274               
275        #Intialise centroid and edge_values
276        self.interpolate()
277
278 
279    def smooth_vertex_values(self, value_array='field_values',
280                             precision = None):
281        """ Smooths field_values or conserved_quantities data.
282        TODO: be able to smooth individual fields
283        NOTE:  This function does not have a test.
284        FIXME: NOT DONE - do we need it?
285        """
286
287        from Numeric import concatenate, zeros, Float, Int, array, reshape
288
289       
290        A,V = self.get_vertex_values(xy=False,
291                                     value_array=value_array,
292                                     smooth = True,
293                                     precision = precision)
294
295        #Set some field values
296        for volume in self:
297            for i,v in enumerate(volume.vertices):
298                if value_array == 'field_values':
299                    volume.set_field_values('vertex', i, A[v,:])
300                elif value_array == 'conserved_quantities':
301                    volume.set_conserved_quantities('vertex', i, A[v,:])
302
303        if value_array == 'field_values':
304            self.precompute()
305        elif value_array == 'conserved_quantities':
306            Volume.interpolate_conserved_quantities()
307       
308   
309    #Method for outputting model results
310    #FIXME: Split up into geometric and numeric stuff.
311    #FIXME: Geometric (X,Y,V) should live in mesh.py
312    #FIXME: STill remember to move XY to mesh
313    def get_vertex_values(self,
314                          xy=True, 
315                          smooth = None,
316                          precision = None,
317                          reduction = None):
318        """Return vertex values like an OBJ format
319
320        The vertex values are returned as one sequence in the 1D float array A.
321        If requested the coordinates will be returned in 1D arrays X and Y.
322       
323        The connectivity is represented as an integer array, V, of dimension
324        M x 3, where M is the number of volumes. Each row has three indices
325        into the X, Y, A arrays defining the triangle.
326
327        if smooth is True, vertex values corresponding to one common
328        coordinate set will be smoothed according to the given
329        reduction operator. In this case vertex coordinates will be
330        de-duplicated.
331       
332        If no smoothings is required, vertex coordinates and values will
333        be aggregated as a concatenation of values at
334        vertices 0, vertices 1 and vertices 2
335
336       
337        Calling convention
338        if xy is True:
339           X,Y,A,V = get_vertex_values
340        else:
341           A,V = get_vertex_values         
342           
343        """
344
345        from Numeric import concatenate, zeros, Float, Int, array, reshape
346
347
348        if smooth is None:
349            smooth = self.domain.smooth
350
351        if precision is None:
352            precision = Float
353
354        if reduction is None:
355            reduction = self.domain.reduction
356           
357        #Create connectivity
358        V = self.domain.get_triangles(unique=smooth)
359                       
360        if smooth == True:
361
362            N = len(self.domain.vertexlist)
363            A = zeros(N, precision)
364
365            #Smoothing loop
366            for k in range(N):
367                L = self.domain.vertexlist[k]
368               
369                #Go through all triangle, vertex pairs
370                #contributing to vertex k and register vertex value
371
372                if L is None: continue #In case there are unused points
373               
374                contributions = []
375                for volume_id, vertex_id in L:
376                    v = self.vertex_values[volume_id, vertex_id]
377                    contributions.append(v)
378
379                A[k] = reduction(contributions)   
380
381
382            if xy is True:
383                X = self.domain.coordinates[:,0].astype(precision)
384                Y = self.domain.coordinates[:,1].astype(precision)
385               
386                return X, Y, A, V
387            else:
388                return A, V
389        else:
390            #Don't smooth
391
392            A = self.vertex_values.flat
393
394            #Do vertex coordinates   
395            if xy is True:               
396                C = self.domain.get_vertex_coordinates()
397
398                X = C[:,0:6:2].copy()
399                Y = C[:,1:6:2].copy()               
400
401                return X.flat, Y.flat, A, V           
402            else:
403                return A, V
404
405           
406           
407
408
409class Conserved_quantity(Quantity):
410    """Class conserved quantity adds to Quantity:
411
412    boundary values, storage and method for updating, and
413    methods for extrapolation from centropid to vertices inluding
414    gradients and limiters
415    """
416
417    def __init__(self, domain, vertex_values=None):
418        Quantity.__init__(self, domain, vertex_values)
419       
420        from Numeric import zeros, Float
421
422        #Allocate space for boundary values
423        L = len(domain.boundary)
424        self.boundary_values = zeros(L, Float)
425
426        #Allocate space for updates of conserved quantities by
427        #flux calculations and forcing functions
428
429        N = domain.number_of_elements
430        self.explicit_update = zeros(N, Float )
431        self.semi_implicit_update = zeros(N, Float )
432               
433
434    def update(self, timestep):
435        #Call correct module function
436        #(either from this module or C-extension)
437        return update(self, timestep)
438
439
440    def compute_gradients(self):
441        #Call correct module function
442        #(either from this module or C-extension)
443        return compute_gradients(self)
444           
445
446    def limit(self):
447        #Call correct module function
448        #(either from this module or C-extension)
449        limit(self)
450       
451       
452    def extrapolate_first_order(self):
453        """Extrapolate conserved quantities from centroid to
454        vertices for each volume using
455        first order scheme.
456        """
457       
458        qc = self.centroid_values
459        qv = self.vertex_values
460
461        for i in range(3):
462            qv[:,i] = qc
463           
464           
465    def extrapolate_second_order(self):
466        #Call correct module function
467        #(either from this module or C-extension)
468        extrapolate_second_order(self)
469
470
471def update(quantity, timestep):   
472    """Update centroid values based on values stored in
473    explicit_update and semi_implicit_update as well as given timestep
474
475    Function implementing forcing terms must take on argument
476    which is the domain and they must update either explicit
477    or implicit updates, e,g,:
478
479    def gravity(domain):
480        ....
481        domain.quantities['xmomentum'].explicit_update = ...
482        domain.quantities['ymomentum'].explicit_update = ...
483
484       
485
486    Explicit terms must have the form
487   
488        G(q, t)
489   
490    and explicit scheme is
491   
492       q^{(n+1}) = q^{(n)} + delta_t G(q^{n}, n delta_t)
493
494
495    Semi implicit forcing terms are assumed to have the form
496   
497       G(q, t) = H(q, t) q
498   
499    and the semi implicit scheme will then be
500   
501      q^{(n+1}) = q^{(n)} + delta_t H(q^{n}, n delta_t) q^{(n+1})
502
503   
504    """
505   
506    from Numeric import sum, equal, ones, Float
507       
508    N = quantity.centroid_values.shape[0]
509
510
511    #Divide H by conserved quantity to obtain G (see docstring above)
512    for k in range(N):
513        x = quantity.centroid_values[k] 
514        if x == 0.0:
515            quantity.semi_implicit_update[k] = 0.0           
516        else:
517            quantity.semi_implicit_update[k] /= x             
518           
519       
520    #Explicit updates
521    quantity.centroid_values += timestep*quantity.explicit_update
522           
523    #Semi implicit updates
524    denominator = ones(N, Float)-timestep*quantity.semi_implicit_update
525
526    if sum(equal(denominator, 0.0)) > 0.0:
527        msg = 'Zero division in semi implicit update. Call Stephen :-)'
528        raise msg
529    else:
530        #Update conserved_quantities from semi implicit updates
531        quantity.centroid_values /= denominator
532
533
534def interpolate_from_vertices_to_edges(quantity):
535    """Compute edge values from vertex values using linear interpolation
536    """
537
538    for k in range(quantity.vertex_values.shape[0]):
539        q0 = quantity.vertex_values[k, 0]
540        q1 = quantity.vertex_values[k, 1]
541        q2 = quantity.vertex_values[k, 2]
542           
543        quantity.edge_values[k, 0] = 0.5*(q1+q2)
544        quantity.edge_values[k, 1] = 0.5*(q0+q2) 
545        quantity.edge_values[k, 2] = 0.5*(q0+q1)
546
547
548
549def extrapolate_second_order(quantity):       
550    """Extrapolate conserved quantities from centroid to
551    vertices for each volume using
552    second order scheme.
553    """
554       
555    a, b = quantity.compute_gradients()
556
557    X = quantity.domain.get_vertex_coordinates()
558    qc = quantity.centroid_values
559    qv = quantity.vertex_values
560   
561    #Check each triangle
562    for k in range(quantity.domain.number_of_elements):
563        #Centroid coordinates           
564        x, y = quantity.domain.centroid_coordinates[k]
565       
566        #vertex coordinates
567        x0, y0, x1, y1, x2, y2 = X[k,:]
568       
569        #Extrapolate
570        qv[k,0] = qc[k] + a[k]*(x0-x) + b[k]*(y0-y)
571        qv[k,1] = qc[k] + a[k]*(x1-x) + b[k]*(y1-y)
572        qv[k,2] = qc[k] + a[k]*(x2-x) + b[k]*(y2-y)           
573
574
575def compute_gradients(quantity):                   
576    """Compute gradients of triangle surfaces defined by centroids of
577    neighbouring volumes.
578    If one edge is on the boundary, use own centroid as neighbour centroid.
579    If two or more are on the boundary, fall back to first order scheme.
580    """
581
582    from Numeric import zeros, Float
583    from util import gradient
584   
585    centroid_coordinates = quantity.domain.centroid_coordinates
586    surrogate_neighbours = quantity.domain.surrogate_neighbours   
587    centroid_values = quantity.centroid_values   
588    number_of_boundaries = quantity.domain.number_of_boundaries     
589   
590    N = centroid_values.shape[0]
591
592    a = zeros(N, Float)
593    b = zeros(N, Float)
594   
595    for k in range(N):
596        if number_of_boundaries[k] < 2:
597            #Two or three true neighbours
598
599            #Get indices of neighbours (or self when used as surrogate)     
600            k0, k1, k2 = surrogate_neighbours[k,:]
601
602            #Get data       
603            q0 = centroid_values[k0]
604            q1 = centroid_values[k1]
605            q2 = centroid_values[k2]                   
606
607            x0, y0 = centroid_coordinates[k0] #V0 centroid
608            x1, y1 = centroid_coordinates[k1] #V1 centroid
609            x2, y2 = centroid_coordinates[k2] #V2 centroid
610
611            #Gradient
612            a[k], b[k] = gradient(x0, y0, x1, y1, x2, y2, q0, q1, q2)
613       
614        elif number_of_boundaries[k] == 2:
615            #One true neighbour
616
617            #Get index of the one neighbour
618            for k0 in surrogate_neighbours[k,:]:
619                if k0 != k: break
620            assert k0 != k
621
622            k1 = k  #self
623
624            #Get data
625            q0 = centroid_values[k0]
626            q1 = centroid_values[k1]
627
628            x0, y0 = centroid_coordinates[k0] #V0 centroid
629            x1, y1 = centroid_coordinates[k1] #V1 centroid       
630
631            #Gradient
632            det = x0*y1 - x1*y0
633            if det != 0.0:
634                a[k] = (y1*q0 - y0*q1)/det
635                b[k] = (x0*q1 - x1*q0)/det
636
637        else:
638            #No true neighbours -       
639            #Fall back to first order scheme
640            pass
641       
642   
643    return a, b
644       
645                   
646
647def limit(quantity):       
648    """Limit slopes for each volume to eliminate artificial variance
649    introduced by e.g. second order extrapolator
650   
651    This is an unsophisticated limiter as it does not take into
652    account dependencies among quantities.
653   
654    precondition:
655    vertex values are estimated from gradient
656    postcondition:
657    vertex values are updated
658    """
659
660    from Numeric import zeros, Float
661
662    N = quantity.domain.number_of_elements
663   
664    beta = quantity.domain.beta
665       
666    qc = quantity.centroid_values
667    qv = quantity.vertex_values
668       
669    #Find min and max of this and neighbour's centroid values
670    qmax = zeros(qc.shape, Float)
671    qmin = zeros(qc.shape, Float)       
672   
673    for k in range(N):
674        qmax[k] = qmin[k] = qc[k]
675        for i in range(3):
676            n = quantity.domain.neighbours[k,i]
677            if n >= 0:
678                qn = qc[n] #Neighbour's centroid value
679               
680                qmin[k] = min(qmin[k], qn)
681                qmax[k] = max(qmax[k], qn)
682     
683   
684    #Diffences between centroids and maxima/minima
685    dqmax = qmax - qc
686    dqmin = qmin - qc
687       
688    #Deltas between vertex and centroid values
689    dq = zeros(qv.shape, Float)
690    for i in range(3):
691        dq[:,i] = qv[:,i] - qc
692
693    #Phi limiter   
694    for k in range(N):
695       
696        #Find the gradient limiter (phi) across vertices
697        phi = 1.0
698        for i in range(3):
699            r = 1.0
700            if (dq[k,i] > 0): r = dqmax[k]/dq[k,i]
701            if (dq[k,i] < 0): r = dqmin[k]/dq[k,i]
702           
703            phi = min( min(r*beta, 1), phi )   
704
705        #Then update using phi limiter
706        for i in range(3):           
707            qv[k,i] = qc[k] + phi*dq[k,i]
708
709
710
711import compile
712if compile.can_use_C_extension('quantity_ext.c'):
713    #Replace python version with c implementations
714       
715    from quantity_ext import limit, compute_gradients,\
716    extrapolate_second_order, interpolate_from_vertices_to_edges, update
717
Note: See TracBrowser for help on using the repository browser.