source: branches/numpy/anuga/abstract_2d_finite_volumes/domain.py @ 6553

Last change on this file since 6553 was 6553, checked in by rwilson, 15 years ago

Merged trunk into numpy, all tests and validations work.

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