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

Last change on this file since 1671 was 1662, checked in by ole, 20 years ago

Georeferencing defaults

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