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

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

Added diagnostics about boundary forcing

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