source: inundation/pyvolution/domain.py @ 2492

Last change on this file since 2492 was 2491, checked in by ole, 19 years ago

Changed domain.get_quantity() to return a quantity object rather than the values.
Updated tests accordingly

File size: 25.4 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, geo_reference = None,
19                 use_inscribed_circle=False):
20
21        Mesh.__init__(self, coordinates, vertices, boundary,
22                      tagged_elements, geo_reference, use_inscribed_circle)
23
24        from Numeric import zeros, Float, Int
25        from quantity import Quantity, Conserved_quantity
26
27        #List of quantity names entering
28        #the conservation equations
29        #(Must be a subset of quantities)
30        if conserved_quantities is None:
31            self.conserved_quantities = []
32        else:
33            self.conserved_quantities = conserved_quantities
34
35        if other_quantities is None:
36            self.other_quantities = []
37        else:
38            self.other_quantities = other_quantities
39
40
41        #Build dictionary of Quantity instances keyed by quantity names
42        self.quantities = {}
43
44        #FIXME: remove later - maybe OK, though....
45        for name in self.conserved_quantities:
46            self.quantities[name] = Conserved_quantity(self)
47        for name in self.other_quantities:
48            self.quantities[name] = Quantity(self)
49
50        #Create an empty list for explicit forcing terms
51        self.forcing_terms = []
52
53
54        #Defaults
55        from config import max_smallsteps, beta_w, beta_h, epsilon, CFL
56        self.beta_w = beta_w
57        self.beta_h = beta_h
58        self.epsilon = epsilon
59
60        #FIXME: Maybe have separate orders for h-limiter and w-limiter?
61        #Or maybe get rid of order altogether and use beta_w and beta_h
62        self.set_default_order(1)       
63        #self.default_order = 1
64        #self.order = self.default_order
65
66        self.smallsteps = 0
67        self.max_smallsteps = max_smallsteps
68        self.number_of_steps = 0
69        self.number_of_first_order_steps = 0
70        self.CFL = CFL
71
72        #Model time
73        self.time = 0.0
74        self.finaltime = None
75        self.min_timestep = self.max_timestep = 0.0
76        self.starttime = 0 #Physical starttime if any (0 is 1 Jan 1970 00:00:00)
77
78        ######OBSOLETE
79        #Origin in UTM coordinates
80        #FIXME: This should be set if read by a msh file
81        #self.zone = zone
82        #self.xllcorner = xllcorner
83        #self.yllcorner = yllcorner
84
85
86        #Checkpointing and storage
87        from config import default_datadir
88        self.datadir = default_datadir
89        self.filename = 'domain'
90        self.checkpoint = False
91
92        #MH310505 To avoid calculating the flux across each edge twice, keep an integer (boolean) array,
93        #to be used during the flux calculation
94        N=self.number_of_elements
95        self.already_computed_flux = zeros((N, 3), Int)
96
97
98    def set_default_order(self, n):
99        """Set default (spatial) order to either 1 or 2
100        """
101
102        msg = 'Default order must be either 1 or 2. I got %s' %n
103        assert n in [1,2], msg
104       
105        self.default_order = n
106        self.order = self.default_order
107
108
109    #Public interface to Domain
110    def get_conserved_quantities(self, vol_id, vertex=None, edge=None):
111        """Get conserved quantities at volume vol_id
112
113        If vertex is specified use it as index for vertex values
114        If edge is specified use it as index for edge values
115        If neither are specified use centroid values
116        If both are specified an exeception is raised
117
118        Return value: Vector of length == number_of_conserved quantities
119
120        """
121
122        from Numeric import zeros, Float
123
124        if not (vertex is None or edge is None):
125            msg = 'Values for both vertex and edge was specified.'
126            msg += 'Only one (or none) is allowed.'
127            raise msg
128
129        q = zeros( len(self.conserved_quantities), Float)
130
131        for i, name in enumerate(self.conserved_quantities):
132            Q = self.quantities[name]
133            if vertex is not None:
134                q[i] = Q.vertex_values[vol_id, vertex]
135            elif edge is not None:
136                q[i] = Q.edge_values[vol_id, edge]
137            else:
138                q[i] = Q.centroid_values[vol_id]
139
140        return q
141
142    def set_time(self, time=0.0):
143        """Set the time"""
144
145        self.time = time
146
147    def set_quantity_vertices_dict(self, quantity_dict):
148        """Set values for named quantities.
149        The index is the quantity
150
151        name: Name of quantity
152        X: Compatible list, Numeric array, const or function (see below)
153
154        The values will be stored in elements following their
155        internal ordering.
156
157        """
158        for key in quantity_dict.keys():
159            self.set_quantity(key, quantity_dict[key], location='vertices')
160
161
162    def set_quantity(self, name, *args, **kwargs):
163        """Set values for named quantity
164
165
166        One keyword argument is documented here:
167        expression = None, # Arbitrary expression
168
169        expression:
170          Arbitrary expression involving quantity names
171
172        See Quantity.set_values for further documentation.
173        """
174
175        #FIXME (Ole): Allow new quantities here
176        #from quantity import Quantity, Conserved_quantity
177        #Create appropriate quantity object
178        ##if name in self.conserved_quantities:
179        ##    self.quantities[name] = Conserved_quantity(self)
180        ##else:
181        ##    self.quantities[name] = Quantity(self)
182
183
184        #Do the expression stuff
185        if kwargs.has_key('expression'):
186            expression = kwargs['expression']
187            del kwargs['expression']
188
189            Q = self.create_quantity_from_expression(expression)
190            kwargs['quantity'] = Q
191
192
193        #Assign values
194        self.quantities[name].set_values(*args, **kwargs)
195
196
197    def get_quantity(self, name, location='vertices', indices = None):
198        """Get quantity object.
199
200        name: Name of quantity
201
202        See methods inside the quantity object for more options
203        """
204
205        return self.quantities[name] #.get_values( location, indices = indices)
206
207
208    def get_quantity_object(self, name):
209        """Get object for named quantity
210
211        name: Name of quantity
212
213        FIXME: Obsolete
214        """
215
216        print 'get_quantity_object has been deprecated. Please use get_quantity'
217        return self.quantities[name]
218
219
220    def create_quantity_from_expression(self, expression):
221        """Create new quantity from other quantities using arbitrary expression
222
223        Combine existing quantities in domain using expression and return
224        result as a new quantity.
225
226        Note, the new quantity could e.g. be used in set_quantity
227
228        Valid expressions are limited to operators defined in class Quantity
229
230        Example:
231
232
233        """
234
235        from util import apply_expression_to_dictionary
236        return apply_expression_to_dictionary(expression, self.quantities)
237
238
239
240
241    def set_boundary(self, boundary_map):
242        """Associate boundary objects with tagged boundary segments.
243
244        Input boundary_map is a dictionary of boundary objects keyed
245        by symbolic tags to matched against tags in the internal dictionary
246        self.boundary.
247
248        As result one pointer to a boundary object is stored for each vertex
249        in the list self.boundary_objects.
250        More entries may point to the same boundary object
251
252        Schematically the mapping is from two dictionaries to one list
253        where the index is used as pointer to the boundary_values arrays
254        within each quantity.
255
256        self.boundary:          (vol_id, edge_id): tag
257        boundary_map (input):   tag: boundary_object
258        ----------------------------------------------
259        self.boundary_objects:  ((vol_id, edge_id), boundary_object)
260
261
262        Pre-condition:
263          self.boundary has been built.
264
265        Post-condition:
266          self.boundary_objects is built
267
268        If a tag from the domain doesn't appear in the input dictionary an
269        exception is raised.
270        However, if a tag is not used to the domain, no error is thrown.
271        FIXME: This would lead to implementation of a
272        default boundary condition
273
274        Note: If a segment is listed in the boundary dictionary and if it is
275        not None, it *will* become a boundary -
276        even if there is a neighbouring triangle.
277        This would be the case for internal boundaries
278
279        Boundary objects that are None will be skipped.
280
281        FIXME: If set_boundary is called multiple times and if Boundary
282        object is changed into None, the neighbour structure will not be
283        restored!!!
284
285
286        """
287
288        self.boundary_objects = []
289        self.boundary_map = boundary_map  #Store for use with eg. boundary_stats.
290
291        #FIXME: Try to remove the sorting and fix test_mesh.py
292        x = self.boundary.keys()
293        x.sort()
294
295        #Loop through edges that lie on the boundary and associate them with
296        #callable boundary objects depending on their tags
297        for k, (vol_id, edge_id) in enumerate(x):
298            tag = self.boundary[ (vol_id, edge_id) ]
299
300            if boundary_map.has_key(tag):
301                B = boundary_map[tag]  #Get callable boundary object
302
303                if B is not None:
304                    self.boundary_objects.append( ((vol_id, edge_id), B) )
305                    self.neighbours[vol_id, edge_id] = -len(self.boundary_objects)
306                else:
307                    pass
308                    #FIXME: Check and perhaps fix neighbour structure
309
310
311            else:
312                msg = 'ERROR (domain.py): Tag "%s" has not been ' %tag
313                msg += 'bound to a boundary object.\n'
314                msg += 'All boundary tags defined in domain must appear '
315                msg += 'in the supplied dictionary.\n'
316                msg += 'The tags are: %s' %self.get_boundary_tags()
317                raise msg
318
319
320    def set_region(self, functions):
321        # The order of functions in the list is used.
322        if type(functions) not in [types.ListType,types.TupleType]:
323            functions = [functions]
324        for function in functions:
325            for tag in self.tagged_elements.keys():
326                function(tag, self.tagged_elements[tag], self)
327
328                #Do we need to do this sort of thing?
329                #self = function(tag, self.tagged_elements[tag], self)
330
331    #MISC
332    def check_integrity(self):
333        Mesh.check_integrity(self)
334
335        for quantity in self.conserved_quantities:
336            msg = 'Conserved quantities must be a subset of all quantities'
337            assert quantity in self.quantities, msg
338
339        ##assert hasattr(self, 'boundary_objects')
340
341    def write_time(self):
342        print self.timestepping_statistics()
343
344        #Old version
345        #if self.min_timestep == self.max_timestep:
346        #    print 'Time = %.4f, delta t = %.8f, steps=%d (%d)'\
347        #          %(self.time, self.min_timestep, self.number_of_steps,
348        #            self.number_of_first_order_steps)
349        #elif self.min_timestep > self.max_timestep:
350        #    print 'Time = %.4f, steps=%d (%d)'\
351        #          %(self.time, self.number_of_steps,
352        #            self.number_of_first_order_steps)
353        #else:
354        #    print 'Time = %.4f, delta t in [%.8f, %.8f], steps=%d (%d)'\
355        #          %(self.time, self.min_timestep,
356        #            self.max_timestep, self.number_of_steps,
357        #            self.number_of_first_order_steps)
358
359    def timestepping_statistics(self):
360        """Return string with time stepping statistics for printing or logging
361        """
362
363        msg = ''
364        if self.min_timestep == self.max_timestep:
365            msg += 'Time = %.4f, delta t = %.8f, steps=%d (%d)'\
366                   %(self.time, self.min_timestep, self.number_of_steps,
367                     self.number_of_first_order_steps)
368        elif self.min_timestep > self.max_timestep:
369            msg += 'Time = %.4f, steps=%d (%d)'\
370                   %(self.time, self.number_of_steps,
371                     self.number_of_first_order_steps)
372        else:
373            msg += 'Time = %.4f, delta t in [%.8f, %.8f], steps=%d (%d)'\
374                   %(self.time, self.min_timestep,
375                     self.max_timestep, self.number_of_steps,
376                     self.number_of_first_order_steps)
377
378        return msg
379
380
381    def write_boundary_statistics(self, quantities = None, tags = None):
382        print self.boundary_statistics(quantities, tags)
383
384    def boundary_statistics(self, quantities = None, tags = None):
385        """Output statistics about boundary forcing at each timestep
386
387
388        Input:
389          quantities: either None, a string or a list of strings naming the quantities to be reported
390          tags:       either None, a string or a list of strings naming the tags to be reported         
391
392
393        Example output:
394        Tag 'wall':
395            stage in [2, 5.5]
396            xmomentum in []
397            ymomentum in []
398        Tag 'ocean'
399
400
401        If quantities are specified only report on those. Otherwise take all conserved quantities.
402        If tags are specified only report on those, otherwise take all tags.
403
404        """
405
406        #Input checks
407        import types, string
408
409        if quantities is None:
410            quantities = self.conserved_quantities
411        elif type(quantities) == types.StringType:
412            quantities = [quantities] #Turn it into a list
413
414        msg = 'Keyword argument quantities must be either None, '
415        msg += 'string or list. I got %s' %str(quantities)
416        assert type(quantities) == types.ListType, msg
417
418
419        if tags is None:
420            tags = self.get_boundary_tags()
421        elif type(tags) == types.StringType:
422            tags = [tags] #Turn it into a list
423
424        msg = 'Keyword argument tags must be either None, '
425        msg += 'string or list. I got %s' %str(tags)
426        assert type(tags) == types.ListType, msg
427
428        #Determine width of longest quantity name (for cosmetic purposes)
429        maxwidth = 0
430        for name in quantities:
431            w = len(name)
432            if w > maxwidth:
433                maxwidth = w
434
435        #Output stats
436        msg = 'Boundary values at time %.4f:\n' %self.time
437        for tag in tags:
438            msg += '    %s:\n' %tag
439
440            for name in quantities:
441                q = self.quantities[name]
442
443                #Find range of boundary values for tag and q
444                maxval = minval = None
445                for i, ((vol_id, edge_id), B) in\
446                        enumerate(self.boundary_objects):
447                    if self.boundary[(vol_id, edge_id)] == tag:
448                        v = q.boundary_values[i]
449                        if minval is None or v < minval: minval = v
450                        if maxval is None or v > maxval: maxval = v
451
452                if minval is None or maxval is None:
453                    msg += '        Sorry no information available about' +\
454                           ' tag %s and quantity %s\n' %(tag, name)
455                else:
456                    msg += '        %s in [%12.8f, %12.8f]\n'\
457                           %(string.ljust(name, maxwidth), minval, maxval)
458
459
460        return msg
461
462
463    def get_name(self):
464        return self.filename
465
466    def set_name(self, name):
467        self.filename = name
468
469    def get_datadir(self):
470        return self.datadir
471
472    def set_datadir(self, name):
473        self.datadir = name
474
475
476
477    #def set_defaults(self):
478    #    """Set default values for uninitialised quantities.
479    #    Should be overridden or specialised by specific modules
480    #    """#
481    #
482    #    for name in self.conserved_quantities + self.other_quantities:
483    #        self.set_quantity(name, 0.0)
484
485
486    ###########################
487    #Main components of evolve
488
489    def evolve(self, yieldstep = None, finaltime = None,
490               skip_initial_step = False):
491        """Evolve model from time=0.0 to finaltime yielding results
492        every yieldstep.
493
494        Internally, smaller timesteps may be taken.
495
496        Evolve is implemented as a generator and is to be called as such, e.g.
497
498        for t in domain.evolve(timestep, yieldstep, finaltime):
499            <Do something with domain and t>
500
501        """
502
503        from config import min_timestep, max_timestep, epsilon
504
505        #FIXME: Maybe lump into a larger check prior to evolving
506        msg = 'Boundary tags must be bound to boundary objects before evolving system, '
507        msg += 'e.g. using the method set_boundary.\n'
508        msg += 'This system has the boundary tags %s ' %self.get_boundary_tags()
509        assert hasattr(self, 'boundary_objects'), msg
510
511        ##self.set_defaults()
512
513        if yieldstep is None:
514            yieldstep = max_timestep
515        else:
516            yieldstep = float(yieldstep)
517
518        self.order = self.default_order
519
520
521        self.yieldtime = 0.0 #Time between 'yields'
522
523        #Initialise interval of timestep sizes (for reporting only)
524        self.min_timestep = max_timestep
525        self.max_timestep = min_timestep
526        self.finaltime = finaltime
527        self.number_of_steps = 0
528        self.number_of_first_order_steps = 0
529
530        #update ghosts
531        self.update_ghosts()
532
533        #Initial update of vertex and edge values
534        self.distribute_to_vertices_and_edges()
535
536        #Initial update boundary values
537        self.update_boundary()
538
539        #Or maybe restore from latest checkpoint
540        if self.checkpoint is True:
541            self.goto_latest_checkpoint()
542
543        if skip_initial_step is False:
544            yield(self.time)  #Yield initial values
545
546        while True:
547
548            #Compute fluxes across each element edge
549            self.compute_fluxes()
550
551            #Update timestep to fit yieldstep and finaltime
552            self.update_timestep(yieldstep, finaltime)
553
554            #Update conserved quantities
555            self.update_conserved_quantities()
556
557            #update ghosts
558            self.update_ghosts()
559
560            #Update vertex and edge values
561            self.distribute_to_vertices_and_edges()
562
563            #Update boundary values
564            self.update_boundary()
565
566            #Update time
567            self.time += self.timestep
568            self.yieldtime += self.timestep
569            self.number_of_steps += 1
570            if self.order == 1:
571                self.number_of_first_order_steps += 1
572
573            #Yield results
574            if finaltime is not None and abs(self.time - finaltime) < epsilon:
575
576                #FIXME: There is a rare situation where the
577                #final time step is stored twice. Can we make a test?
578
579                # Yield final time and stop
580                yield(self.time)
581                break
582
583
584            if abs(self.yieldtime - yieldstep) < epsilon:
585                # Yield (intermediate) time and allow inspection of domain
586
587                if self.checkpoint is True:
588                    self.store_checkpoint()
589                    self.delete_old_checkpoints()
590
591                #Pass control on to outer loop for more specific actions
592                yield(self.time)
593
594                # Reinitialise
595                self.yieldtime = 0.0
596                self.min_timestep = max_timestep
597                self.max_timestep = min_timestep
598                self.number_of_steps = 0
599                self.number_of_first_order_steps = 0
600
601
602    def evolve_to_end(self, finaltime = 1.0):
603        """Iterate evolve all the way to the end
604        """
605
606        for _ in self.evolve(yieldstep=None, finaltime=finaltime):
607            pass
608
609
610
611    def update_boundary(self):
612        """Go through list of boundary objects and update boundary values
613        for all conserved quantities on boundary.
614        """
615
616        #FIXME: Update only those that change (if that can be worked out)
617        #FIXME: Boundary objects should not include ghost nodes.
618        for i, ((vol_id, edge_id), B) in enumerate(self.boundary_objects):
619            q = B.evaluate(vol_id, edge_id)
620
621            for j, name in enumerate(self.conserved_quantities):
622                Q = self.quantities[name]
623                Q.boundary_values[i] = q[j]
624
625
626    def compute_fluxes(self):
627        msg = 'Method compute_fluxes must be overridden by Domain subclass'
628        raise msg
629
630
631    def update_timestep(self, yieldstep, finaltime):
632
633        from config import min_timestep
634
635        # self.timestep is calculated from speed of characteristics
636        # Apply CFL condition here
637        timestep = self.CFL*self.timestep
638
639        #Record maximal and minimal values of timestep for reporting
640        self.max_timestep = max(timestep, self.max_timestep)
641        self.min_timestep = min(timestep, self.min_timestep)
642
643        #Protect against degenerate time steps
644        if timestep < min_timestep:
645
646            #Number of consecutive small steps taken b4 taking action
647            self.smallsteps += 1
648
649            if self.smallsteps > self.max_smallsteps:
650                self.smallsteps = 0 #Reset
651
652                if self.order == 1:
653                    msg = 'WARNING: Too small timestep %.16f reached '\
654                          %timestep
655                    msg += 'even after %d steps of 1 order scheme'\
656                           %self.max_smallsteps
657                    print msg
658                    timestep = min_timestep  #Try enforcing min_step
659
660                    #raise msg
661                else:
662                    #Try to overcome situation by switching to 1 order
663                    self.order = 1
664
665        else:
666            self.smallsteps = 0
667            if self.order == 1 and self.default_order == 2:
668                self.order = 2
669
670
671        #Ensure that final time is not exceeded
672        if finaltime is not None and self.time + timestep > finaltime:
673            timestep = finaltime-self.time
674
675        #Ensure that model time is aligned with yieldsteps
676        if self.yieldtime + timestep > yieldstep:
677            timestep = yieldstep-self.yieldtime
678
679        self.timestep = timestep
680
681
682
683    def compute_forcing_terms(self):
684        """If there are any forcing functions driving the system
685        they should be defined in Domain subclass and appended to
686        the list self.forcing_terms
687        """
688
689        for f in self.forcing_terms:
690            f(self)
691
692
693
694    def update_conserved_quantities(self):
695        """Update vectors of conserved quantities using previously
696        computed fluxes specified forcing functions.
697        """
698
699        from Numeric import ones, sum, equal, Float
700
701        N = self.number_of_elements
702        d = len(self.conserved_quantities)
703
704        timestep = self.timestep
705
706        #Compute forcing terms
707        self.compute_forcing_terms()
708
709        #Update conserved_quantities
710        for name in self.conserved_quantities:
711            Q = self.quantities[name]
712            Q.update(timestep)
713
714            #Clean up
715            #Note that Q.explicit_update is reset by compute_fluxes
716
717            #MH090605 commented out the following since semi_implicit_update is now re-initialized
718            #at the end of the _update function in quantity_ext.c (This is called by the
719            #preceeding Q.update(timestep) statement above).
720            #For run_profile.py with N=128, the time of update_conserved_quantities is cut from 14.00 secs
721            #to 8.35 secs
722
723            #Q.semi_implicit_update[:] = 0.0
724
725    def update_ghosts(self):
726        pass
727
728    def distribute_to_vertices_and_edges(self):
729        """Extrapolate conserved quantities from centroid to
730        vertices and edge-midpoints for each volume
731
732        Default implementation is straight first order,
733        i.e. constant values throughout each element and
734        no reference to non-conserved quantities.
735        """
736
737        for name in self.conserved_quantities:
738            Q = self.quantities[name]
739            if self.order == 1:
740                Q.extrapolate_first_order()
741            elif self.order == 2:
742                Q.extrapolate_second_order()
743                Q.limit()
744            else:
745                raise 'Unknown order'
746            Q.interpolate_from_vertices_to_edges()
747
748
749
750
751
752
753##############################################
754#Initialise module
755
756#Optimisation with psyco
757from config import use_psyco
758if use_psyco:
759    try:
760        import psyco
761    except:
762        import os
763        if os.name == 'posix' and os.uname()[4] == 'x86_64':
764            pass
765            #Psyco isn't supported on 64 bit systems, but it doesn't matter
766        else:
767            msg = 'WARNING: psyco (speedup) could not import'+\
768                  ', you may want to consider installing it'
769            print msg
770    else:
771        psyco.bind(Domain.update_boundary)
772        #psyco.bind(Domain.update_timestep)     #Not worth it
773        psyco.bind(Domain.update_conserved_quantities)
774        psyco.bind(Domain.distribute_to_vertices_and_edges)
775
776
777if __name__ == "__main__":
778    pass
Note: See TracBrowser for help on using the repository browser.