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

Last change on this file since 525 was 517, checked in by duncan, 20 years ago

set_values can now work over a list of triangles, if values are given. This doesn't support functions yet.

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