source: inundation/pyvolution/domain.py @ 1862

Last change on this file since 1862 was 1862, checked in by ole, 19 years ago

Allowed multiple evolve steps without repeating timesteps

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