source: anuga_core/source/anuga/shallow_water/shallow_water_domain.py @ 7342

Last change on this file since 7342 was 7342, checked in by ole, 14 years ago

Refactored stofrage of quantities to use new concept of static and dynamic quantities.
This is in preparation for flexibly allowing quantities such as elevation or friction
to be time dependent.

All tests and the okushiri validation pass.

  • Property svn:keywords set to LastChangedDate LastChangedRevision LastChangedBy HeadURL Id
File size: 93.1 KB
Line 
1"""
2Finite-volume computations of the shallow water wave equation.
3
4Title: ANGUA shallow_water_domain - 2D triangular domains for finite-volume
5       computations of the shallow water wave equation.
6
7
8Author: Ole Nielsen, Ole.Nielsen@ga.gov.au
9        Stephen Roberts, Stephen.Roberts@anu.edu.au
10        Duncan Gray, Duncan.Gray@ga.gov.au
11
12CreationDate: 2004
13
14Description:
15    This module contains a specialisation of class Domain from
16    module domain.py consisting of methods specific to the
17    Shallow Water Wave Equation
18
19    U_t + E_x + G_y = S
20
21    where
22
23    U = [w, uh, vh]
24    E = [uh, u^2h + gh^2/2, uvh]
25    G = [vh, uvh, v^2h + gh^2/2]
26    S represents source terms forcing the system
27    (e.g. gravity, friction, wind stress, ...)
28
29    and _t, _x, _y denote the derivative with respect to t, x and y
30    respectively.
31
32
33    The quantities are
34
35    symbol    variable name    explanation
36    x         x                horizontal distance from origin [m]
37    y         y                vertical distance from origin [m]
38    z         elevation        elevation of bed on which flow is modelled [m]
39    h         height           water height above z [m]
40    w         stage            absolute water level, w = z+h [m]
41    u                          speed in the x direction [m/s]
42    v                          speed in the y direction [m/s]
43    uh        xmomentum        momentum in the x direction [m^2/s]
44    vh        ymomentum        momentum in the y direction [m^2/s]
45
46    eta                        mannings friction coefficient [to appear]
47    nu                         wind stress coefficient [to appear]
48
49    The conserved quantities are w, uh, vh
50
51Reference:
52    Catastrophic Collapse of Water Supply Reservoirs in Urban Areas,
53    Christopher Zoppou and Stephen Roberts,
54    Journal of Hydraulic Engineering, vol. 127, No. 7 July 1999
55
56    Hydrodynamic modelling of coastal inundation.
57    Nielsen, O., S. Roberts, D. Gray, A. McPherson and A. Hitchman
58    In Zerger, A. and Argent, R.M. (eds) MODSIM 2005 International Congress on
59    Modelling and Simulation. Modelling and Simulation Society of Australia and
60    New Zealand, December 2005, pp. 518-523. ISBN: 0-9758400-2-9.
61    http://www.mssanz.org.au/modsim05/papers/nielsen.pdf
62
63
64SeeAlso:
65    TRAC administration of ANUGA (User Manuals etc) at
66    https://datamining.anu.edu.au/anuga and Subversion repository at
67    $HeadURL: anuga_core/source/anuga/shallow_water/shallow_water_domain.py $
68
69Constraints: See GPL license in the user guide
70Version: 1.0 ($Revision: 7342 $)
71ModifiedBy:
72    $Author: ole $
73    $Date: 2009-08-08 15:20:42 +0000 (Sat, 08 Aug 2009) $
74"""
75
76# Subversion keywords:
77#
78# $LastChangedDate: 2009-08-08 15:20:42 +0000 (Sat, 08 Aug 2009) $
79# $LastChangedRevision: 7342 $
80# $LastChangedBy: ole $
81
82
83import numpy as num
84
85from anuga.abstract_2d_finite_volumes.neighbour_mesh import segment_midpoints
86from anuga.abstract_2d_finite_volumes.domain import Domain as Generic_Domain
87from anuga.abstract_2d_finite_volumes.generic_boundary_conditions\
88     import Boundary
89from anuga.abstract_2d_finite_volumes.generic_boundary_conditions\
90     import File_boundary
91from anuga.abstract_2d_finite_volumes.generic_boundary_conditions\
92     import Dirichlet_boundary
93from anuga.abstract_2d_finite_volumes.generic_boundary_conditions\
94     import Time_boundary
95from anuga.abstract_2d_finite_volumes.generic_boundary_conditions\
96     import Transmissive_boundary
97from anuga.abstract_2d_finite_volumes.generic_boundary_conditions\
98     import AWI_boundary
99
100from anuga.pmesh.mesh_interface import create_mesh_from_regions
101from anuga.utilities.numerical_tools import gradient, mean, ensure_numeric
102from anuga.geospatial_data.geospatial_data import ensure_geospatial
103
104from anuga.config import minimum_storable_height
105from anuga.config import minimum_allowed_height, maximum_allowed_speed
106from anuga.config import g, epsilon, beta_w, beta_w_dry,\
107     beta_uh, beta_uh_dry, beta_vh, beta_vh_dry, tight_slope_limiters
108from anuga.config import alpha_balance
109from anuga.config import optimise_dry_cells
110from anuga.config import optimised_gradient_limiter
111from anuga.config import use_edge_limiter
112from anuga.config import use_centroid_velocities
113from anuga.config import netcdf_mode_r, netcdf_mode_w, netcdf_mode_a
114
115from anuga.fit_interpolate.interpolate import Modeltime_too_late, \
116                                              Modeltime_too_early
117
118from anuga.utilities.polygon import inside_polygon, polygon_area, \
119                                    is_inside_polygon
120import anuga.utilities.log as log
121
122import types
123from types import IntType, FloatType
124from warnings import warn
125
126
127################################################################################
128# Shallow water domain
129################################################################################
130
131##
132# @brief Class for a shallow water domain.
133class Domain(Generic_Domain):
134
135    #conserved_quantities = ['stage', 'xmomentum', 'ymomentum']
136    #other_quantities = ['elevation', 'friction']
137
138    ##
139    # @brief Instantiate a shallow water domain.
140    # @param coordinates
141    # @param vertices
142    # @param boundary
143    # @param tagged_elements
144    # @param geo_reference
145    # @param use_inscribed_circle
146    # @param mesh_filename
147    # @param use_cache
148    # @param verbose
149    # @param full_send_dict
150    # @param ghost_recv_dict
151    # @param processor
152    # @param numproc
153    # @param number_of_full_nodes
154    # @param number_of_full_triangles
155    def __init__(self,
156                 coordinates=None,
157                 vertices=None,
158                 boundary=None,
159                 tagged_elements=None,
160                 geo_reference=None,
161                 use_inscribed_circle=False,
162                 mesh_filename=None,
163                 use_cache=False,
164                 verbose=False,
165                 full_send_dict=None,
166                 ghost_recv_dict=None,
167                 processor=0,
168                 numproc=1,
169                 number_of_full_nodes=None,
170                 number_of_full_triangles=None):
171
172        # Define quantities for the shallow_water domain         
173        conserved_quantities = ['stage', 'xmomentum', 'ymomentum']         
174        other_quantities = ['elevation', 'friction']
175       
176        Generic_Domain.__init__(self,
177                                coordinates,
178                                vertices,
179                                boundary,
180                                conserved_quantities,
181                                other_quantities,
182                                tagged_elements,
183                                geo_reference,
184                                use_inscribed_circle,
185                                mesh_filename,
186                                use_cache,
187                                verbose,
188                                full_send_dict,
189                                ghost_recv_dict,
190                                processor,
191                                numproc,
192                                number_of_full_nodes=number_of_full_nodes,
193                                number_of_full_triangles=number_of_full_triangles)
194
195        self.set_minimum_allowed_height(minimum_allowed_height)
196
197        self.maximum_allowed_speed = maximum_allowed_speed
198        self.g = g
199        self.beta_w = beta_w
200        self.beta_w_dry = beta_w_dry
201        self.beta_uh = beta_uh
202        self.beta_uh_dry = beta_uh_dry
203        self.beta_vh = beta_vh
204        self.beta_vh_dry = beta_vh_dry
205        self.alpha_balance = alpha_balance
206
207        self.tight_slope_limiters = tight_slope_limiters
208        self.optimise_dry_cells = optimise_dry_cells
209
210        self.forcing_terms.append(manning_friction_implicit)
211        self.forcing_terms.append(gravity)
212
213        # Stored output
214        self.store = True
215        self.set_store_vertices_uniquely(False)
216        self.minimum_storable_height = minimum_storable_height
217        self.quantities_to_be_stored = {'elevation': 1, 
218                                        'stage': 2, 
219                                        'xmomentum': 2, 
220                                        'ymomentum': 2}
221
222        # Limiters
223        self.use_edge_limiter = use_edge_limiter
224        self.optimised_gradient_limiter = optimised_gradient_limiter
225        self.use_centroid_velocities = use_centroid_velocities
226
227    ##
228    # @brief
229    # @param beta
230    def set_all_limiters(self, beta):
231        """Shorthand to assign one constant value [0,1] to all limiters.
232        0 Corresponds to first order, where as larger values make use of
233        the second order scheme.
234        """
235
236        self.beta_w = beta
237        self.beta_w_dry = beta
238        self.quantities['stage'].beta = beta
239
240        self.beta_uh = beta
241        self.beta_uh_dry = beta
242        self.quantities['xmomentum'].beta = beta
243
244        self.beta_vh = beta
245        self.beta_vh_dry = beta
246        self.quantities['ymomentum'].beta = beta
247
248    ##
249    # @brief
250    # @param flag
251    # @param reduction
252    def set_store_vertices_uniquely(self, flag, reduction=None):
253        """Decide whether vertex values should be stored uniquely as
254        computed in the model (True) or whether they should be reduced to one
255        value per vertex using self.reduction (False).
256        """
257
258        # FIXME (Ole): how about using the word "continuous vertex values" or
259        # "continuous stage surface"
260        self.smooth = not flag
261
262        # Reduction operation for get_vertex_values
263        if reduction is None:
264            self.reduction = mean
265            #self.reduction = min  #Looks better near steep slopes
266
267    ##
268    # @brief Set the minimum depth that will be written to an SWW file.
269    # @param minimum_storable_height The minimum stored height (in m).
270    def set_minimum_storable_height(self, minimum_storable_height):
271        """Set the minimum depth that will be recognised when writing
272        to an sww file. This is useful for removing thin water layers
273        that seems to be caused by friction creep.
274
275        The minimum allowed sww depth is in meters.
276        """
277
278        self.minimum_storable_height = minimum_storable_height
279
280    ##
281    # @brief
282    # @param minimum_allowed_height
283    def set_minimum_allowed_height(self, minimum_allowed_height):
284        """Set minimum depth that will be recognised in the numerical scheme.
285
286        The minimum allowed depth is in meters.
287
288        The parameter H0 (Minimal height for flux computation)
289        is also set by this function
290        """
291
292        #FIXME (Ole): rename H0 to minimum_allowed_height_in_flux_computation
293
294        #FIXME (Ole): Maybe use histogram to identify isolated extreme speeds
295        #and deal with them adaptively similarly to how we used to use 1 order
296        #steps to recover.
297
298        self.minimum_allowed_height = minimum_allowed_height
299        self.H0 = minimum_allowed_height
300
301    ##
302    # @brief
303    # @param maximum_allowed_speed
304    def set_maximum_allowed_speed(self, maximum_allowed_speed):
305        """Set the maximum particle speed that is allowed in water
306        shallower than minimum_allowed_height. This is useful for
307        controlling speeds in very thin layers of water and at the same time
308        allow some movement avoiding pooling of water.
309        """
310
311        self.maximum_allowed_speed = maximum_allowed_speed
312
313    ##
314    # @brief
315    # @param points_file_block_line_size
316    def set_points_file_block_line_size(self, points_file_block_line_size):
317        """Set the minimum depth that will be recognised when writing
318        to an sww file. This is useful for removing thin water layers
319        that seems to be caused by friction creep.
320
321        The minimum allowed sww depth is in meters.
322        """
323        self.points_file_block_line_size = points_file_block_line_size
324
325       
326    # FIXME: Probably obsolete in its curren form   
327    ##
328    # @brief Set the quantities that will be written to an SWW file.
329    # @param q The quantities to be written.
330    # @note Param 'q' may be None, single quantity or list of quantity strings.
331    # @note If 'q' is None, no quantities will be stored in the SWW file.
332    def set_quantities_to_be_stored(self, q):
333        """Specify which quantities will be stored in the sww file
334       
335        q must be either:
336          - a dictionary with quantity names
337          - a list of quantity names (for backwards compatibility)
338          - None
339
340        The format of the dictionary is as follows
341       
342        quantity_name: flag where flag must be either 1 or 2.
343        If flag is 1, the quantity is considered static and will be
344        stored once at the beginning of the simulation in a 1D array.
345       
346        If flag is 2, the quantity is considered time dependent and
347        it will be stored at each yieldstep by appending it to the
348        appropriate 2D array in the sww file.   
349       
350        If q is None, storage will be switched off altogether.
351       
352        Once the simulation has started and thw sww file opened,
353        this function will have no effect.
354       
355        The format, where q is a list of names is for backwards compatibility only.
356        It will take the specified quantities to be time dependent and assume
357        'elevation' to be static regardless.
358        """
359
360        if q is None:
361            self.quantities_to_be_stored = {}
362            self.store = False
363            return
364
365        # Check correcness
366        for quantity_name in q:
367            msg = ('Quantity %s is not a valid conserved quantity'
368                   % quantity_name)
369            assert quantity_name in self.quantities, msg
370
371        if type(q) == types.ListType:
372
373            msg = 'List arguments to set_quantities_to_be_stored '
374            msg += 'has been deprecated and will be removed in future '
375            msg += 'versions of ANUGA.'
376            msg += 'Please use dictionary argument instead'
377            warn(msg, DeprecationWarning) 
378
379       
380       
381            # FIXME: Raise deprecation warning
382            tmp = {}
383            for x in q:
384                tmp[x] = 2
385            tmp['elevation'] = 1   
386            q = tmp     
387           
388        assert type(q) == types.DictType   
389        self.quantities_to_be_stored = q
390
391    ##
392    # @brief
393    # @param indices
394    def get_wet_elements(self, indices=None):
395        """Return indices for elements where h > minimum_allowed_height
396
397        Optional argument:
398            indices is the set of element ids that the operation applies to.
399
400        Usage:
401            indices = get_wet_elements()
402
403        Note, centroid values are used for this operation
404        """
405
406        # Water depth below which it is considered to be 0 in the model
407        # FIXME (Ole): Allow this to be specified as a keyword argument as well
408        from anuga.config import minimum_allowed_height
409
410        elevation = self.get_quantity('elevation').\
411                        get_values(location='centroids', indices=indices)
412        stage = self.get_quantity('stage').\
413                    get_values(location='centroids', indices=indices)
414        depth = stage - elevation
415
416        # Select indices for which depth > 0
417        wet_indices = num.compress(depth > minimum_allowed_height,
418                                   num.arange(len(depth)))
419        return wet_indices
420
421    ##
422    # @brief
423    # @param indices
424    def get_maximum_inundation_elevation(self, indices=None):
425        """Return highest elevation where h > 0
426
427        Optional argument:
428            indices is the set of element ids that the operation applies to.
429
430        Usage:
431            q = get_maximum_inundation_elevation()
432
433        Note, centroid values are used for this operation
434        """
435
436        wet_elements = self.get_wet_elements(indices)
437        return self.get_quantity('elevation').\
438                   get_maximum_value(indices=wet_elements)
439
440    ##
441    # @brief
442    # @param indices
443    def get_maximum_inundation_location(self, indices=None):
444        """Return location of highest elevation where h > 0
445
446        Optional argument:
447            indices is the set of element ids that the operation applies to.
448
449        Usage:
450            q = get_maximum_inundation_location()
451
452        Note, centroid values are used for this operation
453        """
454
455        wet_elements = self.get_wet_elements(indices)
456        return self.get_quantity('elevation').\
457                   get_maximum_location(indices=wet_elements)
458
459    ##
460    # @brief Get the total flow through an arbitrary poly line.
461    # @param polyline Representation of desired cross section.
462    # @param verbose True if this method is to be verbose.
463    # @note 'polyline' may contain multiple sections allowing complex shapes.
464    # @note Assume absolute UTM coordinates.
465    def get_flow_through_cross_section(self, polyline, verbose=False):
466        """Get the total flow through an arbitrary poly line.
467
468        This is a run-time equivalent of the function with same name
469        in data_manager.py
470
471        Input:
472            polyline: Representation of desired cross section - it may contain
473                      multiple sections allowing for complex shapes. Assume
474                      absolute UTM coordinates.
475                      Format [[x0, y0], [x1, y1], ...]
476
477        Output:
478            Q: Total flow [m^3/s] across given segments.
479        """
480
481        # Find all intersections and associated triangles.
482        segments = self.get_intersecting_segments(polyline, use_cache=True,
483                                                  verbose=verbose)
484
485        # Get midpoints
486        midpoints = segment_midpoints(segments)
487
488        # Make midpoints Geospatial instances
489        midpoints = ensure_geospatial(midpoints, self.geo_reference)
490
491        # Compute flow
492        if verbose:
493            log.critical('Computing flow through specified cross section')
494
495        # Get interpolated values
496        xmomentum = self.get_quantity('xmomentum')
497        ymomentum = self.get_quantity('ymomentum')
498
499        uh = xmomentum.get_values(interpolation_points=midpoints,
500                                  use_cache=True)
501        vh = ymomentum.get_values(interpolation_points=midpoints,
502                                  use_cache=True)
503
504        # Compute and sum flows across each segment
505        total_flow = 0
506        for i in range(len(uh)):
507            # Inner product of momentum vector with segment normal [m^2/s]
508            normal = segments[i].normal
509            normal_momentum = uh[i]*normal[0] + vh[i]*normal[1]
510
511            # Flow across this segment [m^3/s]
512            segment_flow = normal_momentum*segments[i].length
513
514            # Accumulate
515            total_flow += segment_flow
516
517        return total_flow
518
519    ##
520    # @brief
521    # @param polyline Representation of desired cross section.
522    # @param kind Select energy type to compute ('specific' or 'total').
523    # @param verbose True if this method is to be verbose.
524    # @note 'polyline' may contain multiple sections allowing complex shapes.
525    # @note Assume absolute UTM coordinates.
526    def get_energy_through_cross_section(self, polyline,
527                                         kind='total',
528                                         verbose=False):
529        """Obtain average energy head [m] across specified cross section.
530
531        Inputs:
532            polyline: Representation of desired cross section - it may contain
533                      multiple sections allowing for complex shapes. Assume
534                      absolute UTM coordinates.
535                      Format [[x0, y0], [x1, y1], ...]
536            kind:     Select which energy to compute.
537                      Options are 'specific' and 'total' (default)
538
539        Output:
540            E: Average energy [m] across given segments for all stored times.
541
542        The average velocity is computed for each triangle intersected by
543        the polyline and averaged weighted by segment lengths.
544
545        The typical usage of this function would be to get average energy of
546        flow in a channel, and the polyline would then be a cross section
547        perpendicular to the flow.
548
549        #FIXME (Ole) - need name for this energy reflecting that its dimension
550        is [m].
551        """
552
553        from anuga.config import g, epsilon, velocity_protection as h0
554
555        # Find all intersections and associated triangles.
556        segments = self.get_intersecting_segments(polyline, use_cache=True,
557                                                  verbose=verbose)
558
559        # Get midpoints
560        midpoints = segment_midpoints(segments)
561
562        # Make midpoints Geospatial instances
563        midpoints = ensure_geospatial(midpoints, self.geo_reference)
564
565        # Compute energy
566        if verbose: log.critical('Computing %s energy' % kind)
567
568        # Get interpolated values
569        stage = self.get_quantity('stage')
570        elevation = self.get_quantity('elevation')
571        xmomentum = self.get_quantity('xmomentum')
572        ymomentum = self.get_quantity('ymomentum')
573
574        w = stage.get_values(interpolation_points=midpoints, use_cache=True)
575        z = elevation.get_values(interpolation_points=midpoints, use_cache=True)
576        uh = xmomentum.get_values(interpolation_points=midpoints,
577                                  use_cache=True)
578        vh = ymomentum.get_values(interpolation_points=midpoints,
579                                  use_cache=True)
580        h = w-z                # Depth
581
582        # Compute total length of polyline for use with weighted averages
583        total_line_length = 0.0
584        for segment in segments:
585            total_line_length += segment.length
586
587        # Compute and sum flows across each segment
588        average_energy = 0.0
589        for i in range(len(w)):
590            # Average velocity across this segment
591            if h[i] > epsilon:
592                # Use protection against degenerate velocities
593                u = uh[i]/(h[i] + h0/h[i])
594                v = vh[i]/(h[i] + h0/h[i])
595            else:
596                u = v = 0.0
597
598            speed_squared = u*u + v*v
599            kinetic_energy = 0.5*speed_squared/g
600
601            if kind == 'specific':
602                segment_energy = h[i] + kinetic_energy
603            elif kind == 'total':
604                segment_energy = w[i] + kinetic_energy
605            else:
606                msg = 'Energy kind must be either "specific" or "total".'
607                msg += ' I got %s' %kind
608
609            # Add to weighted average
610            weigth = segments[i].length/total_line_length
611            average_energy += segment_energy*weigth
612
613        return average_energy
614
615    ##
616    # @brief Run integrity checks on shallow water domain.
617    def check_integrity(self):
618        Generic_Domain.check_integrity(self)
619
620        #Check that we are solving the shallow water wave equation
621        msg = 'First conserved quantity must be "stage"'
622        assert self.conserved_quantities[0] == 'stage', msg
623        msg = 'Second conserved quantity must be "xmomentum"'
624        assert self.conserved_quantities[1] == 'xmomentum', msg
625        msg = 'Third conserved quantity must be "ymomentum"'
626        assert self.conserved_quantities[2] == 'ymomentum', msg
627
628    ##
629    # @brief
630    def extrapolate_second_order_sw(self):
631        #Call correct module function (either from this module or C-extension)
632        extrapolate_second_order_sw(self)
633
634    ##
635    # @brief
636    def compute_fluxes(self):
637        #Call correct module function (either from this module or C-extension)
638        compute_fluxes(self)
639
640    ##
641    # @brief
642    def distribute_to_vertices_and_edges(self):
643        # Call correct module function
644        if self.use_edge_limiter:
645            distribute_using_edge_limiter(self)
646        else:
647            distribute_using_vertex_limiter(self)
648
649    ##
650    # @brief Evolve the model by one step.
651    # @param yieldstep
652    # @param finaltime
653    # @param duration
654    # @param skip_initial_step
655    def evolve(self,
656               yieldstep=None,
657               finaltime=None,
658               duration=None,
659               skip_initial_step=False):
660        """Specialisation of basic evolve method from parent class"""
661
662        # Call check integrity here rather than from user scripts
663        # self.check_integrity()
664
665        msg = 'Attribute self.beta_w must be in the interval [0, 2]'
666        assert 0 <= self.beta_w <= 2.0, msg
667
668        # Initial update of vertex and edge values before any STORAGE
669        # and or visualisation.
670        # This is done again in the initialisation of the Generic_Domain
671        # evolve loop but we do it here to ensure the values are ok for storage.
672        self.distribute_to_vertices_and_edges()
673
674        if self.store is True and self.time == 0.0:
675            self.initialise_storage()
676
677        # Call basic machinery from parent class
678        for t in Generic_Domain.evolve(self, yieldstep=yieldstep,
679                                       finaltime=finaltime, duration=duration,
680                                       skip_initial_step=skip_initial_step):
681            # Store model data, e.g. for subsequent visualisation
682            if self.store is True:
683                self.store_timestep()
684
685            # Pass control on to outer loop for more specific actions
686            yield(t)
687
688    ##
689    # @brief
690    def initialise_storage(self):
691        """Create and initialise self.writer object for storing data.
692        Also, save x,y and bed elevation
693        """
694
695        from anuga.shallow_water.data_manager import SWW_file
696       
697        # Initialise writer
698        self.writer = SWW_file(self)
699
700        # Store vertices and connectivity
701        self.writer.store_connectivity()
702
703    ##
704    # @brief
705    # @param name
706    def store_timestep(self):
707        """Store time dependent quantities and time.
708
709        Precondition:
710           self.writer has been initialised
711        """
712
713        self.writer.store_timestep()
714
715    ##
716    # @brief Get time stepping statistics string for printing.
717    # @param track_speeds
718    # @param triangle_id
719    def timestepping_statistics(self,
720                                track_speeds=False,
721                                triangle_id=None):
722        """Return string with time stepping statistics for printing or logging
723
724        Optional boolean keyword track_speeds decides whether to report
725        location of smallest timestep as well as a histogram and percentile
726        report.
727        """
728
729        from anuga.config import epsilon, g
730
731        # Call basic machinery from parent class
732        msg = Generic_Domain.timestepping_statistics(self, track_speeds,
733                                                     triangle_id)
734
735        if track_speeds is True:
736            # qwidth determines the text field used for quantities
737            qwidth = self.qwidth
738
739            # Selected triangle
740            k = self.k
741
742            # Report some derived quantities at vertices, edges and centroid
743            # specific to the shallow water wave equation
744            z = self.quantities['elevation']
745            w = self.quantities['stage']
746
747            Vw = w.get_values(location='vertices', indices=[k])[0]
748            Ew = w.get_values(location='edges', indices=[k])[0]
749            Cw = w.get_values(location='centroids', indices=[k])
750
751            Vz = z.get_values(location='vertices', indices=[k])[0]
752            Ez = z.get_values(location='edges', indices=[k])[0]
753            Cz = z.get_values(location='centroids', indices=[k])
754
755            name = 'depth'
756            Vh = Vw-Vz
757            Eh = Ew-Ez
758            Ch = Cw-Cz
759
760            s  = '    %s: vertex_values =  %.4f,\t %.4f,\t %.4f\n'\
761                 %(name.ljust(qwidth), Vh[0], Vh[1], Vh[2])
762
763            s += '    %s: edge_values =    %.4f,\t %.4f,\t %.4f\n'\
764                 %(name.ljust(qwidth), Eh[0], Eh[1], Eh[2])
765
766            s += '    %s: centroid_value = %.4f\n'\
767                 %(name.ljust(qwidth), Ch[0])
768
769            msg += s
770
771            uh = self.quantities['xmomentum']
772            vh = self.quantities['ymomentum']
773
774            Vuh = uh.get_values(location='vertices', indices=[k])[0]
775            Euh = uh.get_values(location='edges', indices=[k])[0]
776            Cuh = uh.get_values(location='centroids', indices=[k])
777
778            Vvh = vh.get_values(location='vertices', indices=[k])[0]
779            Evh = vh.get_values(location='edges', indices=[k])[0]
780            Cvh = vh.get_values(location='centroids', indices=[k])
781
782            # Speeds in each direction
783            Vu = Vuh/(Vh + epsilon)
784            Eu = Euh/(Eh + epsilon)
785            Cu = Cuh/(Ch + epsilon)
786            name = 'U'
787            s  = '    %s: vertex_values =  %.4f,\t %.4f,\t %.4f\n'\
788                 %(name.ljust(qwidth), Vu[0], Vu[1], Vu[2])
789
790            s += '    %s: edge_values =    %.4f,\t %.4f,\t %.4f\n'\
791                 %(name.ljust(qwidth), Eu[0], Eu[1], Eu[2])
792
793            s += '    %s: centroid_value = %.4f\n'\
794                 %(name.ljust(qwidth), Cu[0])
795
796            msg += s
797
798            Vv = Vvh/(Vh + epsilon)
799            Ev = Evh/(Eh + epsilon)
800            Cv = Cvh/(Ch + epsilon)
801            name = 'V'
802            s  = '    %s: vertex_values =  %.4f,\t %.4f,\t %.4f\n'\
803                 %(name.ljust(qwidth), Vv[0], Vv[1], Vv[2])
804
805            s += '    %s: edge_values =    %.4f,\t %.4f,\t %.4f\n'\
806                 %(name.ljust(qwidth), Ev[0], Ev[1], Ev[2])
807
808            s += '    %s: centroid_value = %.4f\n'\
809                 %(name.ljust(qwidth), Cv[0])
810
811            msg += s
812
813            # Froude number in each direction
814            name = 'Froude (x)'
815            Vfx = Vu/(num.sqrt(g*Vh) + epsilon)
816            Efx = Eu/(num.sqrt(g*Eh) + epsilon)
817            Cfx = Cu/(num.sqrt(g*Ch) + epsilon)
818
819            s  = '    %s: vertex_values =  %.4f,\t %.4f,\t %.4f\n'\
820                 %(name.ljust(qwidth), Vfx[0], Vfx[1], Vfx[2])
821
822            s += '    %s: edge_values =    %.4f,\t %.4f,\t %.4f\n'\
823                 %(name.ljust(qwidth), Efx[0], Efx[1], Efx[2])
824
825            s += '    %s: centroid_value = %.4f\n'\
826                 %(name.ljust(qwidth), Cfx[0])
827
828            msg += s
829
830            name = 'Froude (y)'
831            Vfy = Vv/(num.sqrt(g*Vh) + epsilon)
832            Efy = Ev/(num.sqrt(g*Eh) + epsilon)
833            Cfy = Cv/(num.sqrt(g*Ch) + epsilon)
834
835            s  = '    %s: vertex_values =  %.4f,\t %.4f,\t %.4f\n'\
836                 %(name.ljust(qwidth), Vfy[0], Vfy[1], Vfy[2])
837
838            s += '    %s: edge_values =    %.4f,\t %.4f,\t %.4f\n'\
839                 %(name.ljust(qwidth), Efy[0], Efy[1], Efy[2])
840
841            s += '    %s: centroid_value = %.4f\n'\
842                 %(name.ljust(qwidth), Cfy[0])
843
844            msg += s
845
846        return msg
847       
848       
849
850    def compute_boundary_flows(self):
851        """Compute boundary flows at current timestep.
852                       
853        Quantities computed are:
854           Total inflow across boundary
855           Total outflow across boundary
856           Flow across each tagged boundary segment
857        """
858               
859        # Run through boundary array and compute for each segment
860        # the normal momentum ((uh, vh) dot normal) times segment length.
861        # Based on sign accumulate this into boundary_inflow and boundary_outflow.
862                       
863        # Compute flows along boundary
864       
865        uh = self.get_quantity('xmomentum').get_values(location='edges')
866        vh = self.get_quantity('ymomentum').get_values(location='edges')       
867       
868        # Loop through edges that lie on the boundary and calculate
869        # flows
870        boundary_flows = {}
871        total_boundary_inflow = 0.0
872        total_boundary_outflow = 0.0
873        for vol_id, edge_id in self.boundary:
874            # Compute normal flow across edge. Since normal vector points
875            # away from triangle, a positive sign means that water
876            # flows *out* from this triangle.
877             
878            momentum = [uh[vol_id, edge_id], vh[vol_id, edge_id]]
879            normal = self.mesh.get_normal(vol_id, edge_id)
880            length = self.mesh.get_edgelength(vol_id, edge_id)           
881            normal_flow = num.dot(momentum, normal)*length
882           
883            # Reverse sign so that + is taken to mean inflow
884            # and - means outflow. This is more intuitive.
885            edge_flow = -normal_flow
886           
887            # Tally up inflows and outflows separately
888            if edge_flow > 0:
889                # Flow is inflow     
890                total_boundary_inflow += edge_flow                                 
891            else:
892                # Flow is outflow
893                total_boundary_outflow += edge_flow   
894
895            # Tally up flows by boundary tag
896            tag = self.boundary[(vol_id, edge_id)]
897           
898            if tag not in boundary_flows:
899                boundary_flows[tag] = 0.0
900            boundary_flows[tag] += edge_flow
901           
902               
903        return boundary_flows, total_boundary_inflow, total_boundary_outflow
904       
905
906    def compute_forcing_flows(self):
907        """Compute flows in and out of domain due to forcing terms.
908                       
909        Quantities computed are:
910               
911       
912           Total inflow through forcing terms
913           Total outflow through forcing terms
914           Current total volume in domain       
915
916        """
917
918        #FIXME(Ole): We need to separate what part of explicit_update was
919        # due to the normal flux calculations and what is due to forcing terms.
920       
921        pass
922                       
923       
924    def compute_total_volume(self):
925        """Compute total volume (m^3) of water in entire domain
926        """
927       
928        area = self.mesh.get_areas()
929        volume = 0.0
930       
931        stage = self.get_quantity('stage').get_values(location='centroids')
932        elevation = self.get_quantity('elevation').get_values(location='centroids')       
933        depth = stage-elevation
934       
935        return num.sum(depth*area)
936       
937       
938    def volumetric_balance_statistics(self):               
939        """Create volumetric balance report suitable for printing or logging.
940        """
941       
942        (boundary_flows, total_boundary_inflow,
943         total_boundary_outflow) = self.compute_boundary_flows() 
944       
945        s = '---------------------------\n'       
946        s += 'Volumetric balance report:\n'
947        s += '--------------------------\n'
948        s += 'Total boundary inflow [m^3/s]: %.2f\n' % total_boundary_inflow
949        s += 'Total boundary outflow [m^3/s]: %.2f\n' % total_boundary_outflow       
950        s += 'Net boundary flow by tags [m^3/s]\n'
951        for tag in boundary_flows:
952            s += '    %s [m^3/s]: %.2f\n' % (tag, boundary_flows[tag])
953       
954        s += 'Total net boundary flow [m^3/s]: %.2f\n' % (total_boundary_inflow + total_boundary_outflow) 
955        s += 'Total volume in domain [m^3]: %.2f\n' % self.compute_total_volume()
956       
957        # The go through explicit forcing update and record the rate of change for stage and
958        # record into forcing_inflow and forcing_outflow. Finally compute integral
959        # of depth to obtain total volume of domain.
960       
961        # FIXME(Ole): This part is not yet done.               
962       
963        return s       
964           
965################################################################################
966# End of class Shallow Water Domain
967################################################################################
968
969#-----------------
970# Flux computation
971#-----------------
972
973## @brief Compute fluxes and timestep suitable for all volumes in domain.
974# @param domain The domain to calculate fluxes for.
975def compute_fluxes(domain):
976    """Compute fluxes and timestep suitable for all volumes in domain.
977
978    Compute total flux for each conserved quantity using "flux_function"
979
980    Fluxes across each edge are scaled by edgelengths and summed up
981    Resulting flux is then scaled by area and stored in
982    explicit_update for each of the three conserved quantities
983    stage, xmomentum and ymomentum
984
985    The maximal allowable speed computed by the flux_function for each volume
986    is converted to a timestep that must not be exceeded. The minimum of
987    those is computed as the next overall timestep.
988
989    Post conditions:
990      domain.explicit_update is reset to computed flux values
991      domain.timestep is set to the largest step satisfying all volumes.
992
993    This wrapper calls the underlying C version of compute fluxes
994    """
995
996    import sys
997    from shallow_water_ext import compute_fluxes_ext_central \
998                                  as compute_fluxes_ext
999
1000    N = len(domain)    # number_of_triangles
1001
1002    # Shortcuts
1003    Stage = domain.quantities['stage']
1004    Xmom = domain.quantities['xmomentum']
1005    Ymom = domain.quantities['ymomentum']
1006    Bed = domain.quantities['elevation']
1007
1008    timestep = float(sys.maxint)
1009
1010    flux_timestep = compute_fluxes_ext(timestep,
1011                                       domain.epsilon,
1012                                       domain.H0,
1013                                       domain.g,
1014                                       domain.neighbours,
1015                                       domain.neighbour_edges,
1016                                       domain.normals,
1017                                       domain.edgelengths,
1018                                       domain.radii,
1019                                       domain.areas,
1020                                       domain.tri_full_flag,
1021                                       Stage.edge_values,
1022                                       Xmom.edge_values,
1023                                       Ymom.edge_values,
1024                                       Bed.edge_values,
1025                                       Stage.boundary_values,
1026                                       Xmom.boundary_values,
1027                                       Ymom.boundary_values,
1028                                       Stage.explicit_update,
1029                                       Xmom.explicit_update,
1030                                       Ymom.explicit_update,
1031                                       domain.already_computed_flux,
1032                                       domain.max_speed,
1033                                       int(domain.optimise_dry_cells))
1034
1035    domain.flux_timestep = flux_timestep
1036
1037################################################################################
1038# Module functions for gradient limiting
1039################################################################################
1040
1041##
1042# @brief Wrapper for C version of extrapolate_second_order_sw.
1043# @param domain The domain to operate on.
1044# @note MH090605 The following method belongs to the shallow_water domain class
1045#       see comments in the corresponding method in shallow_water_ext.c
1046def extrapolate_second_order_sw(domain):
1047    """Wrapper calling C version of extrapolate_second_order_sw"""
1048
1049    import sys
1050    from shallow_water_ext import extrapolate_second_order_sw as extrapol2
1051
1052    N = len(domain) # number_of_triangles
1053
1054    # Shortcuts
1055    Stage = domain.quantities['stage']
1056    Xmom = domain.quantities['xmomentum']
1057    Ymom = domain.quantities['ymomentum']
1058    Elevation = domain.quantities['elevation']
1059
1060    extrapol2(domain,
1061              domain.surrogate_neighbours,
1062              domain.number_of_boundaries,
1063              domain.centroid_coordinates,
1064              Stage.centroid_values,
1065              Xmom.centroid_values,
1066              Ymom.centroid_values,
1067              Elevation.centroid_values,
1068              domain.vertex_coordinates,
1069              Stage.vertex_values,
1070              Xmom.vertex_values,
1071              Ymom.vertex_values,
1072              Elevation.vertex_values,
1073              int(domain.optimise_dry_cells))
1074
1075##
1076# @brief Distribution from centroids to vertices specific to the SWW eqn.
1077# @param domain The domain to operate on.
1078def distribute_using_vertex_limiter(domain):
1079    """Distribution from centroids to vertices specific to the SWW equation.
1080
1081    It will ensure that h (w-z) is always non-negative even in the
1082    presence of steep bed-slopes by taking a weighted average between shallow
1083    and deep cases.
1084
1085    In addition, all conserved quantities get distributed as per either a
1086    constant (order==1) or a piecewise linear function (order==2).
1087
1088    FIXME: more explanation about removal of artificial variability etc
1089
1090    Precondition:
1091      All quantities defined at centroids and bed elevation defined at
1092      vertices.
1093
1094    Postcondition
1095      Conserved quantities defined at vertices
1096    """
1097
1098    # Remove very thin layers of water
1099    protect_against_infinitesimal_and_negative_heights(domain)
1100
1101    # Extrapolate all conserved quantities
1102    if domain.optimised_gradient_limiter:
1103        # MH090605 if second order,
1104        # perform the extrapolation and limiting on
1105        # all of the conserved quantities
1106
1107        if (domain._order_ == 1):
1108            for name in domain.conserved_quantities:
1109                Q = domain.quantities[name]
1110                Q.extrapolate_first_order()
1111        elif domain._order_ == 2:
1112            domain.extrapolate_second_order_sw()
1113        else:
1114            raise 'Unknown order'
1115    else:
1116        # Old code:
1117        for name in domain.conserved_quantities:
1118            Q = domain.quantities[name]
1119
1120            if domain._order_ == 1:
1121                Q.extrapolate_first_order()
1122            elif domain._order_ == 2:
1123                Q.extrapolate_second_order_and_limit_by_vertex()
1124            else:
1125                raise 'Unknown order'
1126
1127    # Take bed elevation into account when water heights are small
1128    balance_deep_and_shallow(domain)
1129
1130    # Compute edge values by interpolation
1131    for name in domain.conserved_quantities:
1132        Q = domain.quantities[name]
1133        Q.interpolate_from_vertices_to_edges()
1134
1135##
1136# @brief Distribution from centroids to edges specific to the SWW eqn.
1137# @param domain The domain to operate on.
1138def distribute_using_edge_limiter(domain):
1139    """Distribution from centroids to edges specific to the SWW eqn.
1140
1141    It will ensure that h (w-z) is always non-negative even in the
1142    presence of steep bed-slopes by taking a weighted average between shallow
1143    and deep cases.
1144
1145    In addition, all conserved quantities get distributed as per either a
1146    constant (order==1) or a piecewise linear function (order==2).
1147
1148
1149    Precondition:
1150      All quantities defined at centroids and bed elevation defined at
1151      vertices.
1152
1153    Postcondition
1154      Conserved quantities defined at vertices
1155    """
1156
1157    # Remove very thin layers of water
1158    protect_against_infinitesimal_and_negative_heights(domain)
1159
1160    for name in domain.conserved_quantities:
1161        Q = domain.quantities[name]
1162        if domain._order_ == 1:
1163            Q.extrapolate_first_order()
1164        elif domain._order_ == 2:
1165            Q.extrapolate_second_order_and_limit_by_edge()
1166        else:
1167            raise 'Unknown order'
1168
1169    balance_deep_and_shallow(domain)
1170
1171    # Compute edge values by interpolation
1172    for name in domain.conserved_quantities:
1173        Q = domain.quantities[name]
1174        Q.interpolate_from_vertices_to_edges()
1175
1176##
1177# @brief  Protect against infinitesimal heights and associated high velocities.
1178# @param domain The domain to operate on.
1179def protect_against_infinitesimal_and_negative_heights(domain):
1180    """Protect against infinitesimal heights and associated high velocities"""
1181
1182    from shallow_water_ext import protect
1183
1184    # Shortcuts
1185    wc = domain.quantities['stage'].centroid_values
1186    zc = domain.quantities['elevation'].centroid_values
1187    xmomc = domain.quantities['xmomentum'].centroid_values
1188    ymomc = domain.quantities['ymomentum'].centroid_values
1189
1190    protect(domain.minimum_allowed_height, domain.maximum_allowed_speed,
1191            domain.epsilon, wc, zc, xmomc, ymomc)
1192
1193##
1194# @brief Wrapper for C function balance_deep_and_shallow_c().
1195# @param domain The domain to operate on.
1196def balance_deep_and_shallow(domain):
1197    """Compute linear combination between stage as computed by
1198    gradient-limiters limiting using w, and stage computed by
1199    gradient-limiters limiting using h (h-limiter).
1200    The former takes precedence when heights are large compared to the
1201    bed slope while the latter takes precedence when heights are
1202    relatively small.  Anything in between is computed as a balanced
1203    linear combination in order to avoid numerical disturbances which
1204    would otherwise appear as a result of hard switching between
1205    modes.
1206
1207    Wrapper for C implementation
1208    """
1209
1210    from shallow_water_ext import balance_deep_and_shallow \
1211                                  as balance_deep_and_shallow_c
1212
1213    # Shortcuts
1214    wc = domain.quantities['stage'].centroid_values
1215    zc = domain.quantities['elevation'].centroid_values
1216    wv = domain.quantities['stage'].vertex_values
1217    zv = domain.quantities['elevation'].vertex_values
1218
1219    # Momentums at centroids
1220    xmomc = domain.quantities['xmomentum'].centroid_values
1221    ymomc = domain.quantities['ymomentum'].centroid_values
1222
1223    # Momentums at vertices
1224    xmomv = domain.quantities['xmomentum'].vertex_values
1225    ymomv = domain.quantities['ymomentum'].vertex_values
1226
1227    balance_deep_and_shallow_c(domain,
1228                               wc, zc, wv, zv, wc,
1229                               xmomc, ymomc, xmomv, ymomv)
1230
1231
1232################################################################################
1233# Boundary conditions - specific to the shallow water wave equation
1234################################################################################
1235
1236##
1237# @brief Class for a reflective boundary.
1238# @note Inherits from Boundary.
1239class Reflective_boundary(Boundary):
1240    """Reflective boundary returns same conserved quantities as
1241    those present in its neighbour volume but reflected.
1242
1243    This class is specific to the shallow water equation as it
1244    works with the momentum quantities assumed to be the second
1245    and third conserved quantities.
1246    """
1247
1248    ##
1249    # @brief Instantiate a Reflective_boundary.
1250    # @param domain
1251    def __init__(self, domain=None):
1252        Boundary.__init__(self)
1253
1254        if domain is None:
1255            msg = 'Domain must be specified for reflective boundary'
1256            raise Exception, msg
1257
1258        # Handy shorthands
1259        self.stage = domain.quantities['stage'].edge_values
1260        self.xmom = domain.quantities['xmomentum'].edge_values
1261        self.ymom = domain.quantities['ymomentum'].edge_values
1262        self.normals = domain.normals
1263
1264        self.conserved_quantities = num.zeros(3, num.float)
1265
1266    ##
1267    # @brief Return a representation of this instance.
1268    def __repr__(self):
1269        return 'Reflective_boundary'
1270
1271    ##
1272    # @brief Calculate reflections (reverse outward momentum).
1273    # @param vol_id
1274    # @param edge_id
1275    def evaluate(self, vol_id, edge_id):
1276        """Reflective boundaries reverses the outward momentum
1277        of the volume they serve.
1278        """
1279
1280        q = self.conserved_quantities
1281        q[0] = self.stage[vol_id, edge_id]
1282        q[1] = self.xmom[vol_id, edge_id]
1283        q[2] = self.ymom[vol_id, edge_id]
1284
1285        normal = self.normals[vol_id, 2*edge_id:2*edge_id+2]
1286
1287        r = rotate(q, normal, direction = 1)
1288        r[1] = -r[1]
1289        q = rotate(r, normal, direction = -1)
1290
1291        return q
1292
1293
1294##
1295# @brief Class for a transmissive boundary.
1296# @note Inherits from Boundary.
1297class Transmissive_momentum_set_stage_boundary(Boundary):
1298    """Returns same momentum conserved quantities as
1299    those present in its neighbour volume.
1300    Sets stage by specifying a function f of time which may either be a
1301    vector function or a scalar function
1302
1303    Example:
1304
1305    def waveform(t):
1306        return sea_level + normalized_amplitude/cosh(t-25)**2
1307
1308    Bts = Transmissive_momentum_set_stage_boundary(domain, waveform)
1309
1310    Underlying domain must be specified when boundary is instantiated
1311    """
1312
1313    ##
1314    # @brief Instantiate a Transmissive_momentum_set_stage_boundary.
1315    # @param domain
1316    # @param function
1317    def __init__(self, domain=None, function=None):
1318        Boundary.__init__(self)
1319
1320        if domain is None:
1321            msg = 'Domain must be specified for this type boundary'
1322            raise Exception, msg
1323
1324        if function is None:
1325            msg = 'Function must be specified for this type boundary'
1326            raise Exception, msg
1327
1328        self.domain = domain
1329        self.function = function
1330
1331    ##
1332    # @brief Return a representation of this instance.
1333    def __repr__(self):
1334        return 'Transmissive_momentum_set_stage_boundary(%s)' %self.domain
1335
1336    ##
1337    # @brief Calculate transmissive results.
1338    # @param vol_id
1339    # @param edge_id
1340    def evaluate(self, vol_id, edge_id):
1341        """Transmissive momentum set stage boundaries return the edge momentum
1342        values of the volume they serve.
1343        """
1344
1345        q = self.domain.get_conserved_quantities(vol_id, edge = edge_id)
1346
1347        t = self.domain.get_time()
1348
1349        if hasattr(self.function, 'time'):
1350            # Roll boundary over if time exceeds
1351            while t > self.function.time[-1]:
1352                msg = 'WARNING: domain time %.2f has exceeded' % t
1353                msg += 'time provided in '
1354                msg += 'transmissive_momentum_set_stage_boundary object.\n'
1355                msg += 'I will continue, reusing the object from t==0'
1356                log.critical(msg)
1357                t -= self.function.time[-1]
1358
1359        value = self.function(t)
1360        try:
1361            x = float(value)
1362        except:
1363            x = float(value[0])
1364
1365        q[0] = x
1366           
1367        return q
1368
1369        # FIXME: Consider this (taken from File_boundary) to allow
1370        # spatial variation
1371        # if vol_id is not None and edge_id is not None:
1372        #     i = self.boundary_indices[ vol_id, edge_id ]
1373        #     return self.F(t, point_id = i)
1374        # else:
1375        #     return self.F(t)
1376
1377
1378##
1379# @brief Deprecated boundary class.
1380class Transmissive_Momentum_Set_Stage_boundary(Transmissive_momentum_set_stage_boundary):
1381    pass
1382
1383
1384##
1385# @brief Class for a transmissive boundary.
1386# @note Inherits from Boundary.
1387class Transmissive_n_momentum_zero_t_momentum_set_stage_boundary(Boundary):
1388    """Returns the same normal momentum as that
1389    present in neighbour volume edge. Zero out the tangential momentum.
1390    Sets stage by specifying a function f of time which may either be a
1391    vector function or a scalar function
1392
1393    Example:
1394
1395    def waveform(t):
1396        return sea_level + normalized_amplitude/cosh(t-25)**2
1397
1398    Bts = Transmissive_n_momentum_zero_t_momentum_set_stage_boundary(domain, waveform)
1399
1400    Underlying domain must be specified when boundary is instantiated
1401    """
1402
1403    ##
1404    # @brief Instantiate a Transmissive_n_momentum_zero_t_momentum_set_stage_boundary.
1405    # @param domain
1406    # @param function
1407    def __init__(self, domain=None, function=None):
1408        Boundary.__init__(self)
1409
1410        if domain is None:
1411            msg = 'Domain must be specified for this type boundary'
1412            raise Exception, msg
1413
1414        if function is None:
1415            msg = 'Function must be specified for this type boundary'
1416            raise Exception, msg
1417
1418        self.domain = domain
1419        self.function = function
1420
1421    ##
1422    # @brief Return a representation of this instance.
1423    def __repr__(self):
1424        return 'Transmissive_n_momentum_zero_t_momentum_set_stage_boundary(%s)' %self.domain
1425
1426    ##
1427    # @brief Calculate transmissive results.
1428    # @param vol_id
1429    # @param edge_id
1430    def evaluate(self, vol_id, edge_id):
1431        """Transmissive_n_momentum_zero_t_momentum_set_stage_boundary
1432        return the edge momentum values of the volume they serve.
1433        """
1434
1435        q = self.domain.get_conserved_quantities(vol_id, edge = edge_id)
1436
1437        normal = self.domain.get_normal(vol_id, edge_id)
1438
1439
1440        t = self.domain.get_time()
1441
1442        if hasattr(self.function, 'time'):
1443            # Roll boundary over if time exceeds
1444            while t > self.function.time[-1]:
1445                msg = 'WARNING: domain time %.2f has exceeded' % t
1446                msg += 'time provided in '
1447                msg += 'transmissive_momentum_set_stage_boundary object.\n'
1448                msg += 'I will continue, reusing the object from t==0'
1449                log.critical(msg)
1450                t -= self.function.time[-1]
1451
1452        value = self.function(t)
1453        try:
1454            x = float(value)
1455        except:
1456            x = float(value[0])
1457
1458        ## import math
1459        ## if vol_id == 9433:
1460        ##     print 'vol_id = ',vol_id, ' edge_id = ',edge_id, 'q = ', q, ' x = ',x
1461        ##     print 'normal = ', normal
1462        ##     print 'n . p = ', (normal[0]*q[1] + normal[1]*q[2])
1463        ##     print 't . p = ', (normal[1]*q[1] - normal[0]*q[2])
1464
1465
1466        q[0] = x
1467        ndotq = (normal[0]*q[1] + normal[1]*q[2])
1468        q[1] = normal[0]*ndotq
1469        q[2] = normal[1]*ndotq
1470
1471           
1472        return q
1473
1474##
1475# @brief A transmissive boundary, momentum set to zero.
1476# @note Inherits from Bouondary.
1477class Transmissive_stage_zero_momentum_boundary(Boundary):
1478    """Return same stage as those present in its neighbour volume.
1479    Set momentum to zero.
1480
1481    Underlying domain must be specified when boundary is instantiated
1482    """
1483
1484    ##
1485    # @brief Instantiate a Transmissive (zero momentum) boundary.
1486    # @param domain
1487    def __init__(self, domain=None):
1488        Boundary.__init__(self)
1489
1490        if domain is None:
1491            msg = ('Domain must be specified for '
1492                   'Transmissive_stage_zero_momentum boundary')
1493            raise Exception, msg
1494
1495        self.domain = domain
1496
1497    ##
1498    # @brief Return a representation of this instance.
1499    def __repr__(self):
1500        return 'Transmissive_stage_zero_momentum_boundary(%s)' % self.domain
1501
1502    ##
1503    # @brief Calculate transmissive (zero momentum) results.
1504    # @param vol_id
1505    # @param edge_id
1506    def evaluate(self, vol_id, edge_id):
1507        """Transmissive boundaries return the edge values
1508        of the volume they serve.
1509        """
1510
1511        q = self.domain.get_conserved_quantities(vol_id, edge=edge_id)
1512
1513        q[1] = q[2] = 0.0
1514        return q
1515
1516
1517##
1518# @brief Class for a Dirichlet discharge boundary.
1519# @note Inherits from Boundary.
1520class Dirichlet_discharge_boundary(Boundary):
1521    """
1522    Sets stage (stage0)
1523    Sets momentum (wh0) in the inward normal direction.
1524
1525    Underlying domain must be specified when boundary is instantiated
1526    """
1527
1528    ##
1529    # @brief Instantiate a Dirichlet discharge boundary.
1530    # @param domain
1531    # @param stage0
1532    # @param wh0
1533    def __init__(self, domain=None, stage0=None, wh0=None):
1534        Boundary.__init__(self)
1535
1536        if domain is None:
1537            msg = 'Domain must be specified for this type of boundary'
1538            raise Exception, msg
1539
1540        if stage0 is None:
1541            raise Exception, 'Stage must be specified for this type of boundary'
1542
1543        if wh0 is None:
1544            wh0 = 0.0
1545
1546        self.domain = domain
1547        self.stage0 = stage0
1548        self.wh0 = wh0
1549
1550    ##
1551    # @brief Return a representation of this instance.
1552    def __repr__(self):
1553        return 'Dirichlet_Discharge_boundary(%s)' % self.domain
1554
1555    ##
1556    # @brief Calculate Dirichlet discharge boundary results.
1557    # @param vol_id
1558    # @param edge_id
1559    def evaluate(self, vol_id, edge_id):
1560        """Set discharge in the (inward) normal direction"""
1561
1562        normal = self.domain.get_normal(vol_id,edge_id)
1563        q = [self.stage0, -self.wh0*normal[0], -self.wh0*normal[1]]
1564        return q
1565
1566        # FIXME: Consider this (taken from File_boundary) to allow
1567        # spatial variation
1568        # if vol_id is not None and edge_id is not None:
1569        #     i = self.boundary_indices[ vol_id, edge_id ]
1570        #     return self.F(t, point_id = i)
1571        # else:
1572        #     return self.F(t)
1573
1574
1575# Backward compatibility
1576# FIXME(Ole): Deprecate
1577##
1578# @brief Deprecated
1579class Dirichlet_Discharge_boundary(Dirichlet_discharge_boundary):
1580    pass
1581
1582
1583class Inflow_boundary(Boundary):
1584    """Apply given flow in m^3/s to boundary segment.
1585    Depth and momentum is derived using Manning's formula.
1586
1587    Underlying domain must be specified when boundary is instantiated
1588    """
1589   
1590    # FIXME (Ole): This is work in progress and definitely not finished.
1591    # The associated test has been disabled
1592
1593    def __init__(self, domain=None, rate=0.0):
1594        Boundary.__init__(self)
1595
1596        if domain is None:
1597            msg = 'Domain must be specified for '
1598            msg += 'Inflow boundary'
1599            raise Exception, msg
1600
1601        self.domain = domain
1602       
1603        # FIXME(Ole): Allow rate to be time dependent as well
1604        self.rate = rate
1605        self.tag = None # Placeholder for tag associated with this object.
1606
1607    def __repr__(self):
1608        return 'Inflow_boundary(%s)' %self.domain
1609
1610    def evaluate(self, vol_id, edge_id):
1611        """Apply inflow rate at each edge of this boundary
1612        """
1613       
1614        # First find all segments having the same tag is vol_id, edge_id
1615        # This will be done the first time evaluate is called.
1616        if self.tag is None:
1617            boundary = self.domain.boundary
1618            self.tag = boundary[(vol_id, edge_id)]       
1619           
1620            # Find total length of boundary with this tag
1621            length = 0.0
1622            for v_id, e_id in boundary:
1623                if self.tag == boundary[(v_id, e_id)]:
1624                    length += self.domain.mesh.get_edgelength(v_id, e_id)           
1625
1626            self.length = length
1627            self.average_momentum = self.rate/length
1628           
1629           
1630        # Average momentum has now been established across this boundary
1631        # Compute momentum in the inward normal direction
1632       
1633        inward_normal = -self.domain.mesh.get_normal(vol_id, edge_id)       
1634        xmomentum, ymomentum = self.average_momentum * inward_normal
1635           
1636        # Compute depth based on Manning's formula v = 1/n h^{2/3} sqrt(S)
1637        # Where v is velocity, n is manning's coefficient, h is depth and S is the slope into the domain.
1638        # Let mu be the momentum (vh), then this equation becomes: mu = 1/n h^{5/3} sqrt(S)
1639        # from which we can isolate depth to get
1640        # h = (mu n/sqrt(S) )^{3/5}
1641       
1642        slope = 0 # get gradient for this triangle dot normal
1643       
1644        # get manning coef from this triangle
1645        friction = self.domain.get_quantity('friction').get_values(location='edges', 
1646                                                                   indices=[vol_id])[0]
1647        mannings_n = friction[edge_id]
1648
1649        if slope > epsilon and mannings_n > epsilon:
1650            depth = pow(self.average_momentum * mannings_n/math.sqrt(slope), 3.0/5) 
1651        else:
1652            depth = 1.0
1653           
1654        # Elevation on this edge   
1655       
1656        z = self.domain.get_quantity('elevation').get_values(location='edges', 
1657                                                             indices=[vol_id])[0]
1658        elevation = z[edge_id]
1659           
1660        # Assign conserved quantities and return
1661        q = num.array([elevation + depth, xmomentum, ymomentum], num.Float)
1662        return q
1663
1664
1665       
1666   
1667           
1668       
1669class Field_boundary(Boundary):
1670    """Set boundary from given field represented in an sww file containing
1671    values for stage, xmomentum and ymomentum.
1672
1673    Optionally, the user can specify mean_stage to offset the stage provided
1674    in the sww file.
1675
1676    This function is a thin wrapper around the generic File_boundary. The
1677    difference between the file_boundary and field_boundary is only that the
1678    field_boundary will allow you to change the level of the stage height when
1679    you read in the boundary condition. This is very useful when running
1680    different tide heights in the same area as you need only to convert one
1681    boundary condition to a SWW file, ideally for tide height of 0 m
1682    (saving disk space). Then you can use field_boundary to read this SWW file
1683    and change the stage height (tide) on the fly depending on the scenario.
1684    """
1685
1686    ##
1687    # @brief Construct an instance of a 'field' boundary.
1688    # @param filename Name of SWW file containing stage, x and ymomentum.
1689    # @param domain Shallow water domain for which the boundary applies.
1690    # @param mean_stage Mean water level added to stage derived from SWW file.
1691    # @param time_thinning Time step thinning factor.
1692    # @param time_limit
1693    # @param boundary_polygon
1694    # @param default_boundary None or an instance of Boundary.
1695    # @param use_cache True if caching is to be used.
1696    # @param verbose True if this method is to be verbose.
1697    def __init__(self,
1698                 filename,
1699                 domain,
1700                 mean_stage=0.0,
1701                 time_thinning=1,
1702                 time_limit=None,
1703                 boundary_polygon=None,
1704                 default_boundary=None,
1705                 use_cache=False,
1706                 verbose=False):
1707        """Constructor
1708
1709        filename: Name of sww file
1710        domain: pointer to shallow water domain for which the boundary applies
1711        mean_stage: The mean water level which will be added to stage derived
1712                    from the boundary condition
1713        time_thinning: Will set how many time steps from the sww file read in
1714                       will be interpolated to the boundary. For example if
1715                       the sww file has 1 second time steps and is 24 hours
1716                       in length it has 86400 time steps. If you set
1717                       time_thinning to 1 it will read all these steps.
1718                       If you set it to 100 it will read every 100th step eg
1719                       only 864 step. This parameter is very useful to increase
1720                       the speed of a model run that you are setting up
1721                       and testing.
1722
1723        default_boundary: Must be either None or an instance of a
1724                          class descending from class Boundary.
1725                          This will be used in case model time exceeds
1726                          that available in the underlying data.
1727
1728                          Note that mean_stage will also be added to this.
1729                                               
1730        use_cache:
1731        verbose:
1732        """
1733
1734        # Create generic file_boundary object
1735        self.file_boundary = File_boundary(filename,
1736                                           domain,
1737                                           time_thinning=time_thinning,
1738                                           time_limit=time_limit,
1739                                           boundary_polygon=boundary_polygon,
1740                                           default_boundary=default_boundary,
1741                                           use_cache=use_cache,
1742                                           verbose=verbose)
1743
1744        # Record information from File_boundary
1745        self.F = self.file_boundary.F
1746        self.domain = self.file_boundary.domain
1747
1748        # Record mean stage
1749        self.mean_stage = mean_stage
1750
1751    ##
1752    # @note Generate a string representation of this instance.
1753    def __repr__(self):
1754        return 'Field boundary'
1755
1756    ##
1757    # @brief Calculate 'field' boundary results.
1758    # @param vol_id
1759    # @param edge_id
1760    def evaluate(self, vol_id=None, edge_id=None):
1761        """Return linearly interpolated values based on domain.time
1762
1763        vol_id and edge_id are ignored
1764        """
1765
1766        # Evaluate file boundary
1767        q = self.file_boundary.evaluate(vol_id, edge_id)
1768
1769        # Adjust stage
1770        for j, name in enumerate(self.domain.conserved_quantities):
1771            if name == 'stage':
1772                q[j] += self.mean_stage
1773        return q
1774
1775
1776################################################################################
1777# Standard forcing terms
1778################################################################################
1779
1780##
1781# @brief Apply gravitational pull in the presence of bed slope.
1782# @param domain The domain to apply gravity to.
1783# @note Wrapper for C function gravity_c().
1784def gravity(domain):
1785    """Apply gravitational pull in the presence of bed slope
1786    Wrapper calls underlying C implementation
1787    """
1788
1789    from shallow_water_ext import gravity as gravity_c
1790
1791    xmom = domain.quantities['xmomentum'].explicit_update
1792    ymom = domain.quantities['ymomentum'].explicit_update
1793
1794    stage = domain.quantities['stage']
1795    elevation = domain.quantities['elevation']
1796
1797    h = stage.centroid_values - elevation.centroid_values
1798    z = elevation.vertex_values
1799
1800    x = domain.get_vertex_coordinates()
1801    g = domain.g
1802
1803    gravity_c(g, h, z, x, xmom, ymom)    #, 1.0e-6)
1804
1805##
1806# @brief Apply friction to a surface (implicit).
1807# @param domain The domain to apply Manning friction to.
1808# @note Wrapper for C function manning_friction_c().
1809def manning_friction_implicit(domain):
1810    """Apply (Manning) friction to water momentum
1811    Wrapper for c version
1812    """
1813
1814    from shallow_water_ext import manning_friction as manning_friction_c
1815
1816    xmom = domain.quantities['xmomentum']
1817    ymom = domain.quantities['ymomentum']
1818
1819    w = domain.quantities['stage'].centroid_values
1820    z = domain.quantities['elevation'].centroid_values
1821
1822    uh = xmom.centroid_values
1823    vh = ymom.centroid_values
1824    eta = domain.quantities['friction'].centroid_values
1825
1826    xmom_update = xmom.semi_implicit_update
1827    ymom_update = ymom.semi_implicit_update
1828
1829    N = len(domain)
1830    eps = domain.minimum_allowed_height
1831    g = domain.g
1832
1833    manning_friction_c(g, eps, w, z, uh, vh, eta, xmom_update, ymom_update)
1834
1835##
1836# @brief Apply friction to a surface (explicit).
1837# @param domain The domain to apply Manning friction to.
1838# @note Wrapper for C function manning_friction_c().
1839def manning_friction_explicit(domain):
1840    """Apply (Manning) friction to water momentum
1841    Wrapper for c version
1842    """
1843
1844    from shallow_water_ext import manning_friction as manning_friction_c
1845
1846    xmom = domain.quantities['xmomentum']
1847    ymom = domain.quantities['ymomentum']
1848
1849    w = domain.quantities['stage'].centroid_values
1850    z = domain.quantities['elevation'].centroid_values
1851
1852    uh = xmom.centroid_values
1853    vh = ymom.centroid_values
1854    eta = domain.quantities['friction'].centroid_values
1855
1856    xmom_update = xmom.explicit_update
1857    ymom_update = ymom.explicit_update
1858
1859    N = len(domain)
1860    eps = domain.minimum_allowed_height
1861    g = domain.g
1862
1863    manning_friction_c(g, eps, w, z, uh, vh, eta, xmom_update, ymom_update)
1864
1865
1866# FIXME (Ole): This was implemented for use with one of the analytical solutions (Sampson?)
1867##
1868# @brief Apply linear friction to a surface.
1869# @param domain The domain to apply Manning friction to.
1870# @note Is this still used (30 Oct 2007)?
1871def linear_friction(domain):
1872    """Apply linear friction to water momentum
1873
1874    Assumes quantity: 'linear_friction' to be present
1875    """
1876
1877    from math import sqrt
1878
1879    w = domain.quantities['stage'].centroid_values
1880    z = domain.quantities['elevation'].centroid_values
1881    h = w-z
1882
1883    uh = domain.quantities['xmomentum'].centroid_values
1884    vh = domain.quantities['ymomentum'].centroid_values
1885    tau = domain.quantities['linear_friction'].centroid_values
1886
1887    xmom_update = domain.quantities['xmomentum'].semi_implicit_update
1888    ymom_update = domain.quantities['ymomentum'].semi_implicit_update
1889
1890    N = len(domain) # number_of_triangles
1891    eps = domain.minimum_allowed_height
1892    g = domain.g #Not necessary? Why was this added?
1893
1894    for k in range(N):
1895        if tau[k] >= eps:
1896            if h[k] >= eps:
1897                S = -tau[k]/h[k]
1898
1899                #Update momentum
1900                xmom_update[k] += S*uh[k]
1901                ymom_update[k] += S*vh[k]
1902
1903def depth_dependent_friction(domain, default_friction,
1904                             surface_roughness_data,
1905                             verbose=False):
1906    """Returns an array of friction values for each wet element adjusted for depth.
1907
1908    Inputs:
1909        domain - computational domain object
1910        default_friction - depth independent bottom friction
1911        surface_roughness_data - N x 5 array of n0, d1, n1, d2, n2 values for each
1912        friction region.
1913
1914    Outputs:
1915        wet_friction - Array that can be used directly to update friction as follows:
1916                       domain.set_quantity('friction', wet_friction)
1917
1918       
1919       
1920    """
1921
1922    import numpy as num
1923   
1924    # Create a temp array to store updated depth dependent friction for wet elements
1925    # EHR this is outwardly inneficient but not obvious how to avoid recreating each call??????
1926    N=len(domain)
1927    wet_friction    = num.zeros(N, num.float)
1928    wet_friction[:] = default_n0   # Initially assign default_n0 to all array so sure have no zeros values
1929   
1930   
1931    depth = domain.create_quantity_from_expression('stage - elevation')  # create depth instance for this timestep
1932    # Recompute depth as vector 
1933    d = depth.get_values(location='centroids')
1934 
1935    # rebuild the 'friction' values adjusted for depth at this instant
1936    for i in domain.get_wet_elements():                                  # loop for each wet element in domain
1937       
1938        # Get roughness data for each element
1939        n0 = float(surface_roughness_data[i,0])
1940        d1 = float(surface_roughness_data[i,1])
1941        n1 = float(surface_roughness_data[i,2])
1942        d2 = float(surface_roughness_data[i,3])
1943        n2 = float(surface_roughness_data[i,4])
1944       
1945       
1946        # Recompute friction values from depth for this element
1947               
1948        if d[i]   <= d1: 
1949            depth_dependent_friction = n1
1950        elif d[i] >= d2:
1951            depth_dependent_friction = n2
1952        else:
1953            depth_dependent_friction = n1+((n2-n1)/(d2-d1))*(d[i]-d1)
1954           
1955        # check sanity of result
1956        if (depth_dependent_friction  < 0.010 or depth_dependent_friction > 9999.0) :
1957            log.critical('%s >>>> WARNING: computed depth_dependent friction '
1958                         'out of range, ddf%f, n1=%f, n2=%f'
1959                         % (model_data.basename,
1960                            depth_dependent_friction, n1, n2))
1961       
1962        # update depth dependent friction  for that wet element
1963        wet_friction[i] = depth_dependent_friction
1964       
1965    # EHR add code to show range of 'friction across domain at this instant as sanity check?????????
1966   
1967    if verbose :
1968        nvals=domain.get_quantity('friction').get_values(location='centroids')        # return array of domain nvals
1969        n_min=min(nvals)
1970        n_max=max(nvals)
1971       
1972        log.critical('         ++++ calculate_depth_dependent_friction - '
1973                     'Updated friction - range  %7.3f to %7.3f'
1974                     % (n_min, n_max))
1975   
1976    return wet_friction
1977
1978
1979################################################################################
1980# Experimental auxiliary functions
1981################################################################################
1982
1983##
1984# @brief Check forcefield parameter.
1985# @param f Object to check.
1986# @note 'f' may be a callable object or a scalar value.
1987def check_forcefield(f):
1988    """Check that force object is as expected.
1989   
1990    Check that f is either:
1991    1: a callable object f(t,x,y), where x and y are vectors
1992       and that it returns an array or a list of same length
1993       as x and y
1994    2: a scalar
1995    """
1996
1997    if callable(f):
1998        N = 3
1999        x = num.ones(3, num.float)
2000        y = num.ones(3, num.float)
2001        try:
2002            q = f(1.0, x=x, y=y)
2003        except Exception, e:
2004            msg = 'Function %s could not be executed:\n%s' %(f, e)
2005            # FIXME: Reconsider this semantics
2006            raise Exception, msg
2007
2008        try:
2009            q = num.array(q, num.float)
2010        except:
2011            msg = ('Return value from vector function %s could not '
2012                   'be converted into a numeric array of floats.\nSpecified '
2013                   'function should return either list or array.' % f)
2014            raise Exception, msg
2015
2016        # Is this really what we want?
2017        # info is "(func name, filename, defining line)"
2018        func_info = (f.func_name, f.func_code.co_filename,
2019                     f.func_code.co_firstlineno)
2020        func_msg = 'Function %s (defined in %s, line %d)' % func_info
2021        try:
2022            result_len = len(q)
2023        except:
2024            msg = '%s must return vector' % func_msg
2025            self.fail(msg)
2026        msg = '%s must return vector of length %d' % (func_msg, N)
2027        assert result_len == N, msg
2028    else:
2029        try:
2030            f = float(f)
2031        except:
2032            msg = ('Force field %s must be a scalar value coercible to float.'
2033                   % str(f))
2034            raise Exception, msg
2035
2036    return f
2037
2038
2039##
2040# Class to apply a wind stress to a domain.
2041class Wind_stress:
2042    """Apply wind stress to water momentum in terms of
2043    wind speed [m/s] and wind direction [degrees]
2044    """
2045
2046    ##
2047    # @brief Create an instance of Wind_stress.
2048    # @param *args
2049    # @param **kwargs
2050    def __init__(self, *args, **kwargs):
2051        """Initialise windfield from wind speed s [m/s]
2052        and wind direction phi [degrees]
2053
2054        Inputs v and phi can be either scalars or Python functions, e.g.
2055
2056        W = Wind_stress(10, 178)
2057
2058        #FIXME - 'normal' degrees are assumed for now, i.e. the
2059        vector (1,0) has zero degrees.
2060        We may need to convert from 'compass' degrees later on and also
2061        map from True north to grid north.
2062
2063        Arguments can also be Python functions of t,x,y as in
2064
2065        def speed(t,x,y):
2066            ...
2067            return s
2068
2069        def angle(t,x,y):
2070            ...
2071            return phi
2072
2073        where x and y are vectors.
2074
2075        and then pass the functions in
2076
2077        W = Wind_stress(speed, angle)
2078
2079        The instantiated object W can be appended to the list of
2080        forcing_terms as in
2081
2082        Alternatively, one vector valued function for (speed, angle)
2083        can be applied, providing both quantities simultaneously.
2084        As in
2085        W = Wind_stress(F), where returns (speed, angle) for each t.
2086
2087        domain.forcing_terms.append(W)
2088        """
2089
2090        from anuga.config import rho_a, rho_w, eta_w
2091
2092        if len(args) == 2:
2093            s = args[0]
2094            phi = args[1]
2095        elif len(args) == 1:
2096            # Assume vector function returning (s, phi)(t,x,y)
2097            vector_function = args[0]
2098            s = lambda t,x,y: vector_function(t,x=x,y=y)[0]
2099            phi = lambda t,x,y: vector_function(t,x=x,y=y)[1]
2100        else:
2101           # Assume info is in 2 keyword arguments
2102           if len(kwargs) == 2:
2103               s = kwargs['s']
2104               phi = kwargs['phi']
2105           else:
2106               raise Exception, 'Assumes two keyword arguments: s=..., phi=....'
2107
2108        self.speed = check_forcefield(s)
2109        self.phi = check_forcefield(phi)
2110
2111        self.const = eta_w*rho_a/rho_w
2112
2113    ##
2114    # @brief 'execute' this class instance.
2115    # @param domain
2116    def __call__(self, domain):
2117        """Evaluate windfield based on values found in domain"""
2118
2119        from math import pi, cos, sin, sqrt
2120
2121        xmom_update = domain.quantities['xmomentum'].explicit_update
2122        ymom_update = domain.quantities['ymomentum'].explicit_update
2123
2124        N = len(domain)    # number_of_triangles
2125        t = domain.time
2126
2127        if callable(self.speed):
2128            xc = domain.get_centroid_coordinates()
2129            s_vec = self.speed(t, xc[:,0], xc[:,1])
2130        else:
2131            # Assume s is a scalar
2132            try:
2133                s_vec = self.speed * num.ones(N, num.float)
2134            except:
2135                msg = 'Speed must be either callable or a scalar: %s' %self.s
2136                raise msg
2137
2138        if callable(self.phi):
2139            xc = domain.get_centroid_coordinates()
2140            phi_vec = self.phi(t, xc[:,0], xc[:,1])
2141        else:
2142            # Assume phi is a scalar
2143
2144            try:
2145                phi_vec = self.phi * num.ones(N, num.float)
2146            except:
2147                msg = 'Angle must be either callable or a scalar: %s' %self.phi
2148                raise msg
2149
2150        assign_windfield_values(xmom_update, ymom_update,
2151                                s_vec, phi_vec, self.const)
2152
2153
2154##
2155# @brief Assign wind field values
2156# @param xmom_update
2157# @param ymom_update
2158# @param s_vec
2159# @param phi_vec
2160# @param const
2161def assign_windfield_values(xmom_update, ymom_update,
2162                            s_vec, phi_vec, const):
2163    """Python version of assigning wind field to update vectors.
2164    A C version also exists (for speed)
2165    """
2166
2167    from math import pi, cos, sin, sqrt
2168
2169    N = len(s_vec)
2170    for k in range(N):
2171        s = s_vec[k]
2172        phi = phi_vec[k]
2173
2174        # Convert to radians
2175        phi = phi*pi/180
2176
2177        # Compute velocity vector (u, v)
2178        u = s*cos(phi)
2179        v = s*sin(phi)
2180
2181        # Compute wind stress
2182        S = const * sqrt(u**2 + v**2)
2183        xmom_update[k] += S*u
2184        ymom_update[k] += S*v
2185
2186
2187##
2188# @brief A class for a general explicit forcing term.
2189class General_forcing:
2190    """General explicit forcing term for update of quantity
2191
2192    This is used by Inflow and Rainfall for instance
2193
2194
2195    General_forcing(quantity_name, rate, center, radius, polygon)
2196
2197    domain:     ANUGA computational domain
2198    quantity_name: Name of quantity to update.
2199                   It must be a known conserved quantity.
2200
2201    rate [?/s]: Total rate of change over the specified area.
2202                This parameter can be either a constant or a
2203                function of time. Positive values indicate increases,
2204                negative values indicate decreases.
2205                Rate can be None at initialisation but must be specified
2206                before forcing term is applied (i.e. simulation has started).
2207
2208    center [m]: Coordinates at center of flow point
2209    radius [m]: Size of circular area
2210    polygon:    Arbitrary polygon
2211    default_rate: Rate to be used if rate fails (e.g. if model time exceeds its data)
2212                  Admissible types: None, constant number or function of t
2213
2214
2215    Either center, radius or polygon can be specified but not both.
2216    If neither are specified the entire domain gets updated.
2217    All coordinates to be specified in absolute UTM coordinates (x, y) assuming the zone of domain.
2218
2219    Inflow or Rainfall for examples of use
2220    """
2221
2222
2223    # FIXME (AnyOne) : Add various methods to allow spatial variations
2224
2225    ##
2226    # @brief Create an instance of this forcing term.
2227    # @param domain
2228    # @param quantity_name
2229    # @param rate
2230    # @param center
2231    # @param radius
2232    # @param polygon
2233    # @param default_rate
2234    # @param verbose
2235    def __init__(self,
2236                 domain,
2237                 quantity_name,
2238                 rate=0.0,
2239                 center=None,
2240                 radius=None,
2241                 polygon=None,
2242                 default_rate=None,
2243                 verbose=False):
2244
2245        from math import pi, cos, sin
2246
2247        if center is None:
2248            msg = 'I got radius but no center.'
2249            assert radius is None, msg
2250
2251        if radius is None:
2252            msg += 'I got center but no radius.'
2253            assert center is None, msg
2254
2255        self.domain = domain
2256        self.quantity_name = quantity_name
2257        self.rate = rate
2258        self.center = ensure_numeric(center)
2259        self.radius = radius
2260        self.polygon = polygon
2261        self.verbose = verbose
2262        self.value = 0.0    # Can be used to remember value at
2263                            # previous timestep in order to obtain rate
2264
2265        # Get boundary (in absolute coordinates)
2266        bounding_polygon = domain.get_boundary_polygon()
2267
2268        # Update area if applicable
2269        if center is not None and radius is not None:
2270            assert len(center) == 2
2271            msg = 'Polygon cannot be specified when center and radius are'
2272            assert polygon is None, msg
2273
2274            # Check that circle center lies within the mesh.
2275            msg = 'Center %s specified for forcing term did not' % str(center)
2276            msg += 'fall within the domain boundary.'
2277            assert is_inside_polygon(center, bounding_polygon), msg
2278
2279            # Check that circle periphery lies within the mesh.
2280            N = 100
2281            periphery_points = []
2282            for i in range(N):
2283                theta = 2*pi*i/100
2284
2285                x = center[0] + radius*cos(theta)
2286                y = center[1] + radius*sin(theta)
2287
2288                periphery_points.append([x,y])
2289
2290            for point in periphery_points:
2291                msg = 'Point %s on periphery for forcing term' % str(point)
2292                msg += ' did not fall within the domain boundary.'
2293                assert is_inside_polygon(point, bounding_polygon), msg
2294
2295        if polygon is not None:
2296            # Check that polygon lies within the mesh.
2297            for point in self.polygon:
2298                msg = 'Point %s in polygon for forcing term' % str(point)
2299                msg += ' did not fall within the domain boundary.'
2300                assert is_inside_polygon(point, bounding_polygon), msg
2301
2302        # Pointer to update vector
2303        self.update = domain.quantities[self.quantity_name].explicit_update
2304
2305        # Determine indices in flow area
2306        N = len(domain)
2307        points = domain.get_centroid_coordinates(absolute=True)
2308
2309        # Calculate indices in exchange area for this forcing term
2310        self.exchange_indices = None
2311        if self.center is not None and self.radius is not None:
2312            # Inlet is circular
2313            inlet_region = 'center=%s, radius=%s' % (self.center, self.radius)
2314
2315            self.exchange_indices = []
2316            for k in range(N):
2317                x, y = points[k,:]    # Centroid
2318
2319                c = self.center
2320                if ((x-c[0])**2+(y-c[1])**2) < self.radius**2:
2321                    self.exchange_indices.append(k)
2322
2323        if self.polygon is not None:
2324            # Inlet is polygon
2325            inlet_region = 'polygon=%s' % (self.polygon) 
2326            self.exchange_indices = inside_polygon(points, self.polygon)
2327
2328        if self.exchange_indices is None:
2329            self.exchange_area = polygon_area(bounding_polygon)
2330        else:   
2331            if len(self.exchange_indices) == 0:
2332                msg = 'No triangles have been identified in '
2333                msg += 'specified region: %s' % inlet_region
2334                raise Exception, msg
2335
2336            # Compute exchange area as the sum of areas of triangles identified
2337            # by circle or polygon
2338            self.exchange_area = 0.0
2339            for i in self.exchange_indices:
2340                self.exchange_area += domain.areas[i]
2341           
2342
2343        msg = 'Exchange area in forcing term'
2344        msg += ' has area = %f' %self.exchange_area
2345        assert self.exchange_area > 0.0           
2346           
2347               
2348
2349           
2350        # Check and store default_rate
2351        msg = ('Keyword argument default_rate must be either None '
2352               'or a function of time.\nI got %s.' % str(default_rate))
2353        assert (default_rate is None or
2354                type(default_rate) in [IntType, FloatType] or
2355                callable(default_rate)), msg
2356
2357        if default_rate is not None:
2358            # If it is a constant, make it a function
2359            if not callable(default_rate):
2360                tmp = default_rate
2361                default_rate = lambda t: tmp
2362
2363            # Check that default_rate is a function of one argument
2364            try:
2365                default_rate(0.0)
2366            except:
2367                raise Exception, msg
2368
2369        self.default_rate = default_rate
2370        self.default_rate_invoked = False    # Flag
2371
2372    ##
2373    # @brief Execute this instance.
2374    # @param domain
2375    def __call__(self, domain):
2376        """Apply inflow function at time specified in domain, update stage"""
2377
2378        # Call virtual method allowing local modifications
2379        t = domain.get_time()
2380        try:
2381            rate = self.update_rate(t)
2382        except Modeltime_too_early, e:
2383            raise Modeltime_too_early, e
2384        except Modeltime_too_late, e:
2385            if self.default_rate is None:
2386                raise Exception, e    # Reraise exception
2387            else:
2388                # Pass control to default rate function
2389                rate = self.default_rate(t)
2390
2391                if self.default_rate_invoked is False:
2392                    # Issue warning the first time
2393                    msg = ('%s\n'
2394                           'Instead I will use the default rate: %s\n'
2395                           'Note: Further warnings will be supressed'
2396                           % (str(e), str(self.default_rate)))
2397                    warn(msg)
2398
2399                    # FIXME (Ole): Replace this crude flag with
2400                    # Python's ability to print warnings only once.
2401                    # See http://docs.python.org/lib/warning-filter.html
2402                    self.default_rate_invoked = True
2403
2404        if rate is None:
2405            msg = ('Attribute rate must be specified in General_forcing '
2406                   'or its descendants before attempting to call it')
2407            raise Exception, msg
2408
2409        # Now rate is a number
2410        if self.verbose is True:
2411            log.critical('Rate of %s at time = %.2f = %f'
2412                         % (self.quantity_name, domain.get_time(), rate))
2413
2414        if self.exchange_indices is None:
2415            self.update[:] += rate
2416        else:
2417            # Brute force assignment of restricted rate
2418            for k in self.exchange_indices:
2419                self.update[k] += rate
2420
2421    ##
2422    # @brief Update the internal rate.
2423    # @param t A callable or scalar used to set the rate.
2424    # @return The new rate.
2425    def update_rate(self, t):
2426        """Virtual method allowing local modifications by writing an
2427        overriding version in descendant
2428        """
2429
2430        if callable(self.rate):
2431            rate = self.rate(t)
2432        else:
2433            rate = self.rate
2434
2435        return rate
2436
2437    ##
2438    # @brief Get values for the specified quantity.
2439    # @param quantity_name Name of the quantity of interest.
2440    # @return The value(s) of the quantity.
2441    # @note If 'quantity_name' is None, use self.quantity_name.
2442    def get_quantity_values(self, quantity_name=None):
2443        """Return values for specified quantity restricted to opening
2444
2445        Optionally a quantity name can be specified if values from another
2446        quantity is sought
2447        """
2448
2449        if quantity_name is None:
2450            quantity_name = self.quantity_name
2451
2452        q = self.domain.quantities[quantity_name]
2453        return q.get_values(location='centroids',
2454                            indices=self.exchange_indices)
2455
2456    ##
2457    # @brief Set value for the specified quantity.
2458    # @param val The value object used to set value.
2459    # @param quantity_name Name of the quantity of interest.
2460    # @note If 'quantity_name' is None, use self.quantity_name.
2461    def set_quantity_values(self, val, quantity_name=None):
2462        """Set values for specified quantity restricted to opening
2463
2464        Optionally a quantity name can be specified if values from another
2465        quantity is sought
2466        """
2467
2468        if quantity_name is None:
2469            quantity_name = self.quantity_name
2470
2471        q = self.domain.quantities[self.quantity_name]
2472        q.set_values(val,
2473                     location='centroids',
2474                     indices=self.exchange_indices)
2475
2476
2477##
2478# @brief A class for rainfall forcing function.
2479# @note Inherits from General_forcing.
2480class Rainfall(General_forcing):
2481    """Class Rainfall - general 'rain over entire domain' forcing term.
2482
2483    Used for implementing Rainfall over the entire domain.
2484
2485        Current Limited to only One Gauge..
2486
2487        Need to add Spatial Varying Capability
2488        (This module came from copying and amending the Inflow Code)
2489
2490    Rainfall(rain)
2491
2492    domain
2493    rain [mm/s]:  Total rain rate over the specified domain.
2494                  NOTE: Raingauge Data needs to reflect the time step.
2495                  IE: if Gauge is mm read at a time step, then the input
2496                  here is as mm/(timeStep) so 10mm in 5minutes becomes
2497                  10/(5x60) = 0.0333mm/s.
2498
2499                  This parameter can be either a constant or a
2500                  function of time. Positive values indicate inflow,
2501                  negative values indicate outflow.
2502                  (and be used for Infiltration - Write Seperate Module)
2503                  The specified flow will be divided by the area of
2504                  the inflow region and then applied to update the
2505                  stage quantity.
2506
2507    polygon: Specifies a polygon to restrict the rainfall.
2508
2509    Examples
2510    How to put them in a run File...
2511
2512    #------------------------------------------------------------------------
2513    # Setup specialised forcing terms
2514    #------------------------------------------------------------------------
2515    # This is the new element implemented by Ole and Rudy to allow direct
2516    # input of Rainfall in mm/s
2517
2518    catchmentrainfall = Rainfall(rain=file_function('Q100_2hr_Rain.tms'))
2519                        # Note need path to File in String.
2520                        # Else assumed in same directory
2521
2522    domain.forcing_terms.append(catchmentrainfall)
2523    """
2524
2525    ##
2526    # @brief Create an instance of the class.
2527    # @param domain Domain of interest.
2528    # @param rate Total rain rate over the specified domain (mm/s).
2529    # @param center
2530    # @param radius
2531    # @param polygon Polygon  to restrict rainfall.
2532    # @param default_rate
2533    # @param verbose True if this instance is to be verbose.
2534    def __init__(self,
2535                 domain,
2536                 rate=0.0,
2537                 center=None,
2538                 radius=None,
2539                 polygon=None,
2540                 default_rate=None,
2541                 verbose=False):
2542
2543        # Converting mm/s to m/s to apply in ANUGA)
2544        if callable(rate):
2545            rain = lambda t: rate(t)/1000.0
2546        else:
2547            rain = rate/1000.0
2548
2549        if default_rate is not None:
2550            if callable(default_rate):
2551                default_rain = lambda t: default_rate(t)/1000.0
2552            else:
2553                default_rain = default_rate/1000.0
2554        else:
2555            default_rain = None
2556
2557           
2558           
2559        General_forcing.__init__(self,
2560                                 domain,
2561                                 'stage',
2562                                 rate=rain,
2563                                 center=center,
2564                                 radius=radius,
2565                                 polygon=polygon,
2566                                 default_rate=default_rain,
2567                                 verbose=verbose)
2568
2569
2570##
2571# @brief A class for inflow (rain and drain) forcing function.
2572# @note Inherits from General_forcing.
2573class Inflow(General_forcing):
2574    """Class Inflow - general 'rain and drain' forcing term.
2575
2576    Useful for implementing flows in and out of the domain.
2577
2578    Inflow(flow, center, radius, polygon)
2579
2580    domain
2581    rate [m^3/s]: Total flow rate over the specified area.
2582                  This parameter can be either a constant or a
2583                  function of time. Positive values indicate inflow,
2584                  negative values indicate outflow.
2585                  The specified flow will be divided by the area of
2586                  the inflow region and then applied to update stage.
2587    center [m]: Coordinates at center of flow point
2588    radius [m]: Size of circular area
2589    polygon:    Arbitrary polygon.
2590
2591    Either center, radius or polygon must be specified
2592
2593    Examples
2594
2595    # Constant drain at 0.003 m^3/s.
2596    # The outflow area is 0.07**2*pi=0.0154 m^2
2597    # This corresponds to a rate of change of 0.003/0.0154 = 0.2 m/s
2598    #
2599    Inflow((0.7, 0.4), 0.07, -0.003)
2600
2601
2602    # Tap turning up to a maximum inflow of 0.0142 m^3/s.
2603    # The inflow area is 0.03**2*pi = 0.00283 m^2
2604    # This corresponds to a rate of change of 0.0142/0.00283 = 5 m/s
2605    # over the specified area
2606    Inflow((0.5, 0.5), 0.03, lambda t: min(0.01*t, 0.0142))
2607
2608
2609    #------------------------------------------------------------------------
2610    # Setup specialised forcing terms
2611    #------------------------------------------------------------------------
2612    # This is the new element implemented by Ole to allow direct input
2613    # of Inflow in m^3/s
2614
2615    hydrograph = Inflow(center=(320, 300), radius=10,
2616                        rate=file_function('Q/QPMF_Rot_Sub13.tms'))
2617
2618    domain.forcing_terms.append(hydrograph)
2619    """
2620
2621    ##
2622    # @brief Create an instance of the class.
2623    # @param domain Domain of interest.
2624    # @param rate Total rain rate over the specified domain (mm/s).
2625    # @param center
2626    # @param radius
2627    # @param polygon Polygon  to restrict rainfall.
2628    # @param default_rate
2629    # @param verbose True if this instance is to be verbose.
2630    def __init__(self,
2631                 domain,
2632                 rate=0.0,
2633                 center=None,
2634                 radius=None,
2635                 polygon=None,
2636                 default_rate=None,
2637                 verbose=False):
2638        # Create object first to make area is available
2639        General_forcing.__init__(self,
2640                                 domain,
2641                                 'stage',
2642                                 rate=rate,
2643                                 center=center,
2644                                 radius=radius,
2645                                 polygon=polygon,
2646                                 default_rate=default_rate,
2647                                 verbose=verbose)
2648
2649    ##
2650    # @brief Update the instance rate.
2651    # @param t New rate object.
2652    def update_rate(self, t):
2653        """Virtual method allowing local modifications by writing an
2654        overriding version in descendant
2655
2656        This one converts m^3/s to m/s which can be added directly
2657        to 'stage' in ANUGA
2658        """
2659
2660        if callable(self.rate):
2661            _rate = self.rate(t)/self.exchange_area
2662        else:
2663            _rate = self.rate/self.exchange_area
2664
2665        return _rate
2666
2667
2668################################################################################
2669# Initialise module
2670################################################################################
2671
2672from anuga.utilities import compile
2673if compile.can_use_C_extension('shallow_water_ext.c'):
2674    # Underlying C implementations can be accessed
2675    from shallow_water_ext import rotate, assign_windfield_values
2676else:
2677    msg = 'C implementations could not be accessed by %s.\n ' % __file__
2678    msg += 'Make sure compile_all.py has been run as described in '
2679    msg += 'the ANUGA installation guide.'
2680    raise Exception, msg
2681
2682# Optimisation with psyco
2683from anuga.config import use_psyco
2684if use_psyco:
2685    try:
2686        import psyco
2687    except:
2688        import os
2689        if os.name == 'posix' and os.uname()[4] in ['x86_64', 'ia64']:
2690            pass
2691            #Psyco isn't supported on 64 bit systems, but it doesn't matter
2692        else:
2693            msg = ('WARNING: psyco (speedup) could not be imported, '
2694                   'you may want to consider installing it')
2695            log.critical(msg)
2696    else:
2697        psyco.bind(Domain.distribute_to_vertices_and_edges)
2698        psyco.bind(Domain.compute_fluxes)
2699
2700
2701if __name__ == "__main__":
2702    pass
Note: See TracBrowser for help on using the repository browser.