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

Last change on this file since 1508 was 1508, checked in by matthew, 19 years ago

To avoid computing the flux function at each internal edge twice, we now call the C implementation of compute_fluxes with the extra integer vector argument already_computed_flux. This is an array of dimension (N,3) in python (or 3N in C) and thus each entry corresponds to a triangle-edge pair. If triangle t has neighbour n across its i-th edge, and this edge is the m-th edge of triangle n, then we update this edge's flux contribution to explicit_update for both triangle n and triangle t. Both corresponding entries of already_computed_flux will be incremented so that, when we come to edge m of triangle n, we know that the update across this edge has already been done.
The array already_computed_flux is defined and initialised to zero in domain.py.

For run_profile.py, with N=128, the version of compute_fluxes in which each edge was computed twice consumed 13.64 seconds (averaged over 3 runs). By keeping track of which edges have been computed, compute_fluxes now consumes 8.68 seconds.

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