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

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

Arranged for timestepping statistic for chosen triangle, e.g. one of the
gauges in the Okushiri example.
The underlying function is currently brute force, but OK for now.

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