source: inundation/ga/storm_surge/pyvolution/domain.py @ 947

Last change on this file since 947 was 907, checked in by ole, 20 years ago

Addressed problem with artificial momentum generated by discontinuous water depths in the presence of steep slopes.
Now very shallow water is limited with a separate h-limiter controlled by beta_h
(see config.py) for details.

File size: 19.5 KB
Line 
1"""Class Domain - 2D triangular domains for finite-volume computations of
2   the shallow water wave equation
3
4
5   Copyright 2004
6   Ole Nielsen, Stephen Roberts, Duncan Gray, Christopher Zoppou
7   Geoscience Australia
8"""
9
10from mesh import Mesh
11from generic_boundary_conditions import *
12import types
13
14class Domain(Mesh):
15
16    def __init__(self, coordinates, vertices, boundary = None,
17                 conserved_quantities = None, other_quantities = None,
18                 tagged_elements = None):
19
20        Mesh.__init__(self, coordinates, vertices, boundary, tagged_elements)
21
22        from Numeric import zeros, Float
23        from quantity import Quantity, Conserved_quantity
24
25        #List of quantity names entering
26        #the conservation equations
27        #(Must be a subset of quantities)
28        if conserved_quantities is None:           
29            self.conserved_quantities = []
30        else:
31            self.conserved_quantities = conserved_quantities           
32
33        if other_quantities is None:
34            self.other_quantities = []
35        else:
36            self.other_quantities = other_quantities           
37           
38
39        #Build dictionary of Quantity instances keyed by quantity names
40        self.quantities = {}
41
42        #FIXME: remove later - maybe OK, though....
43        for name in self.conserved_quantities:
44            self.quantities[name] = Conserved_quantity(self)
45        for name in self.other_quantities:
46            self.quantities[name] = Quantity(self)
47
48        #Create an empty list for explicit forcing terms
49        self.forcing_terms = []
50
51
52
53        #Defaults
54        from config import max_smallsteps, beta_w, beta_h, epsilon
55        self.beta_w = beta_w
56        self.beta_h = beta_h       
57        self.epsilon = epsilon
58
59        #FIXME: Maybe have separate orders for h-limiter and w-limiter?
60        #Or maybe get rid of order altogether and use beta_w and beta_h
61        self.default_order = 1
62        self.order = self.default_order
63        self.smallsteps = 0
64        self.max_smallsteps = max_smallsteps       
65        self.number_of_steps = 0
66        self.number_of_first_order_steps = 0       
67
68        #Model time   
69        self.time = 0.0
70        self.finaltime = None
71        self.min_timestep = self.max_timestep = 0.0
72        self.starttime = 0    #Physical starttime if any (0 is 1 Jan 1970 00:00:00)
73       
74        #Checkpointing and storage             
75        from config import default_datadir
76        self.datadir = default_datadir
77        self.filename = 'domain'
78        self.checkpoint = False
79       
80
81    #Public interface to Domain       
82    def get_conserved_quantities(self, vol_id, vertex=None, edge=None):
83        """Get conserved quantities at volume vol_id
84
85        If vertex is specified use it as index for vertex values
86        If edge is specified use it as index for edge values
87        If neither are specified use centroid values
88        If both are specified an exeception is raised
89
90        Return value: Vector of length == number_of_conserved quantities
91       
92        """
93
94        from Numeric import zeros, Float
95       
96        if not (vertex is None or edge is None):
97            msg = 'Values for both vertex and edge was specified.'
98            msg += 'Only one (or none) is allowed.' 
99            raise msg
100
101        q = zeros( len(self.conserved_quantities), Float)
102       
103        for i, name in enumerate(self.conserved_quantities):
104            Q = self.quantities[name]
105            if vertex is not None:
106                q[i] = Q.vertex_values[vol_id, vertex]
107            elif edge is not None:
108                q[i] = Q.edge_values[vol_id, edge]
109            else:
110                q[i] = Q.centroid_values[vol_id]               
111
112        return q
113
114
115    def set_quantity_vertices_dict(self, quantity_dict):
116        """Set values for named quantities.
117        The index is the quantity
118       
119        name: Name of quantity
120        X: Compatible list, Numeric array, const or function (see below)
121       
122        The values will be stored in elements following their
123        internal ordering.
124
125        """
126        for key in quantity_dict.keys():
127            self.set_quantity(key, quantity_dict[key], location='vertices')
128       
129
130    def set_quantity(self, name, X, location='vertices', indexes = None):
131        """Set values for named quantity
132       
133        name: Name of quantity
134        X: Compatible list, Numeric array, const or function (see below)
135        location: Where values are to be stored.
136                  Permissible options are: vertices, edges, centroid
137
138        In case of location == 'centroid' the dimension values must
139        be a list of a Numerical array of length N, N being the number
140        of elements. Otherwise it must be of dimension Nx3.
141       
142        Indexes is the set of element ids that the operation applies to
143       
144        The values will be stored in elements following their
145        internal ordering.
146       
147        #FIXME (Ole): I suggest the following interface
148        set_quantity(name, X, location, region)
149        where
150            name: Name of quantity
151            X:
152              -Compatible list,
153              -Numeric array,
154              -const or function (see below)
155              -another quantity Q or an expression of the form
156              a*Q+b, where a is a scalar or a compatible array or a quantity
157              Q is a quantity
158              b is either a scalar, a quantity or a compatible array
159            location: Where values are to be stored.
160                      Permissible options are: vertices, edges, centroid
161            region: Identify subset of triangles. Permissible values are
162                    - tag name (refers to tagged region)
163                    - indices (refers to specific triangles)
164                    - polygon (identifies region)
165                   
166            This should work for all values of X
167           
168
169
170        """
171
172        #from quantity import Quantity, Conserved_quantity
173       
174        #Create appropriate quantity object
175        ##if name in self.conserved_quantities:
176        ##    self.quantities[name] = Conserved_quantity(self)
177        ##else:
178        ##    self.quantities[name] = Quantity(self)           
179
180        #Set value
181        self.quantities[name].set_values(X, location, indexes = indexes)
182
183         
184    def get_quantity(self, name, location='vertices', indexes = None):
185        """Get values for named quantity
186       
187        name: Name of quantity
188       
189        In case of location == 'centroid' the dimension values must
190        be a list of a Numerical array of length N, N being the number
191        of elements. Otherwise it must be of dimension Nx3.
192
193        Indexes is the set of element ids that the operation applies to.
194       
195        The values will be stored in elements following their
196        internal ordering.
197        """
198
199        return self.quantities[name].get_values( location, indexes = indexes)
200   
201
202    def set_boundary(self, boundary_map):
203        """Associate boundary objects with tagged boundary segments.
204
205        Input boundary_map is a dictionary of boundary objects keyed
206        by symbolic tags to matched against tags in the internal dictionary
207        self.boundary.
208
209        As result one pointer to a boundary object is stored for each vertex
210        in the list self.boundary_objects.
211        More entries may point to the same boundary object
212       
213        Schematically the mapping is from two dictionaries to one list
214        where the index is used as pointer to the boundary_values arrays
215        within each quantity.
216
217        self.boundary:          (vol_id, edge_id): tag
218        boundary_map (input):   tag: boundary_object
219        ----------------------------------------------
220        self.boundary_objects:  ((vol_id, edge_id), boundary_object)
221       
222
223        Pre-condition:
224          self.boundary has been built.
225
226        Post-condition:
227          self.boundary_objects is built
228
229        If a tag from the domain doesn't appear in the input dictionary an
230        exception is raised.
231        However, if a tag is not used to the domain, no error is thrown.
232        FIXME: This would lead to implementation of a
233        default boundary condition       
234
235        Note: If a segment is listed in the boundary dictionary and if it is
236        not None, it *will* become a boundary -
237        even if there is a neighbouring triangle.
238        This would be the case for internal boundaries
239       
240        Boundary objects that are None will be skipped.
241
242        FIXME: If set_boundary is called multiple times and if Boundary
243        object is changed into None, the neighbour structure will not be
244        restored!!!
245       
246
247        """
248
249        self.boundary_objects = []
250
251        #FIXME: Try to remove the sorting and fix test_mesh.py
252        x = self.boundary.keys()
253        x.sort()
254        for k, (vol_id, edge_id) in enumerate(x):
255            tag = self.boundary[ (vol_id, edge_id) ]
256
257            if boundary_map.has_key(tag):
258                B = boundary_map[tag]
259
260                if B is not None:
261                    self.boundary_objects.append( ((vol_id, edge_id), B) )
262                    self.neighbours[vol_id, edge_id] = -len(self.boundary_objects)
263                else:
264                    pass
265                    #FIXME: Check and perhaps fix neighbour structure
266                   
267
268            else:
269                msg = 'ERROR (domain.py): Tag "%s" has not been ' %tag
270                msg += 'bound to a boundary object.\n'
271                msg += 'All boundary tags defined in domain must appear '
272                msg += 'in the supplied dictionary.\n'
273                msg += 'The tags are: %s' %self.get_boundary_tags()
274                raise msg
275
276               
277    def set_region(self, functions):
278        # The order of functions in the list is used.
279        if type(functions) not in [types.ListType,types.TupleType]:
280            functions = [functions]
281        for function in functions:
282            for tag in self.tagged_elements.keys():
283                function(tag, self.tagged_elements[tag], self)
284
285                #Do we need to do this sort of thing?
286                #self = function(tag, self.tagged_elements[tag], self)
287           
288    #MISC       
289    def check_integrity(self):
290        Mesh.check_integrity(self)
291
292        for quantity in self.conserved_quantities:
293            msg = 'Conserved quantities must be a subset of all quantities'
294            assert quantity in self.quantities, msg
295
296        ##assert hasattr(self, 'boundary_objects')   
297   
298    def write_time(self):
299        if self.min_timestep == self.max_timestep:
300            print 'Time = %.4f, delta t = %.8f, steps=%d (%d)'\
301                  %(self.time, self.min_timestep, self.number_of_steps,
302                    self.number_of_first_order_steps)
303        elif self.min_timestep > self.max_timestep:
304            print 'Time = %.4f, steps=%d (%d)'\
305                  %(self.time, self.number_of_steps,
306                    self.number_of_first_order_steps)           
307        else:
308            print 'Time = %.4f, delta t in [%.8f, %.8f], steps=%d (%d)'\
309                  %(self.time, self.min_timestep,
310                    self.max_timestep, self.number_of_steps,
311                    self.number_of_first_order_steps)
312
313
314    def get_name(self):
315        return self.filename   
316   
317    def set_name(self, name):
318        self.filename = name   
319
320    def get_datadir(self):
321        return self.datadir     
322                       
323    def set_datadir(self, name):
324        self.datadir = name     
325
326       
327
328    #def set_defaults(self):
329    #    """Set default values for uninitialised quantities.
330    #    Should be overridden or specialised by specific modules
331    #    """#
332    #
333    #    for name in self.conserved_quantities + self.other_quantities:
334    #        self.set_quantity(name, 0.0)       
335               
336
337    ###########################
338    #Main components of evolve
339
340    def evolve(self, yieldstep = None, finaltime = None):
341        """Evolve model from time=0.0 to finaltime yielding results
342        every yieldstep.
343       
344        Internally, smaller timesteps may be taken.
345
346        Evolve is implemented as a generator and is to be called as such, e.g.
347       
348        for t in domain.evolve(timestep, yieldstep, finaltime):
349            <Do something with domain and t>
350       
351        """
352
353        from config import min_timestep, max_timestep, epsilon
354
355        #FIXME: Maybe lump into a larger check prior to evolving
356        msg = 'Boundary tags must be bound to boundary objects before evolving system, '
357        msg += 'e.g. using the method set_boundary.\n'       
358        msg += 'This system has the boundary tags %s ' %self.get_boundary_tags()
359        assert hasattr(self, 'boundary_objects'), msg
360       
361        ##self.set_defaults()
362
363        if yieldstep is None:
364            yieldstep = max_timestep
365
366        self.order = self.default_order
367       
368           
369        self.yieldtime = 0.0 #Time between 'yields'
370
371        #Initialise interval of timestep sizes (for reporting only)
372        self.min_timestep = max_timestep
373        self.max_timestep = min_timestep
374        self.finaltime = finaltime
375        self.number_of_steps = 0
376        self.number_of_first_order_steps = 0               
377
378        #Initial update of vertex and edge values
379        self.distribute_to_vertices_and_edges()
380       
381       
382        #Or maybe restore from latest checkpoint
383        if self.checkpoint is True:
384            self.goto_latest_checkpoint()
385
386           
387        yield(self.time)  #Yield initial values
388       
389        while True:
390            #Update boundary values
391            self.update_boundary()           
392
393            #Compute fluxes across each element edge
394            self.compute_fluxes()
395
396            #Update timestep to fit yieldstep and finaltime
397            self.update_timestep(yieldstep, finaltime)
398
399            #Update conserved quantities
400            self.update_conserved_quantities()           
401
402            #Update vertex and edge values
403            self.distribute_to_vertices_and_edges()
404           
405            #Update time   
406            self.time += self.timestep
407            self.yieldtime += self.timestep
408            self.number_of_steps += 1
409            if self.order == 1:
410                self.number_of_first_order_steps += 1       
411
412            #Yield results
413            if finaltime is not None and abs(self.time - finaltime) < epsilon:
414                # Yield final time and stop
415                yield(self.time)
416                break
417
418               
419            if abs(self.yieldtime - yieldstep) < epsilon: 
420                # Yield (intermediate) time and allow inspection of domain
421
422                if self.checkpoint is True:
423                    self.store_checkpoint()
424                    self.delete_old_checkpoints()
425                   
426                #Pass control on to outer loop for more specific actions   
427                yield(self.time) 
428
429                # Reinitialise
430                self.yieldtime = 0.0               
431                self.min_timestep = max_timestep
432                self.max_timestep = min_timestep
433                self.number_of_steps = 0
434                self.number_of_first_order_steps = 0                       
435
436           
437    def evolve_to_end(self, finaltime = 1.0):
438        """Iterate evolve all the way to the end
439        """
440
441        for _ in self.evolve(yieldstep=None, finaltime=finaltime):           
442            pass
443       
444
445   
446    def update_boundary(self):
447        """Go through list of boundary objects and update boundary values
448        for all conserved quantities on boundary.
449        """
450
451        #FIXME: Update only those that change (if that can be worked out)
452        for i, ((vol_id, edge_id), B) in enumerate(self.boundary_objects):
453            q = B.evaluate(vol_id, edge_id)
454
455            for j, name in enumerate(self.conserved_quantities):
456                Q = self.quantities[name]
457                Q.boundary_values[i] = q[j]
458
459
460    def compute_fluxes(self):
461        msg = 'Method compute_fluxes must be overridden by Domain subclass'
462        raise msg
463
464
465    def update_timestep(self, yieldstep, finaltime):
466
467        from config import min_timestep
468   
469        timestep = self.timestep
470   
471        #Record maximal and minimal values of timestep for reporting   
472        self.max_timestep = max(timestep, self.max_timestep)
473        self.min_timestep = min(timestep, self.min_timestep)
474   
475        #Protect against degenerate time steps
476        if timestep < min_timestep:
477
478            #Number of consecutive small steps taken b4 taking action
479            self.smallsteps += 1 
480
481            if self.smallsteps > self.max_smallsteps:
482                self.smallsteps = 0 #Reset
483               
484                if self.order == 1:
485                    msg = 'Too small timestep %.16f reached ' %timestep
486                    msg += 'even after %d steps of 1 order scheme'\
487                           %self.max_smallsteps
488
489                    raise msg
490                else:
491                    #Try to overcome situation by switching to 1 order
492                    self.order = 1
493
494        else:
495            self.smallsteps = 0
496            if self.order == 1 and self.default_order == 2:
497                self.order = 2
498                   
499
500        #Ensure that final time is not exceeded
501        if finaltime is not None and self.time + timestep > finaltime:
502            timestep = finaltime-self.time
503
504        #Ensure that model time is aligned with yieldsteps
505        if self.yieldtime + timestep > yieldstep:
506            timestep = yieldstep-self.yieldtime
507
508        self.timestep = timestep
509
510
511
512    def compute_forcing_terms(self):         
513        """If there are any forcing functions driving the system
514        they should be defined in Domain subclass and appended to
515        the list self.forcing_terms
516        """
517
518        for f in self.forcing_terms:
519            f(self)
520
521       
522
523    def update_conserved_quantities(self):
524        """Update vectors of conserved quantities using previously
525        computed fluxes specified forcing functions.
526        """
527
528        from Numeric import ones, sum, equal, Float
529           
530        N = self.number_of_elements
531        d = len(self.conserved_quantities)
532       
533        timestep = self.timestep
534
535        #Compute forcing terms   
536        self.compute_forcing_terms()   
537
538        #Update conserved_quantities
539        for name in self.conserved_quantities:
540            Q = self.quantities[name]
541            Q.update(timestep)
542
543            #Clean up
544            #Note that Q.explicit_update is reset by compute_fluxes           
545            Q.semi_implicit_update[:] = 0.0
546
547           
548
549    def distribute_to_vertices_and_edges(self):
550        """Extrapolate conserved quantities from centroid to
551        vertices and edge-midpoints for each volume
552
553        Default implementation is straight first order,
554        i.e. constant values throughout each element and
555        no reference to non-conserved quantities.
556        """
557
558        for name in self.conserved_quantities:
559            Q = self.quantities[name]
560            if self.order == 1:
561                Q.extrapolate_first_order()
562            elif self.order == 2:
563                Q.extrapolate_second_order()
564                Q.limit()                               
565            else:
566                raise 'Unknown order'
567            Q.interpolate_from_vertices_to_edges()                           
568
569
570               
571##############################################
572#Initialise module
573
574#Optimisation with psyco
575from config import use_psyco
576if use_psyco:
577    try:
578        import psyco
579    except:
580        msg = 'WARNING: psyco (speedup) could not import'+\
581              ', you may want to consider installing it'
582        print msg
583    else:
584        psyco.bind(Domain.update_boundary) 
585        #psyco.bind(Domain.update_timestep)     #Not worth it
586        psyco.bind(Domain.update_conserved_quantities)
587        psyco.bind(Domain.distribute_to_vertices_and_edges)
588       
589
590if __name__ == "__main__":
591    pass
Note: See TracBrowser for help on using the repository browser.