source: anuga_core/source/anuga/abstract_2d_finite_volumes/domain.py @ 4827

Last change on this file since 4827 was 4827, checked in by ole, 16 years ago

Better timestepping statistics including derived quantities such as velocity and Froude numbers.

File size: 52.2 KB
Line 
1"""Class Domain - 2D triangular domains for finite-volume computations of
2   conservation laws.
3
4
5   Copyright 2004
6   Ole Nielsen, Stephen Roberts, Duncan Gray
7   Geoscience Australia
8"""
9
10from Numeric import allclose, argmax
11from anuga.config import epsilon
12
13from anuga.abstract_2d_finite_volumes.neighbour_mesh import Mesh
14from anuga.abstract_2d_finite_volumes.generic_boundary_conditions\
15     import Boundary
16from anuga.abstract_2d_finite_volumes.generic_boundary_conditions\
17     import File_boundary
18from anuga.abstract_2d_finite_volumes.generic_boundary_conditions\
19     import Dirichlet_boundary
20from anuga.abstract_2d_finite_volumes.generic_boundary_conditions\
21     import Time_boundary
22from anuga.abstract_2d_finite_volumes.generic_boundary_conditions\
23     import Transmissive_boundary
24
25from anuga.abstract_2d_finite_volumes.pmesh2domain import pmesh_to_domain
26from anuga.abstract_2d_finite_volumes.region\
27     import Set_region as region_set_region
28
29from anuga.utilities.polygon import inside_polygon
30from anuga.abstract_2d_finite_volumes.util import get_textual_float
31
32import types
33
34
35
36class Domain(Mesh):
37
38
39    def __init__(self,
40                 source=None,
41                 triangles=None,
42                 boundary=None,
43                 conserved_quantities=None,
44                 other_quantities=None,
45                 tagged_elements=None,
46                 geo_reference=None,
47                 use_inscribed_circle=False,
48                 mesh_filename=None,
49                 use_cache=False,
50                 verbose=False,
51                 full_send_dict=None,
52                 ghost_recv_dict=None,
53                 processor=0,
54                 numproc=1,
55                 number_of_full_nodes=None,
56                 number_of_full_triangles=None):
57
58
59        """Instantiate generic computational Domain.
60
61        Input:
62          source:    Either a mesh filename or coordinates of mesh vertices.
63                     If it is a filename values specified for triangles will
64                     be overridden.
65          triangles: Mesh connectivity (see mesh.py for more information)
66          boundary:  See mesh.py for more information
67
68          conserved_quantities: List of quantity names entering the
69                                conservation equations
70          other_quantities:     List of other quantity names
71
72          tagged_elements:
73          ...
74
75
76        """
77
78        # Determine whether source is a mesh filename or coordinates
79        if type(source) == types.StringType:
80            mesh_filename = source
81        else:
82            coordinates = source
83
84
85        # In case a filename has been specified, extract content
86        if mesh_filename is not None:
87            coordinates, triangles, boundary, vertex_quantity_dict, \
88                         tagged_elements, geo_reference = \
89                         pmesh_to_domain(file_name=mesh_filename,
90                                         use_cache=use_cache,
91                                         verbose=verbose)
92
93
94        # Initialise underlying mesh structure
95        Mesh.__init__(self, coordinates, triangles,
96                      boundary=boundary,
97                      tagged_elements=tagged_elements,
98                      geo_reference=geo_reference,
99                      use_inscribed_circle=use_inscribed_circle,
100                      number_of_full_nodes=number_of_full_nodes,
101                      number_of_full_triangles=number_of_full_triangles,
102                      verbose=verbose)
103
104        if verbose: print 'Initialising Domain'
105        from Numeric import zeros, Float, Int, ones
106        from quantity import Quantity, Conserved_quantity
107
108        # List of quantity names entering
109        # the conservation equations
110        if conserved_quantities is None:
111            self.conserved_quantities = []
112        else:
113            self.conserved_quantities = conserved_quantities
114
115        # List of other quantity names
116        if other_quantities is None:
117            self.other_quantities = []
118        else:
119            self.other_quantities = other_quantities
120
121
122        #Build dictionary of Quantity instances keyed by quantity names
123        self.quantities = {}
124
125        #FIXME: remove later - maybe OK, though....
126        for name in self.conserved_quantities:
127            self.quantities[name] = Conserved_quantity(self)
128        for name in self.other_quantities:
129            self.quantities[name] = Quantity(self)
130
131        #Create an empty list for explicit forcing terms
132        self.forcing_terms = []
133
134        #Setup the ghost cell communication
135        if full_send_dict is None:
136            self.full_send_dict = {}
137        else:
138            self.full_send_dict  = full_send_dict
139
140        # List of other quantity names
141        if ghost_recv_dict is None:
142            self.ghost_recv_dict = {}
143        else:
144            self.ghost_recv_dict = ghost_recv_dict
145
146        self.processor = processor
147        self.numproc = numproc
148
149
150        # Setup Communication Buffers
151        if verbose: print 'Domain: Set up communication buffers (parallel)'
152        self.nsys = len(self.conserved_quantities)
153        for key in self.full_send_dict:
154            buffer_shape = self.full_send_dict[key][0].shape[0]
155            self.full_send_dict[key].append(zeros( (buffer_shape,self.nsys) ,Float))
156
157
158        for key in self.ghost_recv_dict:
159            buffer_shape = self.ghost_recv_dict[key][0].shape[0]
160            self.ghost_recv_dict[key].append(zeros( (buffer_shape,self.nsys) ,Float))
161
162
163        # Setup cell full flag
164        # =1 for full
165        # =0 for ghost
166        N = len(self) #number_of_elements
167        self.tri_full_flag = ones(N, Int)
168        for i in self.ghost_recv_dict.keys():
169            for id in self.ghost_recv_dict[i][0]:
170                self.tri_full_flag[id] = 0
171
172        # Test the assumption that all full triangles are store before
173        # the ghost triangles.
174        assert allclose(self.tri_full_flag[:self.number_of_full_nodes],1)
175                       
176
177        # Defaults
178        from anuga.config import max_smallsteps, beta_w, beta_h, epsilon
179        from anuga.config import CFL
180        from anuga.config import timestepping_method
181        from anuga.config import protect_against_isolated_degenerate_timesteps
182        self.beta_w = beta_w
183        self.beta_h = beta_h
184        self.epsilon = epsilon
185        self.protect_against_isolated_degenerate_timesteps = protect_against_isolated_degenerate_timesteps
186       
187       
188
189        # FIXME: Maybe have separate orders for h-limiter and w-limiter?
190        # Or maybe get rid of order altogether and use beta_w and beta_h
191        self.set_default_order(1)
192
193        self.smallsteps = 0
194        self.max_smallsteps = max_smallsteps
195        self.number_of_steps = 0
196        self.number_of_first_order_steps = 0
197        self.CFL = CFL
198        self.set_timestepping_method(timestepping_method)
199       
200        self.boundary_map = None  # Will be populated by set_boundary       
201       
202
203        # Model time
204        self.time = 0.0
205        self.finaltime = None
206        self.min_timestep = self.max_timestep = 0.0
207        self.starttime = 0 # Physical starttime if any
208                           # (0 is 1 Jan 1970 00:00:00)
209        self.timestep = 0.0
210        self.flux_timestep = 0.0
211
212        # Monitoring
213        self.quantities_to_be_monitored = None
214        self.monitor_polygon = None
215        self.monitor_time_interval = None               
216        self.monitor_indices = None
217
218        # Checkpointing and storage
219        from anuga.config import default_datadir
220        self.datadir = default_datadir
221        self.simulation_name = 'domain'
222        self.checkpoint = False
223
224        # To avoid calculating the flux across each edge twice,
225        # keep an integer (boolean) array, to be used during the flux
226        # calculation
227        N = len(self) # Number_of_triangles
228        self.already_computed_flux = zeros((N, 3), Int)
229
230        # Storage for maximal speeds computed for each triangle by
231        # compute_fluxes
232        # This is used for diagnostics only
233        self.max_speed = zeros(N, Float)
234
235        if mesh_filename is not None:
236            # If the mesh file passed any quantity values
237            # , initialise with these values.
238            if verbose: print 'Domain: Initialising quantity values'
239            self.set_quantity_vertices_dict(vertex_quantity_dict)
240
241
242        if verbose: print 'Domain: Done'
243
244
245
246
247
248
249    #---------------------------   
250    # Public interface to Domain
251    #---------------------------       
252    def get_conserved_quantities(self, vol_id, vertex=None, edge=None):
253        """Get conserved quantities at volume vol_id
254
255        If vertex is specified use it as index for vertex values
256        If edge is specified use it as index for edge values
257        If neither are specified use centroid values
258        If both are specified an exeception is raised
259
260        Return value: Vector of length == number_of_conserved quantities
261
262        """
263
264        from Numeric import zeros, Float
265
266        if not (vertex is None or edge is None):
267            msg = 'Values for both vertex and edge was specified.'
268            msg += 'Only one (or none) is allowed.'
269            raise msg
270
271        q = zeros( len(self.conserved_quantities), Float)
272
273        for i, name in enumerate(self.conserved_quantities):
274            Q = self.quantities[name]
275            if vertex is not None:
276                q[i] = Q.vertex_values[vol_id, vertex]
277            elif edge is not None:
278                q[i] = Q.edge_values[vol_id, edge]
279            else:
280                q[i] = Q.centroid_values[vol_id]
281
282        return q
283
284    def set_time(self, time=0.0):
285        """Set the model time (seconds)"""
286
287        self.time = time
288
289    def get_time(self):
290        """Get the model time (seconds)"""
291
292        return self.time
293
294    def set_default_order(self, n):
295        """Set default (spatial) order to either 1 or 2
296        """
297
298        msg = 'Default order must be either 1 or 2. I got %s' %n
299        assert n in [1,2], msg
300
301        self.default_order = n
302        self._order_ = self.default_order
303       
304
305    def set_quantity_vertices_dict(self, quantity_dict):
306        """Set values for named quantities.
307        The index is the quantity
308
309        name: Name of quantity
310        X: Compatible list, Numeric array, const or function (see below)
311
312        The values will be stored in elements following their
313        internal ordering.
314
315        """
316       
317        # FIXME: Could we name this a bit more intuitively
318        # E.g. set_quantities_from_dictionary
319        for key in quantity_dict.keys():
320            self.set_quantity(key, quantity_dict[key], location='vertices')
321
322
323    def set_quantity(self, name, *args, **kwargs):
324        """Set values for named quantity
325
326
327        One keyword argument is documented here:
328        expression = None, # Arbitrary expression
329
330        expression:
331          Arbitrary expression involving quantity names
332
333        See Quantity.set_values for further documentation.
334        """
335
336        # Do the expression stuff
337        if kwargs.has_key('expression'):
338            expression = kwargs['expression']
339            del kwargs['expression']
340
341            Q = self.create_quantity_from_expression(expression)
342            kwargs['quantity'] = Q
343
344        # Assign values
345        self.quantities[name].set_values(*args, **kwargs)
346
347
348    def get_quantity_names(self):
349        """Get a list of all the quantity names that this domain is aware of.
350        Any value in the result should be a valid input to get_quantity.
351        """
352        return self.quantities.keys()
353
354    def get_quantity(self, name, location='vertices', indices = None):
355        """Get quantity object.
356
357        name: Name of quantity
358
359        See methods inside the quantity object for more options
360
361        FIXME: clean input args
362        """
363
364        return self.quantities[name] #.get_values( location, indices = indices)
365
366
367
368    def create_quantity_from_expression(self, expression):
369        """Create new quantity from other quantities using arbitrary expression
370
371        Combine existing quantities in domain using expression and return
372        result as a new quantity.
373
374        Note, the new quantity could e.g. be used in set_quantity
375
376        Valid expressions are limited to operators defined in class Quantity
377
378        Examples creating derived quantities:
379
380            Depth = domain.create_quantity_from_expression('stage-elevation')
381
382            exp = '(xmomentum*xmomentum + ymomentum*ymomentum)**0.5')       
383            Absolute_momentum = domain.create_quantity_from_expression(exp)
384
385        """
386
387        from anuga.abstract_2d_finite_volumes.util import\
388             apply_expression_to_dictionary
389       
390        return apply_expression_to_dictionary(expression, self.quantities)
391
392
393
394    def set_boundary(self, boundary_map):
395        """Associate boundary objects with tagged boundary segments.
396
397        Input boundary_map is a dictionary of boundary objects keyed
398        by symbolic tags to matched against tags in the internal dictionary
399        self.boundary.
400
401        As result one pointer to a boundary object is stored for each vertex
402        in the list self.boundary_objects.
403        More entries may point to the same boundary object
404
405        Schematically the mapping is from two dictionaries to one list
406        where the index is used as pointer to the boundary_values arrays
407        within each quantity.
408
409        self.boundary:          (vol_id, edge_id): tag
410        boundary_map (input):   tag: boundary_object
411        ----------------------------------------------
412        self.boundary_objects:  ((vol_id, edge_id), boundary_object)
413
414
415        Pre-condition:
416          self.boundary has been built.
417
418        Post-condition:
419          self.boundary_objects is built
420
421        If a tag from the domain doesn't appear in the input dictionary an
422        exception is raised.
423        However, if a tag is not used to the domain, no error is thrown.
424        FIXME: This would lead to implementation of a
425        default boundary condition
426
427        Note: If a segment is listed in the boundary dictionary and if it is
428        not None, it *will* become a boundary -
429        even if there is a neighbouring triangle.
430        This would be the case for internal boundaries
431
432        Boundary objects that are None will be skipped.
433
434        If a boundary_map has already been set
435        (i.e. set_boundary has been called before), the old boundary map
436        will be updated with new values. The new map need not define all
437        boundary tags, and can thus change only those that are needed.
438
439        FIXME: If set_boundary is called multiple times and if Boundary
440        object is changed into None, the neighbour structure will not be
441        restored!!!
442
443
444        """
445
446        if self.boundary_map is None:
447            # This the first call to set_boundary. Store
448            # map for later updates and for use with boundary_stats.
449            self.boundary_map = boundary_map
450        else:   
451            # This is a modification of an already existing map
452            # Update map an proceed normally
453
454            for key in boundary_map.keys():
455                self.boundary_map[key] = boundary_map[key]
456               
457       
458        # FIXME (Ole): Try to remove the sorting and fix test_mesh.py
459        x = self.boundary.keys()
460        x.sort()
461
462        # Loop through edges that lie on the boundary and associate them with
463        # callable boundary objects depending on their tags
464        self.boundary_objects = []       
465        for k, (vol_id, edge_id) in enumerate(x):
466            tag = self.boundary[ (vol_id, edge_id) ]
467
468            if self.boundary_map.has_key(tag):
469                B = self.boundary_map[tag]  # Get callable boundary object
470
471                if B is not None:
472                    self.boundary_objects.append( ((vol_id, edge_id), B) )
473                    self.neighbours[vol_id, edge_id] = \
474                                            -len(self.boundary_objects)
475                else:
476                    pass
477                    #FIXME: Check and perhaps fix neighbour structure
478
479
480            else:
481                msg = 'ERROR (domain.py): Tag "%s" has not been ' %tag
482                msg += 'bound to a boundary object.\n'
483                msg += 'All boundary tags defined in domain must appear '
484                msg += 'in set_boundary.\n'
485                msg += 'The tags are: %s' %self.get_boundary_tags()
486                raise msg
487
488
489    def set_region(self, *args, **kwargs):
490        """
491        This method is used to set quantities based on a regional tag.
492       
493        It is most often called with the following parameters;
494        (self, tag, quantity, X, location='vertices')
495        tag: the name of the regional tag used to specify the region
496        quantity: Name of quantity to change
497        X: const or function - how the quantity is changed
498        location: Where values are to be stored.
499            Permissible options are: vertices, centroid and unique vertices
500
501        A callable region class or a list of callable region classes
502        can also be passed into this function.
503        """
504
505        if len(args) == 1:
506            self._set_region(*args, **kwargs)
507        else:
508            # Assume it is arguments for the region.set_region function
509            func = region_set_region(*args, **kwargs)
510            self._set_region(func)
511           
512       
513    def _set_region(self, functions):
514        # The order of functions in the list is used.
515        if type(functions) not in [types.ListType,types.TupleType]:
516            functions = [functions]
517        for function in functions:
518            for tag in self.tagged_elements.keys():
519                function(tag, self.tagged_elements[tag], self)
520
521
522
523
524    def set_quantities_to_be_monitored(self, q,
525                                       polygon=None,
526                                       time_interval=None):
527        """Specify which quantities will be monitored for extrema.
528
529        q must be either:
530          - the name of a quantity or a derived quantity such as 'stage-elevation'
531          - a list of quantity names
532          - None
533
534        In the two first cases, the named quantities will be monitored at
535        each internal timestep
536       
537        If q is None, monitoring will be switched off altogether.
538
539        polygon (if specified) will restrict monitoring to triangles inside polygon.
540        If omitted all triangles will be included.
541
542        time_interval (if specified) will restrict monitoring to time steps in
543        that interval. If omitted all timesteps will be included.
544        """
545
546        from anuga.abstract_2d_finite_volumes.util import\
547             apply_expression_to_dictionary       
548
549        if q is None:
550            self.quantities_to_be_monitored = None
551            self.monitor_polygon = None
552            self.monitor_time_interval = None
553            self.monitor_indices = None           
554            return
555
556        if isinstance(q, basestring):
557            q = [q] # Turn argument into a list
558
559        # Check correcness and initialise
560        self.quantities_to_be_monitored = {}
561        for quantity_name in q:
562            msg = 'Quantity %s is not a valid conserved quantity'\
563                  %quantity_name
564           
565
566            if not quantity_name in self.quantities:
567                # See if this expression is valid
568                apply_expression_to_dictionary(quantity_name, self.quantities)
569
570            # Initialise extrema information
571            info_block = {'min': None,          # Min value
572                          'max': None,          # Max value
573                          'min_location': None, # Argmin (x, y)
574                          'max_location': None, # Argmax (x, y)
575                          'min_time': None,     # Argmin (t)
576                          'max_time': None}     # Argmax (t)
577           
578            self.quantities_to_be_monitored[quantity_name] = info_block
579
580           
581
582        if polygon is not None:
583            # Check input
584            if isinstance(polygon, basestring):
585
586                # Check if multiple quantities were accidentally
587                # given as separate argument rather than a list.
588                msg = 'Multiple quantities must be specified in a list. '
589                msg += 'Not as multiple arguments. '
590                msg += 'I got "%s" as a second argument' %polygon
591               
592                if polygon in self.quantities:
593                    raise Exception, msg
594               
595                try:
596                    apply_expression_to_dictionary(polygon, self.quantities)
597                except:
598                    # At least polygon wasn't an expression involving quantitites
599                    pass
600                else:
601                    raise Exception, msg
602
603                # In any case, we don't allow polygon to be a string
604                msg = 'argument "polygon" must not be a string: '
605                msg += 'I got polygon=\'%s\' ' %polygon
606                raise Exception, msg
607
608
609            # Get indices for centroids that are inside polygon
610            points = self.get_centroid_coordinates(absolute=True)
611            self.monitor_indices = inside_polygon(points, polygon)
612           
613
614        if time_interval is not None:
615            assert len(time_interval) == 2
616
617       
618        self.monitor_polygon = polygon
619        self.monitor_time_interval = time_interval       
620       
621
622    #--------------------------   
623    # Miscellaneous diagnostics
624    #--------------------------       
625    def check_integrity(self):
626        Mesh.check_integrity(self)
627
628        for quantity in self.conserved_quantities:
629            msg = 'Conserved quantities must be a subset of all quantities'
630            assert quantity in self.quantities, msg
631
632        ##assert hasattr(self, 'boundary_objects')
633           
634
635    def write_time(self, track_speeds=False):
636        print self.timestepping_statistics(track_speeds)
637
638
639    def timestepping_statistics(self, track_speeds=False):
640        """Return string with time stepping statistics for printing or logging
641
642        Optional boolean keyword track_speeds decides whether to report
643        location of smallest timestep as well as a histogram and percentile
644        report.
645        """
646
647        from anuga.utilities.numerical_tools import histogram, create_bins
648       
649
650        # qwidth determines the text field used for quantities
651        qwidth = self.qwidth = 12
652
653
654        msg = ''
655        if self.min_timestep == self.max_timestep:
656            msg += 'Time = %.4f, delta t = %.8f, steps=%d (%d)'\
657                   %(self.time, self.min_timestep, self.number_of_steps,
658                     self.number_of_first_order_steps)
659        elif self.min_timestep > self.max_timestep:
660            msg += 'Time = %.4f, steps=%d (%d)'\
661                   %(self.time, self.number_of_steps,
662                     self.number_of_first_order_steps)
663        else:
664            msg += 'Time = %.4f, delta t in [%.8f, %.8f], steps=%d (%d)'\
665                   %(self.time, self.min_timestep,
666                     self.max_timestep, self.number_of_steps,
667                     self.number_of_first_order_steps)
668
669        if track_speeds is True:
670            msg += '\n'
671
672
673            # Setup 10 bins for speed histogram
674            bins = create_bins(self.max_speed, 10)
675            hist = histogram(self.max_speed, bins)
676
677            msg += '------------------------------------------------\n'
678            msg += '  Speeds in [%f, %f]\n' %(min(self.max_speed),
679                                              max(self.max_speed))
680            msg += '  Histogram:\n'
681
682            hi = bins[0]
683            for i, count in enumerate(hist):
684                lo = hi
685                if i+1 < len(bins):
686                    # Open upper interval               
687                    hi = bins[i+1]
688                    msg += '    [%f, %f[: %d\n' %(lo, hi, count)               
689                else:
690                    # Closed upper interval
691                    hi = max(self.max_speed)
692                    msg += '    [%f, %f]: %d\n' %(lo, hi, count)
693
694
695            N = len(self.max_speed)
696            if N > 10:
697                msg += '  Percentiles (10%):\n'
698                speed = self.max_speed.tolist()
699                speed.sort()
700
701                k = 0
702                lower = min(speed)
703                for i, a in enumerate(speed):       
704                    if i % (N/10) == 0 and i != 0:
705                        # For every 10% of the sorted speeds
706                        msg += '    %d speeds in [%f, %f]\n' %(i-k, lower, a)
707                        lower = a
708                        k = i
709                       
710                msg += '    %d speeds in [%f, %f]\n'\
711                       %(N-k, lower, max(speed))                   
712               
713                     
714
715               
716           
717            # Find index of largest computed flux speed
718            k = self.k = argmax(self.max_speed)
719
720            x, y = self.get_centroid_coordinates()[k]
721            radius = self.get_radii()[k]
722            area = self.get_areas()[k]     
723            max_speed = self.max_speed[k]           
724
725            msg += '  Triangle #%d with centroid (%.4f, %.4f), ' %(k, x, y)
726            msg += 'area = %.4f and radius = %.4f ' %(area, radius)
727            msg += 'had the largest computed speed: %.6f m/s ' %(max_speed)
728            if max_speed > 0.0:
729                msg += '(timestep=%.6f)\n' %(radius/max_speed)
730            else:
731                msg += '(timestep=%.6f)\n' %(0)     
732           
733            # Report all quantity values at vertices, edges and centroid
734            msg += '    Quantity'
735            msg += '------------'
736            for name in self.quantities:
737                q = self.quantities[name]
738
739                V = q.get_values(location='vertices', indices=[k])[0]
740                E = q.get_values(location='edges', indices=[k])[0]
741                C = q.get_values(location='centroids', indices=[k])                 
742               
743                s  = '    %s: vertex_values =  %.4f,\t %.4f,\t %.4f\n'\
744                     %(name.ljust(qwidth), V[0], V[1], V[2])
745
746                s += '    %s: edge_values =    %.4f,\t %.4f,\t %.4f\n'\
747                     %(name.ljust(qwidth), E[0], E[1], E[2])
748
749                s += '    %s: centroid_value = %.4f\n'\
750                     %(name.ljust(qwidth), C[0])                               
751
752                msg += s
753
754        return msg
755
756
757    def write_boundary_statistics(self, quantities = None, tags = None):
758        print self.boundary_statistics(quantities, tags)
759
760    def boundary_statistics(self, quantities = None, tags = None):
761        """Output statistics about boundary forcing at each timestep
762
763
764        Input:
765          quantities: either None, a string or a list of strings naming the
766                      quantities to be reported
767          tags:       either None, a string or a list of strings naming the
768                      tags to be reported
769
770
771        Example output:
772        Tag 'wall':
773            stage in [2, 5.5]
774            xmomentum in []
775            ymomentum in []
776        Tag 'ocean'
777
778
779        If quantities are specified only report on those. Otherwise take all
780        conserved quantities.
781        If tags are specified only report on those, otherwise take all tags.
782
783        """
784
785        # Input checks
786        import types, string
787
788        if quantities is None:
789            quantities = self.conserved_quantities
790        elif type(quantities) == types.StringType:
791            quantities = [quantities] #Turn it into a list
792
793        msg = 'Keyword argument quantities must be either None, '
794        msg += 'string or list. I got %s' %str(quantities)
795        assert type(quantities) == types.ListType, msg
796
797
798        if tags is None:
799            tags = self.get_boundary_tags()
800        elif type(tags) == types.StringType:
801            tags = [tags] #Turn it into a list
802
803        msg = 'Keyword argument tags must be either None, '
804        msg += 'string or list. I got %s' %str(tags)
805        assert type(tags) == types.ListType, msg
806
807        # Determine width of longest quantity name (for cosmetic purposes)
808        maxwidth = 0
809        for name in quantities:
810            w = len(name)
811            if w > maxwidth:
812                maxwidth = w
813
814        # Output statistics
815        msg = 'Boundary values at time %.4f:\n' %self.time
816        for tag in tags:
817            msg += '    %s:\n' %tag
818
819            for name in quantities:
820                q = self.quantities[name]
821
822                # Find range of boundary values for tag and q
823                maxval = minval = None
824                for i, ((vol_id, edge_id), B) in\
825                        enumerate(self.boundary_objects):
826                    if self.boundary[(vol_id, edge_id)] == tag:
827                        v = q.boundary_values[i]
828                        if minval is None or v < minval: minval = v
829                        if maxval is None or v > maxval: maxval = v
830
831                if minval is None or maxval is None:
832                    msg += '        Sorry no information available about' +\
833                           ' tag %s and quantity %s\n' %(tag, name)
834                else:
835                    msg += '        %s in [%12.8f, %12.8f]\n'\
836                           %(string.ljust(name, maxwidth), minval, maxval)
837
838
839        return msg
840
841
842    def update_extrema(self):
843        """Update extrema if requested by set_quantities_to_be_monitored.
844        This data is used for reporting e.g. by running
845        print domain.quantity_statistics()
846        and may also stored in output files (see data_manager in shallow_water)
847        """
848
849        # Define a tolerance for extremum computations
850        epsilon = 1.0e-6 # Import 'single_precision' from config
851       
852        if self.quantities_to_be_monitored is None:
853            return
854
855        # Observe time interval restriction if any
856        if self.monitor_time_interval is not None and\
857               (self.time < self.monitor_time_interval[0] or\
858               self.time > self.monitor_time_interval[1]):
859            return
860
861        # Update extrema for each specified quantity subject to
862        # polygon restriction (via monitor_indices).
863        for quantity_name in self.quantities_to_be_monitored:
864
865            if quantity_name in self.quantities:
866                Q = self.get_quantity(quantity_name)
867            else:
868                Q = self.create_quantity_from_expression(quantity_name)
869
870            info_block = self.quantities_to_be_monitored[quantity_name]
871
872            # Update maximum
873            # (n > None is always True, but we check explicitly because
874            # of the epsilon)
875            maxval = Q.get_maximum_value(self.monitor_indices)
876            if info_block['max'] is None or\
877                   maxval > info_block['max'] + epsilon:
878                info_block['max'] = maxval
879                maxloc = Q.get_maximum_location()
880                info_block['max_location'] = maxloc
881                info_block['max_time'] = self.time
882
883
884            # Update minimum
885            minval = Q.get_minimum_value(self.monitor_indices)
886            if info_block['min'] is None or\
887                   minval < info_block['min'] - epsilon:
888                info_block['min'] = minval               
889                minloc = Q.get_minimum_location()
890                info_block['min_location'] = minloc
891                info_block['min_time'] = self.time               
892       
893
894
895    def quantity_statistics(self, precision = '%.4f'):
896        """Return string with statistics about quantities for
897        printing or logging
898
899        Quantities reported are specified through method
900
901           set_quantities_to_be_monitored
902           
903        """
904
905        maxlen = 128 # Max length of polygon string representation
906
907        # Output statistics
908        msg = 'Monitored quantities at time %.4f:\n' %self.time
909        if self.monitor_polygon is not None:
910            p_str = str(self.monitor_polygon)
911            msg += '- Restricted by polygon: %s' %p_str[:maxlen]
912            if len(p_str) >= maxlen:
913                msg += '...\n'
914            else:
915                msg += '\n'
916
917
918        if self.monitor_time_interval is not None:
919            msg += '- Restricted by time interval: %s\n'\
920                   %str(self.monitor_time_interval)
921            time_interval_start = self.monitor_time_interval[0]
922        else:
923            time_interval_start = 0.0
924
925           
926        for quantity_name, info in self.quantities_to_be_monitored.items():
927            msg += '    %s:\n' %quantity_name
928
929            msg += '      values since time = %.2f in [%s, %s]\n'\
930                   %(time_interval_start,
931                     get_textual_float(info['min'], precision),
932                     get_textual_float(info['max'], precision))       
933                     
934            msg += '      minimum attained at time = %s, location = %s\n'\
935                   %(get_textual_float(info['min_time'], precision),
936                     get_textual_float(info['min_location'], precision))
937           
938
939            msg += '      maximum attained at time = %s, location = %s\n'\
940                   %(get_textual_float(info['max_time'], precision),
941                     get_textual_float(info['max_location'], precision))
942
943
944        return msg
945
946    def get_timestepping_method(self):
947        return self.timestepping_method
948
949    def set_timestepping_method(self,timestepping_method):
950       
951        if timestepping_method in ['euler', 'rk2', 'rk3']:
952            self.timestepping_method = timestepping_method
953            return
954
955        msg = '%s is an incorrect timestepping type'% timestepping_method
956        raise Exception, msg
957
958    def get_name(self):
959        return self.simulation_name
960
961    def set_name(self, name):
962        """Assign a name to this simulation.
963        This will be used to identify the output sww file.
964
965        """
966        if name.endswith('.sww'):
967            name = name[:-4]
968           
969        self.simulation_name = name
970
971    def get_datadir(self):
972        return self.datadir
973
974    def set_datadir(self, name):
975        self.datadir = name
976
977    def get_starttime(self):
978        return self.starttime
979
980    def set_starttime(self, time):
981        self.starttime = float(time)       
982
983
984
985    #--------------------------
986    # Main components of evolve
987    #--------------------------   
988
989    def evolve(self,
990               yieldstep = None,
991               finaltime = None,
992               duration = None,
993               skip_initial_step = False):
994        """Evolve model through time starting from self.starttime.
995
996
997        yieldstep: Interval between yields where results are stored,
998                   statistics written and domain inspected or
999                   possibly modified. If omitted the internal predefined
1000                   max timestep is used.
1001                   Internally, smaller timesteps may be taken.
1002
1003        duration: Duration of simulation
1004
1005        finaltime: Time where simulation should end. This is currently
1006        relative time.  So it's the same as duration.
1007
1008        If both duration and finaltime are given an exception is thrown.
1009
1010
1011        skip_initial_step: Boolean flag that decides whether the first
1012        yield step is skipped or not. This is useful for example to avoid
1013        duplicate steps when multiple evolve processes are dove tailed.
1014
1015
1016        Evolve is implemented as a generator and is to be called as such, e.g.
1017
1018        for t in domain.evolve(yieldstep, finaltime):
1019            <Do something with domain and t>
1020
1021
1022        All times are given in seconds
1023
1024        """
1025
1026        from anuga.config import min_timestep, max_timestep, epsilon
1027
1028        # FIXME: Maybe lump into a larger check prior to evolving
1029        msg = 'Boundary tags must be bound to boundary objects before '
1030        msg += 'evolving system, '
1031        msg += 'e.g. using the method set_boundary.\n'
1032        msg += 'This system has the boundary tags %s '\
1033               %self.get_boundary_tags()
1034        assert hasattr(self, 'boundary_objects'), msg
1035
1036
1037        if yieldstep is None:
1038            yieldstep = max_timestep
1039        else:
1040            yieldstep = float(yieldstep)
1041
1042        self._order_ = self.default_order
1043
1044
1045        if finaltime is not None and duration is not None:
1046            # print 'F', finaltime, duration
1047            msg = 'Only one of finaltime and duration may be specified'
1048            raise msg
1049        else:
1050            if finaltime is not None:
1051                self.finaltime = float(finaltime)
1052            if duration is not None:
1053                self.finaltime = self.starttime + float(duration)
1054
1055
1056
1057
1058        self.yieldtime = 0.0 # Track time between 'yields'
1059
1060        # Initialise interval of timestep sizes (for reporting only)
1061        self.min_timestep = max_timestep
1062        self.max_timestep = min_timestep
1063        self.number_of_steps = 0
1064        self.number_of_first_order_steps = 0
1065
1066        # Update ghosts
1067        self.update_ghosts()
1068
1069        # Initial update of vertex and edge values
1070        self.distribute_to_vertices_and_edges()
1071
1072        # Update extrema if necessary (for reporting)
1073        self.update_extrema()
1074       
1075        # Initial update boundary values
1076        self.update_boundary()
1077
1078        # Or maybe restore from latest checkpoint
1079        if self.checkpoint is True:
1080            self.goto_latest_checkpoint()
1081
1082        if skip_initial_step is False:
1083            yield(self.time)  # Yield initial values
1084
1085        while True:
1086
1087            # Evolve One Step, using appropriate timestepping method
1088            if self.get_timestepping_method() == 'euler':
1089                self.evolve_one_euler_step(yieldstep,finaltime)
1090               
1091            elif self.get_timestepping_method() == 'rk2':
1092                self.evolve_one_rk2_step(yieldstep,finaltime)
1093
1094            elif self.get_timestepping_method() == 'rk3':
1095                self.evolve_one_rk3_step(yieldstep,finaltime)               
1096           
1097            # Update extrema if necessary (for reporting)
1098            self.update_extrema()           
1099
1100
1101            self.yieldtime += self.timestep
1102            self.number_of_steps += 1
1103            if self._order_ == 1:
1104                self.number_of_first_order_steps += 1
1105
1106            # Yield results
1107            if finaltime is not None and self.time >= finaltime-epsilon:
1108
1109                if self.time > finaltime:
1110                    # FIXME (Ole, 30 April 2006): Do we need this check?
1111                    # Probably not (Ole, 18 September 2008). Now changed to
1112                    # Exception
1113                    msg = 'WARNING (domain.py): time overshot finaltime. '
1114                    msg += 'Contact Ole.Nielsen@ga.gov.au'
1115                    raise Exception, msg
1116                   
1117
1118                # Yield final time and stop
1119                self.time = finaltime
1120                yield(self.time)
1121                break
1122
1123
1124            if self.yieldtime >= yieldstep:
1125                # Yield (intermediate) time and allow inspection of domain
1126
1127                if self.checkpoint is True:
1128                    self.store_checkpoint()
1129                    self.delete_old_checkpoints()
1130
1131                # Pass control on to outer loop for more specific actions
1132                yield(self.time)
1133
1134                # Reinitialise
1135                self.yieldtime = 0.0
1136                self.min_timestep = max_timestep
1137                self.max_timestep = min_timestep
1138                self.number_of_steps = 0
1139                self.number_of_first_order_steps = 0
1140
1141
1142    def evolve_one_euler_step(self, yieldstep, finaltime):
1143        """
1144        One Euler Time Step
1145        Q^{n+1} = E(h) Q^n
1146        """
1147
1148        #Compute fluxes across each element edge
1149        self.compute_fluxes()
1150
1151        #Update timestep to fit yieldstep and finaltime
1152        self.update_timestep(yieldstep, finaltime)
1153
1154        #Update conserved quantities
1155        self.update_conserved_quantities()
1156
1157        #update ghosts
1158        self.update_ghosts()
1159
1160        #Update vertex and edge values
1161        self.distribute_to_vertices_and_edges()
1162
1163        #Update boundary values
1164        self.update_boundary()
1165
1166        #Update time
1167        self.time += self.timestep
1168
1169       
1170
1171
1172    def evolve_one_rk2_step(self, yieldstep, finaltime):
1173        """
1174        One 2nd order RK timestep
1175        Q^{n+1} = 0.5 Q^n + 0.5 E(h)^2 Q^n
1176        """
1177
1178        #Save initial initial conserved quantities values
1179        self.backup_conserved_quantities()           
1180
1181        #--------------------------------------
1182        #First euler step
1183        #--------------------------------------
1184
1185        #Compute fluxes across each element edge
1186        self.compute_fluxes()
1187
1188        #Update timestep to fit yieldstep and finaltime
1189        self.update_timestep(yieldstep, finaltime)
1190
1191        #Update conserved quantities
1192        self.update_conserved_quantities()
1193
1194        #update ghosts
1195        self.update_ghosts()
1196
1197        #Update vertex and edge values
1198        self.distribute_to_vertices_and_edges()
1199
1200        #Update boundary values
1201        self.update_boundary()
1202
1203        #Update time
1204        self.time += self.timestep
1205
1206        #------------------------------------
1207        #Second Euler step
1208        #------------------------------------
1209           
1210        #Compute fluxes across each element edge
1211        self.compute_fluxes()
1212
1213        #Update conserved quantities
1214        self.update_conserved_quantities()
1215
1216        #------------------------------------
1217        #Combine initial and final values
1218        #of conserved quantities and cleanup
1219        #------------------------------------
1220        #combine steps
1221        self.saxpy_conserved_quantities(0.5, 0.5)
1222 
1223        #update ghosts
1224        self.update_ghosts()
1225
1226        #Update vertex and edge values
1227        self.distribute_to_vertices_and_edges()
1228
1229        #Update boundary values
1230        self.update_boundary()
1231
1232
1233
1234    def evolve_one_rk3_step(self, yieldstep, finaltime):
1235        """
1236        One 3rd order RK timestep
1237        Q^(1) = 3/4 Q^n + 1/4 E(h)^2 Q^n  (at time t^n + h/2)
1238        Q^{n+1} = 1/3 Q^n + 2/3 E(h) Q^(1) (at time t^{n+1})
1239        """
1240
1241        #Save initial initial conserved quantities values
1242        self.backup_conserved_quantities()           
1243
1244        initial_time = self.time
1245       
1246        #--------------------------------------
1247        #First euler step
1248        #--------------------------------------
1249
1250        #Compute fluxes across each element edge
1251        self.compute_fluxes()
1252
1253        #Update timestep to fit yieldstep and finaltime
1254        self.update_timestep(yieldstep, finaltime)
1255
1256        #Update conserved quantities
1257        self.update_conserved_quantities()
1258
1259        #update ghosts
1260        self.update_ghosts()
1261
1262        #Update vertex and edge values
1263        self.distribute_to_vertices_and_edges()
1264
1265        #Update boundary values
1266        self.update_boundary()
1267
1268        #Update time
1269        self.time += self.timestep
1270
1271        #------------------------------------
1272        #Second Euler step
1273        #------------------------------------
1274           
1275        #Compute fluxes across each element edge
1276        self.compute_fluxes()
1277
1278        #Update conserved quantities
1279        self.update_conserved_quantities()
1280
1281        #------------------------------------
1282        #Combine steps to obtain intermediate
1283        #solution at time t^n + 0.5 h
1284        #------------------------------------
1285
1286        #combine steps
1287        self.saxpy_conserved_quantities(0.25, 0.75)
1288 
1289        #update ghosts
1290        self.update_ghosts()
1291
1292        #Update vertex and edge values
1293        self.distribute_to_vertices_and_edges()
1294
1295        #Update boundary values
1296        self.update_boundary()
1297
1298        #set substep time
1299        self.time = initial_time + self.timestep*0.5
1300
1301        #------------------------------------
1302        #Third Euler step
1303        #------------------------------------
1304           
1305        #Compute fluxes across each element edge
1306        self.compute_fluxes()
1307
1308        #Update conserved quantities
1309        self.update_conserved_quantities()
1310
1311        #------------------------------------
1312        #Combine final and initial values
1313        #and cleanup
1314        #------------------------------------
1315        #combine steps
1316        self.saxpy_conserved_quantities(2.0/3.0, 1.0/3.0)
1317 
1318        #update ghosts
1319        self.update_ghosts()
1320
1321        #Update vertex and edge values
1322        self.distribute_to_vertices_and_edges()
1323
1324        #Update boundary values
1325        self.update_boundary()
1326
1327        #set new time
1328        self.time = initial_time + self.timestep       
1329       
1330
1331    def evolve_to_end(self, finaltime = 1.0):
1332        """Iterate evolve all the way to the end
1333        """
1334
1335        for _ in self.evolve(yieldstep=None, finaltime=finaltime):
1336            pass
1337
1338
1339    def backup_conserved_quantities(self):
1340        N = len(self) # Number_of_triangles
1341
1342        # Backup conserved_quantities centroid values
1343        for name in self.conserved_quantities:
1344            Q = self.quantities[name]
1345            Q.backup_centroid_values()       
1346
1347    def saxpy_conserved_quantities(self,a,b):
1348        N = len(self) #number_of_triangles
1349
1350        # Backup conserved_quantities centroid values
1351        for name in self.conserved_quantities:
1352            Q = self.quantities[name]
1353            Q.saxpy_centroid_values(a,b)       
1354   
1355
1356    def update_boundary(self):
1357        """Go through list of boundary objects and update boundary values
1358        for all conserved quantities on boundary.
1359        It is assumed that the ordering of conserved quantities is
1360        consistent between the domain and the boundary object, i.e.
1361        the jth element of vector q must correspond to the jth conserved
1362        quantity in domain.
1363        """
1364
1365        # FIXME: Update only those that change (if that can be worked out)
1366        # FIXME: Boundary objects should not include ghost nodes.
1367        for i, ((vol_id, edge_id), B) in enumerate(self.boundary_objects):
1368            if B is None:
1369                print 'WARNING: Ignored boundary segment %d (None)'
1370            else:
1371                q = B.evaluate(vol_id, edge_id)
1372
1373                for j, name in enumerate(self.conserved_quantities):
1374                    Q = self.quantities[name]
1375                    Q.boundary_values[i] = q[j]
1376
1377
1378    def compute_fluxes(self):
1379        msg = 'Method compute_fluxes must be overridden by Domain subclass'
1380        raise msg
1381
1382
1383    def update_timestep(self, yieldstep, finaltime):
1384
1385        from anuga.config import min_timestep, max_timestep
1386
1387       
1388       
1389        # Protect against degenerate timesteps arising from isolated
1390        # triangles
1391        if self.protect_against_isolated_degenerate_timesteps is True and\
1392               self.max_speed > 10.0: # FIXME (Ole): Make this configurable
1393
1394            # Setup 10 bins for speed histogram
1395            from anuga.utilities.numerical_tools import histogram, create_bins
1396       
1397            bins = create_bins(self.max_speed, 10)
1398            hist = histogram(self.max_speed, bins)
1399
1400            # Look for characteristic signature
1401            if len(hist) > 1 and\
1402                hist[-1] > 0 and\
1403                hist[4] == hist[5] == hist[6] == hist[7] == hist[8] == 0:
1404                    # Danger of isolated degenerate triangles
1405                    # print self.timestepping_statistics(track_speeds=True) 
1406                   
1407                    # Find triangles in last bin
1408                    # FIXME - speed up using Numeric
1409                    d = 0
1410                    for i in range(self.number_of_full_triangles):
1411                        if self.max_speed[i] > bins[-1]:
1412                            msg = 'Time=%f: Ignoring isolated high ' %self.time
1413                            msg += 'speed triangle ' 
1414                            msg += '#%d of %d with max speed=%f'\
1415                                  %(i, self.number_of_full_triangles,
1416                                    self.max_speed[i])
1417                   
1418                            # print 'Found offending triangle', i,
1419                            # self.max_speed[i]
1420                            self.get_quantity('xmomentum').set_values(0.0, indices=[i])
1421                            self.get_quantity('ymomentum').set_values(0.0, indices=[i])
1422                            self.max_speed[i]=0.0
1423                            d += 1
1424                   
1425                    #print 'Adjusted %d triangles' %d       
1426                    #print self.timestepping_statistics(track_speeds=True)     
1427               
1428
1429                       
1430        # self.timestep is calculated from speed of characteristics
1431        # Apply CFL condition here
1432        timestep = min(self.CFL*self.flux_timestep, max_timestep)
1433
1434        # Record maximal and minimal values of timestep for reporting
1435        self.max_timestep = max(timestep, self.max_timestep)
1436        self.min_timestep = min(timestep, self.min_timestep)
1437
1438           
1439 
1440        # Protect against degenerate time steps
1441        if timestep < min_timestep:
1442
1443            # Number of consecutive small steps taken b4 taking action
1444            self.smallsteps += 1
1445
1446            if self.smallsteps > self.max_smallsteps:
1447                self.smallsteps = 0 # Reset
1448
1449                if self._order_ == 1:
1450                    msg = 'WARNING: Too small timestep %.16f reached '\
1451                          %timestep
1452                    msg += 'even after %d steps of 1 order scheme'\
1453                           %self.max_smallsteps
1454                    print msg
1455                    timestep = min_timestep  # Try enforcing min_step
1456
1457                    #print self.timestepping_statistics(track_speeds=True)
1458
1459                    raise Exception, msg
1460                else:
1461                    # Try to overcome situation by switching to 1 order
1462                    self._order_ = 1
1463
1464        else:
1465            self.smallsteps = 0
1466            if self._order_ == 1 and self.default_order == 2:
1467                self._order_ = 2
1468
1469
1470        # Ensure that final time is not exceeded
1471        if finaltime is not None and self.time + timestep > finaltime :
1472            timestep = finaltime-self.time
1473
1474        # Ensure that model time is aligned with yieldsteps
1475        if self.yieldtime + timestep > yieldstep:
1476            timestep = yieldstep-self.yieldtime
1477
1478        self.timestep = timestep
1479
1480
1481
1482    def compute_forcing_terms(self):
1483        """If there are any forcing functions driving the system
1484        they should be defined in Domain subclass and appended to
1485        the list self.forcing_terms
1486        """
1487
1488        for f in self.forcing_terms:
1489            f(self)
1490
1491
1492
1493    def update_conserved_quantities(self):
1494        """Update vectors of conserved quantities using previously
1495        computed fluxes specified forcing functions.
1496        """
1497
1498        from Numeric import ones, sum, equal, Float
1499
1500        N = len(self) # Number_of_triangles
1501        d = len(self.conserved_quantities)
1502
1503        timestep = self.timestep
1504
1505        # Compute forcing terms
1506        self.compute_forcing_terms()
1507
1508        # Update conserved_quantities
1509        for name in self.conserved_quantities:
1510            Q = self.quantities[name]
1511            Q.update(timestep)
1512
1513            # Note that Q.explicit_update is reset by compute_fluxes
1514            # Where is Q.semi_implicit_update reset?
1515           
1516
1517    def update_ghosts(self):
1518        pass
1519
1520    def distribute_to_vertices_and_edges(self):
1521        """Extrapolate conserved quantities from centroid to
1522        vertices and edge-midpoints for each volume
1523
1524        Default implementation is straight first order,
1525        i.e. constant values throughout each element and
1526        no reference to non-conserved quantities.
1527        """
1528
1529        for name in self.conserved_quantities:
1530            Q = self.quantities[name]
1531            if self._order_ == 1:
1532                Q.extrapolate_first_order()
1533            elif self._order_ == 2:
1534                Q.extrapolate_second_order()
1535                Q.limit()
1536            else:
1537                raise 'Unknown order'
1538            Q.interpolate_from_vertices_to_edges()
1539
1540
1541    def centroid_norm(self, quantity, normfunc):
1542        """Calculate the norm of the centroid values
1543        of a specific quantity, using normfunc.
1544
1545        normfunc should take a list to a float.
1546
1547        common normfuncs are provided in the module utilities.norms
1548        """
1549        return normfunc(self.quantities[quantity].centroid_values)
1550
1551
1552
1553#------------------
1554# Initialise module
1555#------------------
1556
1557# Optimisation with psyco
1558from anuga.config import use_psyco
1559if use_psyco:
1560    try:
1561        import psyco
1562    except:
1563        import os
1564        if os.name == 'posix' and os.uname()[4] == 'x86_64':
1565            pass
1566            # Psyco isn't supported on 64 bit systems, but it doesn't matter
1567        else:
1568            msg = 'WARNING: psyco (speedup) could not import'+\
1569                  ', you may want to consider installing it'
1570            print msg
1571    else:
1572        psyco.bind(Domain.update_boundary)
1573        #psyco.bind(Domain.update_timestep) # Not worth it
1574        psyco.bind(Domain.update_conserved_quantities)
1575        psyco.bind(Domain.distribute_to_vertices_and_edges)
1576
1577
1578if __name__ == "__main__":
1579    pass
Note: See TracBrowser for help on using the repository browser.