source: inundation/ga/storm_surge/pyvolution-parallel/domain.py @ 1452

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