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

Last change on this file since 6191 was 6191, checked in by ole, 15 years ago

Refactored Mesh-Domain inheritance pattern into a composition pattern, thereby
paving the way for reuse of Mesh instance in fitting as per ticket:242.
Had to disable test for isinstance(domain, Domain) in quantity.py as it
failed for unknown reasons. All other tests and validation suite passes.

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