source: inundation/ga/storm_surge/pyvolution/domain.py @ 1632

Last change on this file since 1632 was 1581, checked in by steve, 20 years ago
File size: 22.5 KB
Line 
1"""Class Domain - 2D triangular domains for finite-volume computations of
2   the shallow water wave equation
3
4
5   Copyright 2004
6   Ole Nielsen, Stephen Roberts, Duncan Gray, Christopher Zoppou
7   Geoscience Australia
8"""
9
10from mesh import Mesh
11from generic_boundary_conditions import *
12import types
13
14class Domain(Mesh):
15
16    def __init__(self, coordinates, vertices, boundary = None,
17                 conserved_quantities = None, other_quantities = None,
18                 tagged_elements = None, geo_reference = None, use_inscribed_circle=False):
19
20        Mesh.__init__(self, coordinates, vertices, boundary,
21                      tagged_elements, geo_reference, use_inscribed_circle)
22
23        from Numeric import zeros, Float, Int
24        from quantity import Quantity, Conserved_quantity
25
26        #List of quantity names entering
27        #the conservation equations
28        #(Must be a subset of quantities)
29        if conserved_quantities is None:
30            self.conserved_quantities = []
31        else:
32            self.conserved_quantities = conserved_quantities
33
34        if other_quantities is None:
35            self.other_quantities = []
36        else:
37            self.other_quantities = other_quantities
38
39
40        #Build dictionary of Quantity instances keyed by quantity names
41        self.quantities = {}
42
43        #FIXME: remove later - maybe OK, though....
44        for name in self.conserved_quantities:
45            self.quantities[name] = Conserved_quantity(self)
46        for name in self.other_quantities:
47            self.quantities[name] = Quantity(self)
48
49        #Create an empty list for explicit forcing terms
50        self.forcing_terms = []
51
52
53        #Defaults
54        from config import max_smallsteps, beta_w, beta_h, epsilon, CFL
55        self.beta_w = beta_w
56        self.beta_h = beta_h
57        self.epsilon = epsilon
58
59        #FIXME: Maybe have separate orders for h-limiter and w-limiter?
60        #Or maybe get rid of order altogether and use beta_w and beta_h
61        self.default_order = 1
62        self.order = self.default_order
63        self.smallsteps = 0
64        self.max_smallsteps = max_smallsteps
65        self.number_of_steps = 0
66        self.number_of_first_order_steps = 0
67        self.CFL = CFL
68
69        #Model time
70        self.time = 0.0
71        self.finaltime = None
72        self.min_timestep = self.max_timestep = 0.0
73        self.starttime = 0 #Physical starttime if any (0 is 1 Jan 1970 00:00:00)
74        #Origin in UTM coordinates
75        #FIXME: This should be set if read by a msh file
76        #self.zone = zone
77        #self.xllcorner = xllcorner
78        #self.yllcorner = yllcorner
79
80
81        #Checkpointing and storage
82        from config import default_datadir
83        self.datadir = default_datadir
84        self.filename = 'domain'
85        self.checkpoint = False
86
87        #MH310505 To avoid calculating the flux across each edge twice, keep an integer (boolean) array,
88        #to be used during the flux calculation
89        N=self.number_of_elements
90        self.already_computed_flux = zeros((N, 3), Int)
91
92    #Public interface to Domain
93    def get_conserved_quantities(self, vol_id, vertex=None, edge=None):
94        """Get conserved quantities at volume vol_id
95
96        If vertex is specified use it as index for vertex values
97        If edge is specified use it as index for edge values
98        If neither are specified use centroid values
99        If both are specified an exeception is raised
100
101        Return value: Vector of length == number_of_conserved quantities
102
103        """
104
105        from Numeric import zeros, Float
106
107        if not (vertex is None or edge is None):
108            msg = 'Values for both vertex and edge was specified.'
109            msg += 'Only one (or none) is allowed.'
110            raise msg
111
112        q = zeros( len(self.conserved_quantities), Float)
113
114        for i, name in enumerate(self.conserved_quantities):
115            Q = self.quantities[name]
116            if vertex is not None:
117                q[i] = Q.vertex_values[vol_id, vertex]
118            elif edge is not None:
119                q[i] = Q.edge_values[vol_id, edge]
120            else:
121                q[i] = Q.centroid_values[vol_id]
122
123        return q
124
125
126    def set_quantity_vertices_dict(self, quantity_dict):
127        """Set values for named quantities.
128        The index is the quantity
129
130        name: Name of quantity
131        X: Compatible list, Numeric array, const or function (see below)
132
133        The values will be stored in elements following their
134        internal ordering.
135
136        """
137        for key in quantity_dict.keys():
138            self.set_quantity(key, quantity_dict[key], location='vertices')
139
140
141    def set_quantity(self, name, X, location='vertices', indexes = None):
142        """Set values for named quantity
143
144        name: Name of quantity
145        X: Compatible list, Numeric array, const or function (see below)
146        location: Where values are to be stored.
147                  Permissible options are: vertices, edges, centroids
148
149        In case of location == 'centroids' the dimension values must
150        be a list of a Numerical array of length N, N being the number
151        of elements. Otherwise it must be of dimension Nx3.
152
153        Indexes is the set of element ids that the operation applies to
154
155        The values will be stored in elements following their
156        internal ordering.
157
158        #FIXME (Ole): I suggest the following interface
159        set_quantity(name, X, location, region)
160        where
161            name: Name of quantity
162            X:
163              -Compatible list,
164              -Numeric array,
165              -const or function (see below)
166              -another quantity Q or an expression of the form
167              a*Q+b, where a is a scalar or a compatible array or a quantity
168              Q is a quantity
169              b is either a scalar, a quantity or a compatible array
170            location: Where values are to be stored.
171                      Permissible options are: vertices, edges, centroid
172            region: Identify subset of triangles. Permissible values are
173                    - tag name (refers to tagged region)
174                    - indices (refers to specific triangles)
175                    - polygon (identifies region)
176
177            This should work for all values of X
178
179
180
181        """
182
183        #from quantity import Quantity, Conserved_quantity
184
185        #Create appropriate quantity object
186        ##if name in self.conserved_quantities:
187        ##    self.quantities[name] = Conserved_quantity(self)
188        ##else:
189        ##    self.quantities[name] = Quantity(self)
190
191        #Set value
192        self.quantities[name].set_values(X, location, indexes = indexes)
193
194
195    def get_quantity(self, name, location='vertices', indexes = None):
196        """Get values for named quantity
197
198        name: Name of quantity
199
200        In case of location == 'centroids' the dimension values must
201        be a list of a Numerical array of length N, N being the number
202        of elements. Otherwise it must be of dimension Nx3.
203
204        Indexes is the set of element ids that the operation applies to.
205
206        The values will be stored in elements following their
207        internal ordering.
208        """
209
210        return self.quantities[name].get_values( location, indexes = indexes)
211
212
213    def set_boundary(self, boundary_map):
214        """Associate boundary objects with tagged boundary segments.
215
216        Input boundary_map is a dictionary of boundary objects keyed
217        by symbolic tags to matched against tags in the internal dictionary
218        self.boundary.
219
220        As result one pointer to a boundary object is stored for each vertex
221        in the list self.boundary_objects.
222        More entries may point to the same boundary object
223
224        Schematically the mapping is from two dictionaries to one list
225        where the index is used as pointer to the boundary_values arrays
226        within each quantity.
227
228        self.boundary:          (vol_id, edge_id): tag
229        boundary_map (input):   tag: boundary_object
230        ----------------------------------------------
231        self.boundary_objects:  ((vol_id, edge_id), boundary_object)
232
233
234        Pre-condition:
235          self.boundary has been built.
236
237        Post-condition:
238          self.boundary_objects is built
239
240        If a tag from the domain doesn't appear in the input dictionary an
241        exception is raised.
242        However, if a tag is not used to the domain, no error is thrown.
243        FIXME: This would lead to implementation of a
244        default boundary condition
245
246        Note: If a segment is listed in the boundary dictionary and if it is
247        not None, it *will* become a boundary -
248        even if there is a neighbouring triangle.
249        This would be the case for internal boundaries
250
251        Boundary objects that are None will be skipped.
252
253        FIXME: If set_boundary is called multiple times and if Boundary
254        object is changed into None, the neighbour structure will not be
255        restored!!!
256
257
258        """
259
260        self.boundary_objects = []
261        self.boundary_map = boundary_map  #Store for use with eg. boundary_stats.
262
263        #FIXME: Try to remove the sorting and fix test_mesh.py
264        x = self.boundary.keys()
265        x.sort()
266
267        #Loop through edges that lie on the boundary and associate them with
268        #callable boundary objects depending on their tags
269        for k, (vol_id, edge_id) in enumerate(x):
270            tag = self.boundary[ (vol_id, edge_id) ]
271
272            if boundary_map.has_key(tag):
273                B = boundary_map[tag]  #Get callable boundary object
274
275                if B is not None:
276                    self.boundary_objects.append( ((vol_id, edge_id), B) )
277                    self.neighbours[vol_id, edge_id] = -len(self.boundary_objects)
278                else:
279                    pass
280                    #FIXME: Check and perhaps fix neighbour structure
281
282
283            else:
284                msg = 'ERROR (domain.py): Tag "%s" has not been ' %tag
285                msg += 'bound to a boundary object.\n'
286                msg += 'All boundary tags defined in domain must appear '
287                msg += 'in the supplied dictionary.\n'
288                msg += 'The tags are: %s' %self.get_boundary_tags()
289                raise msg
290
291
292    def set_region(self, functions):
293        # The order of functions in the list is used.
294        if type(functions) not in [types.ListType,types.TupleType]:
295            functions = [functions]
296        for function in functions:
297            for tag in self.tagged_elements.keys():
298                function(tag, self.tagged_elements[tag], self)
299
300                #Do we need to do this sort of thing?
301                #self = function(tag, self.tagged_elements[tag], self)
302
303    #MISC
304    def check_integrity(self):
305        Mesh.check_integrity(self)
306
307        for quantity in self.conserved_quantities:
308            msg = 'Conserved quantities must be a subset of all quantities'
309            assert quantity in self.quantities, msg
310
311        ##assert hasattr(self, 'boundary_objects')
312
313    def write_time(self):
314        if self.min_timestep == self.max_timestep:
315            print 'Time = %.4f, delta t = %.8f, steps=%d (%d)'\
316                  %(self.time, self.min_timestep, self.number_of_steps,
317                    self.number_of_first_order_steps)
318        elif self.min_timestep > self.max_timestep:
319            print 'Time = %.4f, steps=%d (%d)'\
320                  %(self.time, self.number_of_steps,
321                    self.number_of_first_order_steps)
322        else:
323            print 'Time = %.4f, delta t in [%.8f, %.8f], steps=%d (%d)'\
324                  %(self.time, self.min_timestep,
325                    self.max_timestep, self.number_of_steps,
326                    self.number_of_first_order_steps)
327
328    def boundary_stats(self, quantities = None, tag = None):
329        """Output statistics about boundary forcing
330
331
332        """
333
334        if quantities is None:
335            quantities = self.conserved_quantities
336
337
338        print 'Boundary values at time %.4f:' %self.time
339        for name in quantities:
340            q = self.quantities[name]
341
342            if tag is None:
343                #Take entire boundary
344                print '    Quantity %s: min = %12.8f, max = %12.8f'\
345                      %(name, min(q.boundary_values), max(q.boundary_values))
346            else:
347                #Take only boundary associated with tag
348                maxval = minval = None
349                for i, ((vol_id, edge_id), B) in enumerate(self.boundary_objects):
350                    if self.boundary[(vol_id, edge_id)] == tag:
351                        v = q.boundary_values[i]
352                        if minval is None or v < minval: minval = v
353                        if maxval is None or v > maxval: maxval = v
354
355                if minval is None or maxval is None:
356                    print 'Sorry no information about tag %s' %tag
357                else:
358                    print '    Quantity %s, tag %s: min = %12.8f, max = %12.8f'\
359                          %(name, tag, minval, maxval)
360
361
362
363
364
365
366
367
368
369
370    def get_name(self):
371        return self.filename
372
373    def set_name(self, name):
374        self.filename = name
375
376    def get_datadir(self):
377        return self.datadir
378
379    def set_datadir(self, name):
380        self.datadir = name
381
382
383
384    #def set_defaults(self):
385    #    """Set default values for uninitialised quantities.
386    #    Should be overridden or specialised by specific modules
387    #    """#
388    #
389    #    for name in self.conserved_quantities + self.other_quantities:
390    #        self.set_quantity(name, 0.0)
391
392
393    ###########################
394    #Main components of evolve
395
396    def evolve(self, yieldstep = None, finaltime = None):
397        """Evolve model from time=0.0 to finaltime yielding results
398        every yieldstep.
399
400        Internally, smaller timesteps may be taken.
401
402        Evolve is implemented as a generator and is to be called as such, e.g.
403
404        for t in domain.evolve(timestep, yieldstep, finaltime):
405            <Do something with domain and t>
406
407        """
408
409        from config import min_timestep, max_timestep, epsilon
410
411        #FIXME: Maybe lump into a larger check prior to evolving
412        msg = 'Boundary tags must be bound to boundary objects before evolving system, '
413        msg += 'e.g. using the method set_boundary.\n'
414        msg += 'This system has the boundary tags %s ' %self.get_boundary_tags()
415        assert hasattr(self, 'boundary_objects'), msg
416
417        ##self.set_defaults()
418
419        if yieldstep is None:
420            yieldstep = max_timestep
421        else:
422            yieldstep = float(yieldstep)
423
424        self.order = self.default_order
425
426
427        self.yieldtime = 0.0 #Time between 'yields'
428
429        #Initialise interval of timestep sizes (for reporting only)
430        self.min_timestep = max_timestep
431        self.max_timestep = min_timestep
432        self.finaltime = finaltime
433        self.number_of_steps = 0
434        self.number_of_first_order_steps = 0
435
436        #update ghosts
437        self.update_ghosts()
438
439        #Initial update of vertex and edge values
440        self.distribute_to_vertices_and_edges()
441
442        #Initial update boundary values
443        self.update_boundary()
444
445        #Or maybe restore from latest checkpoint
446        if self.checkpoint is True:
447            self.goto_latest_checkpoint()
448
449        yield(self.time)  #Yield initial values
450
451        while True:
452
453            #Compute fluxes across each element edge
454            self.compute_fluxes()
455
456            #Update timestep to fit yieldstep and finaltime
457            self.update_timestep(yieldstep, finaltime)
458
459            #Update conserved quantities
460            self.update_conserved_quantities()
461
462            #update ghosts
463            self.update_ghosts()
464
465            #Update vertex and edge values
466            self.distribute_to_vertices_and_edges()
467
468            #Update boundary values
469            self.update_boundary()
470
471            #Update time
472            self.time += self.timestep
473            self.yieldtime += self.timestep
474            self.number_of_steps += 1
475            if self.order == 1:
476                self.number_of_first_order_steps += 1
477
478            #Yield results
479            if finaltime is not None and abs(self.time - finaltime) < epsilon:
480
481                #FIXME: There is a rare situation where the
482                #final time step is stored twice. Can we make a test?
483
484                # Yield final time and stop
485                yield(self.time)
486                break
487
488
489            if abs(self.yieldtime - yieldstep) < epsilon:
490                # Yield (intermediate) time and allow inspection of domain
491
492                if self.checkpoint is True:
493                    self.store_checkpoint()
494                    self.delete_old_checkpoints()
495
496                #Pass control on to outer loop for more specific actions
497                yield(self.time)
498
499                # Reinitialise
500                self.yieldtime = 0.0
501                self.min_timestep = max_timestep
502                self.max_timestep = min_timestep
503                self.number_of_steps = 0
504                self.number_of_first_order_steps = 0
505
506
507    def evolve_to_end(self, finaltime = 1.0):
508        """Iterate evolve all the way to the end
509        """
510
511        for _ in self.evolve(yieldstep=None, finaltime=finaltime):
512            pass
513
514
515
516    def update_boundary(self):
517        """Go through list of boundary objects and update boundary values
518        for all conserved quantities on boundary.
519        """
520
521        #FIXME: Update only those that change (if that can be worked out)
522        for i, ((vol_id, edge_id), B) in enumerate(self.boundary_objects):
523            q = B.evaluate(vol_id, edge_id)
524
525            for j, name in enumerate(self.conserved_quantities):
526                Q = self.quantities[name]
527                Q.boundary_values[i] = q[j]
528
529
530    def compute_fluxes(self):
531        msg = 'Method compute_fluxes must be overridden by Domain subclass'
532        raise msg
533
534
535    def update_timestep(self, yieldstep, finaltime):
536
537        from config import min_timestep
538
539        timestep = self.timestep
540
541        #Record maximal and minimal values of timestep for reporting
542        self.max_timestep = max(timestep, self.max_timestep)
543        self.min_timestep = min(timestep, self.min_timestep)
544
545        #Protect against degenerate time steps
546        if timestep < min_timestep:
547
548            #Number of consecutive small steps taken b4 taking action
549            self.smallsteps += 1
550
551            if self.smallsteps > self.max_smallsteps:
552                self.smallsteps = 0 #Reset
553
554                if self.order == 1:
555                    msg = 'WARNING: Too small timestep %.16f reached '\
556                          %timestep
557                    msg += 'even after %d steps of 1 order scheme'\
558                           %self.max_smallsteps
559                    print msg
560                    timestep = min_timestep  #Try enforcing min_step
561
562                    #raise msg
563                else:
564                    #Try to overcome situation by switching to 1 order
565                    self.order = 1
566
567        else:
568            self.smallsteps = 0
569            if self.order == 1 and self.default_order == 2:
570                self.order = 2
571
572
573        #Ensure that final time is not exceeded
574        if finaltime is not None and self.time + timestep > finaltime:
575            timestep = finaltime-self.time
576
577        #Ensure that model time is aligned with yieldsteps
578        if self.yieldtime + timestep > yieldstep:
579            timestep = yieldstep-self.yieldtime
580
581        self.timestep = timestep
582
583
584
585    def compute_forcing_terms(self):
586        """If there are any forcing functions driving the system
587        they should be defined in Domain subclass and appended to
588        the list self.forcing_terms
589        """
590
591        for f in self.forcing_terms:
592            f(self)
593
594
595
596    def update_conserved_quantities(self):
597        """Update vectors of conserved quantities using previously
598        computed fluxes specified forcing functions.
599        """
600
601        from Numeric import ones, sum, equal, Float
602
603        N = self.number_of_elements
604        d = len(self.conserved_quantities)
605
606        timestep = self.timestep
607
608        #Compute forcing terms
609        self.compute_forcing_terms()
610
611        #Update conserved_quantities
612        for name in self.conserved_quantities:
613            Q = self.quantities[name]
614            Q.update(timestep)
615
616            #Clean up
617            #Note that Q.explicit_update is reset by compute_fluxes
618
619            #MH090605 commented out the following since semi_implicit_update is now re-initialized
620            #at the end of the _update function in quantity_ext.c (This is called by the
621            #preceeding Q.update(timestep) statement above).
622            #For run_profile.py with N=128, the time of update_conserved_quantities is cut from 14.00 secs
623            #to 8.35 secs
624
625            #Q.semi_implicit_update[:] = 0.0
626
627    def update_ghosts(self):
628        pass
629
630
631    def distribute_to_vertices_and_edges(self):
632        """Extrapolate conserved quantities from centroid to
633        vertices and edge-midpoints for each volume
634
635        Default implementation is straight first order,
636        i.e. constant values throughout each element and
637        no reference to non-conserved quantities.
638        """
639
640        for name in self.conserved_quantities:
641            Q = self.quantities[name]
642            if self.order == 1:
643                Q.extrapolate_first_order()
644            elif self.order == 2:
645                Q.extrapolate_second_order()
646                Q.limit()
647            else:
648                raise 'Unknown order'
649            Q.interpolate_from_vertices_to_edges()
650
651
652
653##############################################
654#Initialise module
655
656#Optimisation with psyco
657from config import use_psyco
658if use_psyco:
659    try:
660        import psyco
661    except:
662        import os
663        if os.name == 'posix' and os.uname()[4] == 'x86_64':
664            pass
665            #Psyco isn't supported on 64 bit systems, but it doesn't matter
666        else:
667            msg = 'WARNING: psyco (speedup) could not import'+\
668                  ', you may want to consider installing it'
669            print msg
670    else:
671        psyco.bind(Domain.update_boundary)
672        #psyco.bind(Domain.update_timestep)     #Not worth it
673        psyco.bind(Domain.update_conserved_quantities)
674        psyco.bind(Domain.distribute_to_vertices_and_edges)
675
676
677if __name__ == "__main__":
678    pass
Note: See TracBrowser for help on using the repository browser.