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

Last change on this file since 7519 was 7519, checked in by steve, 15 years ago

Have been playing with using a slope limited velocity to calculate
fluxes (hence the addition of evolved_quantities as well as conserved
quantities.

But the commit is to fix a problem Rudy found with sww2dem. Seems
numpy.array2string is a little too clever, in that it summarizes
output if there is a long sequence of zeros to
[0.0, 0.0, 0.0, ... 0.0, 0.0 ,0.0] To get around this I have added
a call to numpy.set_options(threshold=sys.max_int) to turn this
behaviour off!

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