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

Last change on this file since 4712 was 4712, checked in by steve, 17 years ago

Working on 2nd order time stepping

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