source: inundation/pyvolution/domain.py @ 1794

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

Embedded caching functionality within quantity.set_values and modified validation example lwru2.py to illustrate the advantages that can be gained from supervised caching.

File size: 23.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                     #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        if self.min_timestep == self.max_timestep:
325            print 'Time = %.4f, delta t = %.8f, steps=%d (%d)'\
326                  %(self.time, self.min_timestep, self.number_of_steps,
327                    self.number_of_first_order_steps)
328        elif self.min_timestep > self.max_timestep:
329            print 'Time = %.4f, steps=%d (%d)'\
330                  %(self.time, self.number_of_steps,
331                    self.number_of_first_order_steps)
332        else:
333            print 'Time = %.4f, delta t in [%.8f, %.8f], steps=%d (%d)'\
334                  %(self.time, self.min_timestep,
335                    self.max_timestep, self.number_of_steps,
336                    self.number_of_first_order_steps)
337
338    def boundary_stats(self, quantities = None, tag = None):
339        """Output statistics about boundary forcing
340
341
342        """
343
344        if quantities is None:
345            quantities = self.conserved_quantities
346
347
348        print 'Boundary values at time %.4f:' %self.time
349        for name in quantities:
350            q = self.quantities[name]
351
352            if tag is None:
353                #Take entire boundary
354                print '    Quantity %s: min = %12.8f, max = %12.8f'\
355                      %(name, min(q.boundary_values), max(q.boundary_values))
356            else:
357                #Take only boundary associated with tag
358                maxval = minval = None
359                for i, ((vol_id, edge_id), B) in enumerate(self.boundary_objects):
360                    if self.boundary[(vol_id, edge_id)] == tag:
361                        v = q.boundary_values[i]
362                        if minval is None or v < minval: minval = v
363                        if maxval is None or v > maxval: maxval = v
364
365                if minval is None or maxval is None:
366                    print 'Sorry no information about tag %s' %tag
367                else:
368                    print '    Quantity %s, tag %s: min = %12.8f, max = %12.8f'\
369                          %(name, tag, minval, maxval)
370
371
372
373
374
375
376
377
378
379
380    def get_name(self):
381        return self.filename
382
383    def set_name(self, name):
384        self.filename = name
385
386    def get_datadir(self):
387        return self.datadir
388
389    def set_datadir(self, name):
390        self.datadir = name
391
392
393
394    #def set_defaults(self):
395    #    """Set default values for uninitialised quantities.
396    #    Should be overridden or specialised by specific modules
397    #    """#
398    #
399    #    for name in self.conserved_quantities + self.other_quantities:
400    #        self.set_quantity(name, 0.0)
401
402
403    ###########################
404    #Main components of evolve
405
406    def evolve(self, yieldstep = None, finaltime = None):
407        """Evolve model from time=0.0 to finaltime yielding results
408        every yieldstep.
409
410        Internally, smaller timesteps may be taken.
411
412        Evolve is implemented as a generator and is to be called as such, e.g.
413
414        for t in domain.evolve(timestep, yieldstep, finaltime):
415            <Do something with domain and t>
416
417        """
418
419        from config import min_timestep, max_timestep, epsilon
420
421        #FIXME: Maybe lump into a larger check prior to evolving
422        msg = 'Boundary tags must be bound to boundary objects before evolving system, '
423        msg += 'e.g. using the method set_boundary.\n'
424        msg += 'This system has the boundary tags %s ' %self.get_boundary_tags()
425        assert hasattr(self, 'boundary_objects'), msg
426
427        ##self.set_defaults()
428
429        if yieldstep is None:
430            yieldstep = max_timestep
431        else:
432            yieldstep = float(yieldstep)
433
434        self.order = self.default_order
435
436
437        self.yieldtime = 0.0 #Time between 'yields'
438
439        #Initialise interval of timestep sizes (for reporting only)
440        self.min_timestep = max_timestep
441        self.max_timestep = min_timestep
442        self.finaltime = finaltime
443        self.number_of_steps = 0
444        self.number_of_first_order_steps = 0
445
446        #update ghosts
447        self.update_ghosts()
448
449        #Initial update of vertex and edge values
450        self.distribute_to_vertices_and_edges()
451
452        #Initial update boundary values
453        self.update_boundary()
454
455        #Or maybe restore from latest checkpoint
456        if self.checkpoint is True:
457            self.goto_latest_checkpoint()
458
459        yield(self.time)  #Yield initial values
460
461        while True:
462
463            #Compute fluxes across each element edge
464            self.compute_fluxes()
465
466            #Update timestep to fit yieldstep and finaltime
467            self.update_timestep(yieldstep, finaltime)
468
469            #Update conserved quantities
470            self.update_conserved_quantities()
471
472            #update ghosts
473            self.update_ghosts()
474
475            #Update vertex and edge values
476            self.distribute_to_vertices_and_edges()
477
478            #Update boundary values
479            self.update_boundary()
480
481            #Update time
482            self.time += self.timestep
483            self.yieldtime += self.timestep
484            self.number_of_steps += 1
485            if self.order == 1:
486                self.number_of_first_order_steps += 1
487
488            #Yield results
489            if finaltime is not None and abs(self.time - finaltime) < epsilon:
490
491                #FIXME: There is a rare situation where the
492                #final time step is stored twice. Can we make a test?
493
494                # Yield final time and stop
495                yield(self.time)
496                break
497
498
499            if abs(self.yieldtime - yieldstep) < epsilon:
500                # Yield (intermediate) time and allow inspection of domain
501
502                if self.checkpoint is True:
503                    self.store_checkpoint()
504                    self.delete_old_checkpoints()
505
506                #Pass control on to outer loop for more specific actions
507                yield(self.time)
508
509                # Reinitialise
510                self.yieldtime = 0.0
511                self.min_timestep = max_timestep
512                self.max_timestep = min_timestep
513                self.number_of_steps = 0
514                self.number_of_first_order_steps = 0
515
516
517    def evolve_to_end(self, finaltime = 1.0):
518        """Iterate evolve all the way to the end
519        """
520
521        for _ in self.evolve(yieldstep=None, finaltime=finaltime):
522            pass
523
524
525
526    def update_boundary(self):
527        """Go through list of boundary objects and update boundary values
528        for all conserved quantities on boundary.
529        """
530
531        #FIXME: Update only those that change (if that can be worked out)
532        for i, ((vol_id, edge_id), B) in enumerate(self.boundary_objects):
533            q = B.evaluate(vol_id, edge_id)
534
535            for j, name in enumerate(self.conserved_quantities):
536                Q = self.quantities[name]
537                Q.boundary_values[i] = q[j]
538
539
540    def compute_fluxes(self):
541        msg = 'Method compute_fluxes must be overridden by Domain subclass'
542        raise msg
543
544
545    def update_timestep(self, yieldstep, finaltime):
546
547        from config import min_timestep
548
549        # self.timestep is calculated from speed of characteristics
550        # Apply CFL condition here
551        timestep = self.CFL*self.timestep
552
553        #Record maximal and minimal values of timestep for reporting
554        self.max_timestep = max(timestep, self.max_timestep)
555        self.min_timestep = min(timestep, self.min_timestep)
556
557        #Protect against degenerate time steps
558        if timestep < min_timestep:
559
560            #Number of consecutive small steps taken b4 taking action
561            self.smallsteps += 1
562
563            if self.smallsteps > self.max_smallsteps:
564                self.smallsteps = 0 #Reset
565
566                if self.order == 1:
567                    msg = 'WARNING: Too small timestep %.16f reached '\
568                          %timestep
569                    msg += 'even after %d steps of 1 order scheme'\
570                           %self.max_smallsteps
571                    print msg
572                    timestep = min_timestep  #Try enforcing min_step
573
574                    #raise msg
575                else:
576                    #Try to overcome situation by switching to 1 order
577                    self.order = 1
578
579        else:
580            self.smallsteps = 0
581            if self.order == 1 and self.default_order == 2:
582                self.order = 2
583
584
585        #Ensure that final time is not exceeded
586        if finaltime is not None and self.time + timestep > finaltime:
587            timestep = finaltime-self.time
588
589        #Ensure that model time is aligned with yieldsteps
590        if self.yieldtime + timestep > yieldstep:
591            timestep = yieldstep-self.yieldtime
592
593        self.timestep = timestep
594
595
596
597    def compute_forcing_terms(self):
598        """If there are any forcing functions driving the system
599        they should be defined in Domain subclass and appended to
600        the list self.forcing_terms
601        """
602
603        for f in self.forcing_terms:
604            f(self)
605
606
607
608    def update_conserved_quantities(self):
609        """Update vectors of conserved quantities using previously
610        computed fluxes specified forcing functions.
611        """
612
613        from Numeric import ones, sum, equal, Float
614
615        N = self.number_of_elements
616        d = len(self.conserved_quantities)
617
618        timestep = self.timestep
619
620        #Compute forcing terms
621        self.compute_forcing_terms()
622
623        #Update conserved_quantities
624        for name in self.conserved_quantities:
625            Q = self.quantities[name]
626            Q.update(timestep)
627
628            #Clean up
629            #Note that Q.explicit_update is reset by compute_fluxes
630
631            #MH090605 commented out the following since semi_implicit_update is now re-initialized
632            #at the end of the _update function in quantity_ext.c (This is called by the
633            #preceeding Q.update(timestep) statement above).
634            #For run_profile.py with N=128, the time of update_conserved_quantities is cut from 14.00 secs
635            #to 8.35 secs
636
637            #Q.semi_implicit_update[:] = 0.0
638
639    def update_ghosts(self):
640        pass
641
642    def distribute_to_vertices_and_edges(self):
643        """Extrapolate conserved quantities from centroid to
644        vertices and edge-midpoints for each volume
645
646        Default implementation is straight first order,
647        i.e. constant values throughout each element and
648        no reference to non-conserved quantities.
649        """
650
651        for name in self.conserved_quantities:
652            Q = self.quantities[name]
653            if self.order == 1:
654                Q.extrapolate_first_order()
655            elif self.order == 2:
656                Q.extrapolate_second_order()
657                Q.limit()
658            else:
659                raise 'Unknown order'
660            Q.interpolate_from_vertices_to_edges()
661
662
663
664##############################################
665#Initialise module
666
667#Optimisation with psyco
668from config import use_psyco
669if use_psyco:
670    try:
671        import psyco
672    except:
673        import os
674        if os.name == 'posix' and os.uname()[4] == 'x86_64':
675            pass
676            #Psyco isn't supported on 64 bit systems, but it doesn't matter
677        else:
678            msg = 'WARNING: psyco (speedup) could not import'+\
679                  ', you may want to consider installing it'
680            print msg
681    else:
682        psyco.bind(Domain.update_boundary)
683        #psyco.bind(Domain.update_timestep)     #Not worth it
684        psyco.bind(Domain.update_conserved_quantities)
685        psyco.bind(Domain.distribute_to_vertices_and_edges)
686
687
688if __name__ == "__main__":
689    pass
Note: See TracBrowser for help on using the repository browser.