source: inundation/pyvolution/domain.py @ 1928

Last change on this file since 1928 was 1928, checked in by ole, 18 years ago

Made create_quantity_from_expression a method

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