source: inundation/ga/storm_surge/pyvolution/util.py @ 1668

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

Separated Interpolation_function out from File_function and refactored the latter in
terms of the former.
Started unifying the general version with temporal interpolation only.

File size: 62.6 KB
Line 
1"""This module contains various auxiliary function used by pyvolution.
2
3It is also a clearing house for functions that may later earn a module
4of their own.
5"""
6
7#FIXME: Move polygon stuff out
8#FIXME: Finish Interpolation function and get rid of File_function_ASCII
9
10
11def angle(v):
12    """Compute angle between e1 (the unit vector in the x-direction)
13    and the specified vector
14    """
15
16    from math import acos, pi, sqrt
17    from Numeric import sum, array
18
19    l = sqrt( sum (array(v)**2))
20    v1 = v[0]/l
21    v2 = v[1]/l
22
23
24    theta = acos(v1)
25   
26    #try:
27    #   theta = acos(v1)
28    #except ValueError, e:
29    #    print 'WARNING (util.py): Angle acos(%s) failed: %s' %(str(v1), e)
30    #
31    #    #FIXME, hack to avoid acos(1.0) Value error
32    #    # why is it happening?
33    #    # is it catching something we should avoid?
34    #    #FIXME (Ole): When did this happen? We need a unit test to
35    #    #reveal this condition or otherwise remove the hack
36    #   
37    #    s = 1e-6
38    #    if (v1+s >  1.0) and (v1-s < 1.0) :
39    #        theta = 0.0
40    #    elif (v1+s >  -1.0) and (v1-s < -1.0):
41    #        theta = 3.1415926535897931
42    #    print 'WARNING (util.py): angle v1 is %f, setting acos to %f '\
43    #          %(v1, theta)
44
45    if v2 < 0:
46        #Quadrant 3 or 4
47        theta = 2*pi-theta
48
49    return theta
50
51
52def anglediff(v0, v1):
53    """Compute difference between angle of vector x0, y0 and x1, y1.
54    This is used for determining the ordering of vertices,
55    e.g. for checking if they are counter clockwise.
56
57    Always return a positive value
58    """
59
60    from math import pi
61
62    a0 = angle(v0)
63    a1 = angle(v1)
64
65    #Ensure that difference will be positive
66    if a0 < a1:
67        a0 += 2*pi
68
69    return a0-a1
70
71
72def mean(x):
73    from Numeric import sum
74    return sum(x)/len(x)
75
76
77def point_on_line(x, y, x0, y0, x1, y1):
78    """Determine whether a point is on a line segment
79
80    Input: x, y, x0, x0, x1, y1: where
81        point is given by x, y
82        line is given by (x0, y0) and (x1, y1)
83
84    """
85
86    from Numeric import array, dot, allclose
87    from math import sqrt
88
89    a = array([x - x0, y - y0])
90    a_normal = array([a[1], -a[0]])
91
92    b = array([x1 - x0, y1 - y0])
93
94    if dot(a_normal, b) == 0:
95        #Point is somewhere on the infinite extension of the line
96
97        len_a = sqrt(sum(a**2))
98        len_b = sqrt(sum(b**2))
99        if dot(a, b) >= 0 and len_a <= len_b:
100           return True
101        else:
102           return False
103    else:
104      return False
105
106def ensure_numeric(A, typecode = None):
107    """Ensure that sequence is a Numeric array.
108    Inputs:
109        A: Sequance. If A is already a Numeric array it will be returned
110                     unaltered
111                     If not, an attempt is made to convert it to a Numeric
112                     array
113        typecode: Numeric type. If specified, use this in the conversion.
114                                If not, let Numeric decide
115
116    This function is necessary as array(A) can cause memory overflow.
117    """
118
119    from Numeric import ArrayType, array
120
121    if typecode is None:
122        if type(A) == ArrayType:
123            return A
124        else:
125            return array(A)
126    else:
127        if type(A) == ArrayType:
128            if A.typecode == typecode:
129                return array(A)
130            else:
131                return A.astype(typecode)
132        else:
133            return array(A).astype(typecode)
134
135
136
137def file_function(filename, domain=None, quantities = None, interpolation_points = None, verbose = False):
138    """If domain is specified, don't specify quantites as they are automatically derived
139    """
140
141
142    #FIXME (OLE): Should check origin of domain against that of file
143    #In fact, this is where origin should be converted to that of domain
144    #Also, check that file covers domain fully.
145
146    assert type(filename) == type(''),\
147               'First argument to File_function must be a string'
148
149    try:
150        fid = open(filename)
151    except Exception, e:
152        msg = 'File "%s" could not be opened: Error="%s"'\
153                  %(filename, e)
154        raise msg
155
156    line = fid.readline()
157    fid.close()
158
159    if domain is not None:
160        quantities = domain.conserved_quantities
161    else:
162        quantities = None
163
164
165    if line[:3] == 'CDF':
166        return get_netcdf_file_function(filename, domain, quantities,
167                                        interpolation_points, verbose = verbose)
168    else:
169        #FIXME Should be phased out
170        return File_function_ASCII(filename, domain, quantities,
171                                   interpolation_points)
172   
173   
174
175
176
177
178
179
180
181
182def get_netcdf_file_function(filename, domain=None, quantity_names=None,
183                             interpolation_points=None, verbose = False):
184    """Read time history of spatial data from NetCDF sww file and
185    return a callable object f(t,x,y)
186    which will return interpolated values based on the input file.
187
188    If domain is specified, model time (domain.starttime)
189    will be checked and possibly modified
190   
191    All times are assumed to be in UTC
192
193    See Interpolation function for further documetation
194   
195
196    """
197   
198    #verbose = True
199   
200    #FIXME: Check that model origin is the same as file's origin
201    #(both in UTM coordinates)
202    #If not - modify those from file to match domain
203    #Take this code from e.g. dem2pts in data_manager.py
204    #FIXME: Use geo_reference to read and write xllcorner...
205       
206
207    import time, calendar
208    from config import time_format
209    from Scientific.IO.NetCDF import NetCDFFile
210    from Numeric import array, zeros, Float, alltrue, concatenate, reshape       
211
212    #Open NetCDF file
213    if verbose: print 'Reading', filename
214    fid = NetCDFFile(filename, 'r')
215
216    if quantity_names is None or len(quantity_names) < 1:
217        msg = 'ERROR: At least one independent value must be specified'
218        raise msg
219
220    missing = []
221    for quantity in ['x', 'y', 'time'] + quantity_names:
222        if not fid.variables.has_key(quantity):
223            missing.append(quantity)
224
225    if len(missing) > 0:
226        msg = 'Quantities %s could not be found in file %s'\
227              %(str(missing), filename)
228        raise msg
229
230
231    #Get first timestep
232    try:
233        starttime = fid.starttime[0]
234    except ValueError:
235        msg = 'Could not read starttime from file %s' %filename
236        raise msg
237
238    #Get origin
239    xllcorner = fid.xllcorner[0]
240    yllcorner = fid.yllcorner[0]
241
242
243    #Get variables
244    if verbose: print 'Get variables'
245    x = fid.variables['x'][:]
246    y = fid.variables['y'][:]
247    triangles = fid.variables['volumes'][:]
248    time = fid.variables['time'][:]
249
250    x = reshape(x, (len(x),1))
251    y = reshape(y, (len(y),1))
252    vertex_coordinates = concatenate((x,y), axis=1) #m x 2 array
253
254    if domain is not None:
255        #Update domain.startime if it is *earlier* than starttime
256        if starttime > domain.starttime:
257            msg = 'WARNING: Start time as specified in domain (%f)'\
258                  %domain.starttime
259            msg += ' is earlier than the starttime of file %s (%f).'\
260                     %(filename, starttime)
261            msg += ' Modifying domain starttime accordingly.'
262           
263            if verbose: print msg
264            domain.starttime = starttime #Modifying model time
265            if verbose: print 'Domain starttime is now set to %f'\
266               %domain.starttime
267
268
269        #If domain.startime is *later* than starttime,
270        #move time back - relative to domain's time
271        if domain.starttime > starttime:
272            time = time - domain.starttime + starttime
273
274
275    if verbose:
276        print 'File_function data obtained from: %s' %filename
277        print '  References:'
278        #print '    Datum ....' #FIXME
279        print '    Lower left corner: [%f, %f]'\
280              %(xllcorner, yllcorner)
281        print '    Start time:   %f' %starttime               
282       
283   
284    #Produce values for desired data points at
285    #each timestep
286
287    quantities = {}
288    for i, name in enumerate(quantity_names):
289        quantities[name] = fid.variables[name][:]
290    fid.close()
291   
292
293    return Interpolation_function(vertex_coordinates,
294                                  triangles,
295                                  time,
296                                  quantities,
297                                  quantity_names, 
298                                  interpolation_points,
299                                  alpha = 0,
300                                  verbose = verbose)
301
302
303
304
305
306class Interpolation_function:
307    """Interpolation_function - creates callable object f(t, id) or f(t,x,y)
308    which is interpolated from time series defined at vertices of
309    triangular mesh
310
311    Input
312        vertex_coordinates: mx2 array of coordinates (Float)
313        triangles:          nx3 array of indices into vertex_coordinates (Int)
314        time:               px1 array of times (Float)               
315        quantities:         Dictionary of pxm values
316        quantity_names:
317        interpolation_points:
318        alpha:
319        verbose:
320   
321
322   
323    The quantities returned by the callable object are specified by
324    the list quantities which must contain the names of the
325    quantities to be returned and also reflect the order, e.g. for
326    the shallow water wave equation, on would have
327    quantities = ['stage', 'xmomentum', 'ymomentum']
328
329    The parameter interpolation_points decides at which points interpolated
330    quantities are to be computed whenever object is called.
331    If None, return average value
332    """
333
334   
335
336   
337    def __init__(self,
338                 vertex_coordinates,
339                 triangles,
340                 time,
341                 quantities,
342                 quantity_names = None, 
343                 interpolation_points = None,
344                 alpha = 0,
345                 verbose = False):
346        """
347        """
348
349        from Numeric import array, zeros, Float, alltrue, concatenate,\
350             reshape, ArrayType
351
352        from config import time_format
353        from least_squares import Interpolation
354
355        #Check
356        msg = 'Time must be a monotonuosly '
357        msg += 'increasing sequence %s' %time
358        assert alltrue(time[1:] - time[:-1] > 0 ), msg
359
360
361        #Check if quantities is a single array only
362        if type(quantities) == ArrayType:
363            quantity_names = 'Attribute'
364            quantities = {quantity_names: quantities}           
365
366        #Use keys if no names specified
367        if quantity_names is not None:
368            self.quantity_names = quantity_names
369        else:
370            self.quantity_names = quantities.keys()
371
372
373        self.interpolation_points = interpolation_points
374        self.T = time[:]  #Time assumed to be relative to starttime
375        self.index = 0    #Initial time index
376        self.values = {}
377           
378
379        if interpolation_points is not None:
380            #Spatial interpolation
381
382            try:
383                interpolation_points = ensure_numeric(interpolation_points)
384            except:
385                msg = 'Interpolation points must be an N x 2 Numeric array or a list of points\n'
386                msg += 'I got: %s.' %( str(interpolation_points)[:60] + '...')
387                raise msg
388
389
390            for name in quantity_names:
391                self.values[name] = zeros( (len(self.T),
392                                            len(interpolation_points)),
393                                            Float)
394
395            #Build interpolator
396            interpol = Interpolation(vertex_coordinates,
397                                     triangles,
398                                     point_coordinates = interpolation_points,
399                                     alpha = 0,
400                                     verbose = verbose)
401
402
403            if verbose: print 'Interpolate'
404            for i, t in enumerate(self.T):
405                #Interpolate quantities at this timestep
406                if verbose: print ' time step %d of %d' %(i, len(self.T))
407                for name in quantity_names:
408                    self.values[name][i, :] =\
409                    interpol.interpolate(quantities[name][i,:])
410
411            #Report
412            if verbose:
413                x = vertex_coordinates[:,0]
414                y = vertex_coordinates[:,1]               
415               
416                print '------------------------------------------------'
417                print 'Interpolation_function statistics:'
418                print '  Extent:'
419                print '    x in [%f, %f], len(x) == %d'\
420                      %(min(x.flat), max(x.flat), len(x.flat))
421                print '    y in [%f, %f], len(y) == %d'\
422                      %(min(y.flat), max(y.flat), len(y.flat))
423                print '    t in [%f, %f], len(t) == %d'\
424                      %(min(self.T), max(self.T), len(self.T))
425                print '  Quantities:'
426                for name in quantity_names:
427                    q = quantities[name][:].flat
428                    print '    %s in [%f, %f]' %(name, min(q), max(q))
429
430
431                print '  Interpolation points (xi, eta):'\
432                      'number of points == %d ' %interpolation_points.shape[0]
433                print '    xi in [%f, %f]' %(min(interpolation_points[:,0]),
434                                             max(interpolation_points[:,0]))
435                print '    eta in [%f, %f]' %(min(interpolation_points[:,1]),
436                                              max(interpolation_points[:,1]))
437                print '  Interpolated quantities (over all timesteps):'
438                for name in quantity_names:
439                    q = self.values[name][:].flat
440                    print '    %s at interpolation points in [%f, %f]'\
441                          %(name, min(q), max(q))
442                print '------------------------------------------------'
443        else:
444            #Return an average, making this a time series
445            #FIXME: Needs a test
446            for name in quantity_names:
447                self.values[name] = avg(quantities[name][i,:])
448
449
450    def __repr__(self):
451        return 'Interpolation function'
452
453    def __call__(self, t, point_id = None, x = None, y = None):
454        """Evaluate f(t), f(t, point_id) or f(t, x, y)
455
456        Inputs:
457          t: time - Model time. Must lie within existing timesteps
458          point_id: index of one of the preprocessed points.
459                    If point_id is None all preprocessed points are computed
460                   
461          If no x,y info is present, point_id and x,y arguments are ignored
462          making f a function of time only.
463         
464          FIXME: point_id could also be a slice
465          FIXME: One could allow arbitrary x, y coordinates
466          (requires computation of a new interpolator)
467          FIXME: What if x and y are vectors?
468          FIXME: Does point_id or x,y take precedence?
469          FIXME: What about f(x,y) without t
470         
471        """
472
473        from math import pi, cos, sin, sqrt
474        from Numeric import zeros, Float
475
476
477        msg = 'Time interval [%s:%s]' %(self.T[0], self.T[1])
478        msg += ' does not match model time: %s\n' %t
479        if t < self.T[0]: raise msg
480        if t > self.T[-1]: raise msg
481
482        oldindex = self.index #Time index
483
484        #Find current time slot
485        while t > self.T[self.index]: self.index += 1
486        while t < self.T[self.index]: self.index -= 1
487
488        if t == self.T[self.index]:
489            #Protect against case where t == T[-1] (last time)
490            # - also works in general when t == T[i]
491            ratio = 0
492        else:
493            #t is now between index and index+1
494            ratio = (t - self.T[self.index])/\
495                    (self.T[self.index+1] - self.T[self.index])
496
497        #Compute interpolated values
498        q = zeros(len(self.quantity_names), Float)
499
500        for i, name in enumerate(self.quantity_names):
501            Q = self.values[name]
502
503            if point_id is None:
504                Q0 = Q[self.index]
505
506                try:
507                    Q1 = Q[self.index+1]
508                except:
509                    pass               
510            else:   
511                Q0 = Q[self.index, point_id]
512
513                try:
514                    Q1 = Q[self.index+1, point_id]
515                except:
516                    pass
517               
518
519            #Linear temporal interpolation   
520            if ratio > 0:
521                q[i] = Q0 + ratio*(Q1 - Q0)
522            else:
523                q[i] = Q0
524           
525
526            #if ratio > 0:
527            #    q[i] = Q[self.index, point_id] +\
528            #           ratio*(Q[self.index+1, point_id] -\
529            #                  Q[self.index, point_id])
530            #else:
531            #    q[i] = Q[self.index, point_id]
532
533        #Return vector of interpolated values
534        return q
535
536
537
538
539
540
541
542
543
544
545#FIXME Obsolete
546class File_function_NetCDF:
547    """Read time history of spatial data from NetCDF sww file and
548    return a callable object f(t,x,y)
549    which will return interpolated values based on the input file.
550
551    x, y may be either scalars or vectors
552
553    #FIXME: More about format, interpolation and order of quantities
554
555    The quantities returned by the callable objects are specified by
556    the list quantities which must contain the names of the
557    quantities to be returned and also reflect the order, e.g. for
558    the shallow water wave equation, on would have
559    quantities = ['stage', 'xmomentum', 'ymomentum']
560
561    interpolation_points decides at which points interpolated
562    quantities are to be computed whenever object is called.
563    If None, return average value
564    """
565
566    def  __init__(self, filename, domain=None, quantities=None,
567                  interpolation_points=None, verbose = False):
568        """Initialise callable object from a file.
569
570        If domain is specified, model time (domain.starttime)
571        will be checked and possibly modified
572
573        All times are assumed to be in UTC
574        """
575
576        #FIXME: Check that model origin is the same as file's origin
577        #(both in UTM coordinates)
578        #If not - modify those from file to match domain
579        #Take this code from e.g. dem2pts in data_manager.py
580        #FIXME: Use geo_reference to read and write xllcorner...
581       
582
583        import time, calendar
584        from config import time_format
585        from Scientific.IO.NetCDF import NetCDFFile
586
587        #Open NetCDF file
588        if verbose: print 'Reading', filename
589        fid = NetCDFFile(filename, 'r')
590
591        if quantities is None or len(quantities) < 1:
592            msg = 'ERROR: At least one independent value must be specified'
593            raise msg
594
595        missing = []
596        for quantity in ['x', 'y', 'time'] + quantities:
597             if not fid.variables.has_key(quantity):
598                 missing.append(quantity)
599
600        if len(missing) > 0:
601            msg = 'Quantities %s could not be found in file %s'\
602                  %(str(missing), filename)
603            raise msg
604
605
606        #Get first timestep
607        try:
608            self.starttime = fid.starttime[0]
609        except ValueError:
610            msg = 'Could not read starttime from file %s' %filename
611            raise msg
612
613        #Get origin
614        self.xllcorner = fid.xllcorner[0]
615        self.yllcorner = fid.yllcorner[0]
616
617        self.number_of_values = len(quantities)
618        self.fid = fid
619        self.filename = filename
620        self.quantities = quantities
621        self.domain = domain
622        self.interpolation_points = interpolation_points
623
624        if domain is not None:
625            msg = 'WARNING: Start time as specified in domain (%f)'\
626                  %domain.starttime
627            msg += ' is earlier than the starttime of file %s (%f).'\
628                     %(self.filename, self.starttime)
629            msg += ' Modifying domain starttime accordingly.'
630            if self.starttime > domain.starttime:
631                if verbose: print msg
632                domain.starttime = self.starttime #Modifying model time
633                if verbose: print 'Domain starttime is now set to %f'\
634                   %domain.starttime
635
636        #Read all data in and produce values for desired data points at
637        #each timestep
638        self.spatial_interpolation(interpolation_points, verbose = verbose)
639        fid.close()
640
641    def spatial_interpolation(self, interpolation_points, verbose = False):
642        """For each timestep in fid: read surface,
643        interpolate to desired points and store values for use when
644        object is called.
645        """
646
647        from Numeric import array, zeros, Float, alltrue, concatenate, reshape
648
649        from config import time_format
650        from least_squares import Interpolation
651        import time, calendar
652
653        fid = self.fid
654
655        #Get variables
656        if verbose: print 'Get variables'
657        x = fid.variables['x'][:]
658        y = fid.variables['y'][:]
659        #z = fid.variables['elevation'][:]
660        triangles = fid.variables['volumes'][:]
661        time = fid.variables['time'][:]
662
663        #Check
664        msg = 'File %s must list time as a monotonuosly ' %self.filename
665        msg += 'increasing sequence'
666        assert alltrue(time[1:] - time[:-1] > 0 ), msg
667
668
669        if interpolation_points is not None:
670
671            try:
672                P = ensure_numeric(interpolation_points)
673            except:
674                msg = 'Interpolation points must be an N x 2 Numeric array or a list of points\n'
675                msg += 'I got: %s.' %( str(interpolation_points)[:60] + '...')
676                raise msg
677
678
679            self.T = time[:]  #Time assumed to be relative to starttime
680            self.index = 0    #Initial time index
681
682            self.values = {}
683            for name in self.quantities:
684                self.values[name] = zeros( (len(self.T),
685                                            len(interpolation_points)),
686                                            Float)
687
688            #Build interpolator
689            if verbose: print 'Build interpolation matrix'
690            x = reshape(x, (len(x),1))
691            y = reshape(y, (len(y),1))
692            vertex_coordinates = concatenate((x,y), axis=1) #m x 2 array
693
694            interpol = Interpolation(vertex_coordinates,
695                                     triangles,
696                                     point_coordinates = P,
697                                     alpha = 0,
698                                     verbose = verbose)
699
700
701            if verbose: print 'Interpolate'
702            for i, t in enumerate(self.T):
703                #Interpolate quantities at this timestep
704                if verbose: print ' time step %d of %d' %(i, len(self.T))
705                for name in self.quantities:
706                    self.values[name][i, :] =\
707                    interpol.interpolate(fid.variables[name][i,:])
708
709            #Report
710            if verbose:
711                print '------------------------------------------------'
712                print 'File_function statistics:'
713                print '  Name: %s' %self.filename
714                print '  References:'
715                #print '    Datum ....' #FIXME
716                print '    Lower left corner: [%f, %f]'\
717                      %(self.xllcorner, self.yllcorner)
718                print '    Start time:   %f' %self.starttime               
719                #print '    Start time (file):   %f' %self.starttime
720                #print '    Start time (domain): %f' %domain.starttime
721                print '  Extent:'
722                print '    x in [%f, %f], len(x) == %d'\
723                      %(min(x.flat), max(x.flat), len(x.flat))
724                print '    y in [%f, %f], len(y) == %d'\
725                      %(min(y.flat), max(y.flat), len(y.flat))
726                print '    t in [%f, %f], len(t) == %d'\
727                      %(min(self.T), max(self.T), len(self.T))
728                print '  Quantities:'
729                for name in self.quantities:
730                    q = fid.variables[name][:].flat
731                    print '    %s in [%f, %f]' %(name, min(q), max(q))
732
733
734                print '  Interpolation points (xi, eta):'\
735                      'number of points == %d ' %P.shape[0]
736                print '    xi in [%f, %f]' %(min(P[:,0]), max(P[:,0]))
737                print '    eta in [%f, %f]' %(min(P[:,1]), max(P[:,1]))
738                print '  Interpolated quantities (over all timesteps):'
739                for name in self.quantities:
740                    q = self.values[name][:].flat
741                    print '    %s at interpolation points in [%f, %f]'\
742                          %(name, min(q), max(q))
743                print '------------------------------------------------'
744        else:
745            msg = 'File_function_NetCDF must be invoked with '
746            msg += 'a list of interpolation points'
747            raise msg
748            #FIXME: Should not be an error.
749            #__call__ could return an average or in case of time series only
750            #no point s are needed
751
752
753    def __repr__(self):
754        return 'File function'
755
756    def __call__(self, t, x=None, y=None, point_id = None):
757        """Evaluate f(t, point_id)
758
759        Inputs:
760          t: time - Model time (tau = domain.starttime-self.starttime+t)
761                    must lie within existing timesteps
762          point_id: index of one of the preprocessed points.
763                    If point_id is None all preprocessed points are computed
764
765          FIXME: point_id could also be a slice
766          FIXME: One could allow arbitrary x, y coordinates
767          (requires computation of a new interpolator)
768          Maybe not,.,.
769          FIXME: What if x and y are vectors?
770        """
771
772        from math import pi, cos, sin, sqrt
773        from Numeric import zeros, Float
774
775
776        if point_id is None:
777            msg = 'NetCDF File function needs a point_id when invoked'
778            raise msg
779
780
781        #Find time tau such that
782        #
783        # domain.starttime + t == self.starttime + tau
784
785        if self.domain is not None:
786            tau = self.domain.starttime-self.starttime+t
787        else:
788            tau = t
789
790
791        msg = 'Time interval derived from file %s [%s:%s]'\
792              %(self.filename, self.T[0], self.T[1])
793        msg += ' does not match model time: %s\n' %tau
794        msg += 'Domain says its starttime == %f' %(self.domain.starttime)
795
796        if tau < self.T[0]: raise msg
797        if tau > self.T[-1]: raise msg
798
799
800        oldindex = self.index #Time index
801
802        #Find time slot
803        while tau > self.T[self.index]: self.index += 1
804        while tau < self.T[self.index]: self.index -= 1
805
806
807        if tau == self.T[self.index]:
808            #Protect against case where tau == T[-1] (last time)
809            # - also works in general when tau == T[i]
810            ratio = 0
811        else:
812            #t is now between index and index+1
813            ratio = (tau - self.T[self.index])/\
814                    (self.T[self.index+1] - self.T[self.index])
815
816
817
818
819        #Compute interpolated values
820        q = zeros( len(self.quantities), Float)
821
822        for i, name in enumerate(self.quantities):
823            Q = self.values[name]
824
825            if ratio > 0:
826                q[i] = Q[self.index, point_id] +\
827                       ratio*(Q[self.index+1, point_id] -\
828                              Q[self.index, point_id])
829            else:
830                q[i] = Q[self.index, point_id]
831
832
833
834        #Return vector of interpolated values
835        return q
836
837#FIXME Obsolete
838class File_function_ASCII:
839    """Read time series from file and return a callable object:
840    f(t,x,y)
841    which will return interpolated values based on the input file.
842
843    The file format is assumed to be either two fields separated by a comma:
844
845        time [DD/MM/YY hh:mm:ss], value0 value1 value2 ...
846
847    or four comma separated fields
848
849        time [DD/MM/YY hh:mm:ss], x, y, value0 value1 value2 ...
850
851    In either case, the callable object will return a tuple of
852    interpolated values, one each value stated in the file.
853
854
855    E.g
856
857      31/08/04 00:00:00, 1.328223 0 0
858      31/08/04 00:15:00, 1.292912 0 0
859
860    will provide a time dependent function f(t,x=None,y=None),
861    ignoring x and y, which returns three values per call.
862
863
864    NOTE: At this stage the function is assumed to depend on
865    time only, i.e  no spatial dependency!!!!!
866    When that is needed we can use the least_squares interpolation.
867
868    #FIXME: This should work with netcdf (e.g. sww) and thus render the
869    #spatio-temporal boundary condition in shallow water fully general
870
871    #FIXME: Specified quantites not used here -
872    #always return whatever is in the file
873    """
874
875
876    def __init__(self, filename, domain=None, quantities = None, interpolation_points=None):
877        """Initialise callable object from a file.
878        See docstring for class File_function
879
880        If domain is specified, model time (domain,starttime)
881        will be checked and possibly modified
882
883        All times are assumed to be in UTC
884        """
885
886        import time, calendar
887        from Numeric import array
888        from config import time_format
889
890        fid = open(filename)
891        line = fid.readline()
892        fid.close()
893
894        fields = line.split(',')
895        msg = 'File %s must have the format date, value0 value1 value2 ...'
896        msg += ' or date, x, y, value0 value1 value2 ...'
897        mode = len(fields)
898        assert mode in [2,4], msg
899
900        #time.strptime(fields[0], time_format)
901        try:
902            starttime = calendar.timegm(time.strptime(fields[0], time_format))
903        except ValueError:
904            msg = 'First field in file %s must be' %filename
905            msg += ' date-time with format %s.\n' %time_format
906            msg += 'I got %s instead.' %fields[0]
907            raise msg
908
909
910        #Split values
911        values = []
912        for value in fields[mode-1].split():
913            values.append(float(value))
914
915        q = ensure_numeric(values)
916
917        msg = 'ERROR: File must contain at least one independent value'
918        assert len(q.shape) == 1, msg
919
920        self.number_of_values = len(q)
921
922        self.filename = filename
923        self.starttime = starttime
924        self.domain = domain
925
926        if domain is not None:
927            msg = 'WARNING: Start time as specified in domain (%s)'\
928                  %domain.starttime
929            msg += ' is earlier than the starttime of file %s: %s.'\
930                     %(self.filename, self.starttime)
931            msg += 'Modifying starttime accordingly.'
932            if self.starttime > domain.starttime:
933                #FIXME: Print depending on some verbosity setting
934                ##if verbose: print msg
935                domain.starttime = self.starttime #Modifying model time
936
937            #if domain.starttime is None:
938            #    domain.starttime = self.starttime
939            #else:
940            #    msg = 'WARNING: Start time as specified in domain (%s)'\
941            #          %domain.starttime
942            #    msg += ' is earlier than the starttime of file %s: %s.'\
943            #             %(self.filename, self.starttime)
944            #    msg += 'Modifying starttime accordingly.'
945            #    if self.starttime > domain.starttime:
946            #        #FIXME: Print depending on some verbosity setting
947            #        #print msg
948            #        domain.starttime = self.starttime #Modifying model time
949
950        if mode == 2:
951            self.read_times() #Now read all times in.
952        else:
953            raise 'x,y dependency not yet implemented'
954
955
956    def read_times(self):
957        """Read time and values
958        """
959        from Numeric import zeros, Float, alltrue
960        from config import time_format
961        import time, calendar
962
963        fid = open(self.filename)
964        lines = fid.readlines()
965        fid.close()
966
967        N = len(lines)
968        d = self.number_of_values
969
970        T = zeros(N, Float)       #Time
971        Q = zeros((N, d), Float)  #Values
972
973        for i, line in enumerate(lines):
974            fields = line.split(',')
975            realtime = calendar.timegm(time.strptime(fields[0], time_format))
976
977            T[i] = realtime - self.starttime
978
979            for j, value in enumerate(fields[1].split()):
980                Q[i, j] = float(value)
981
982        msg = 'File %s must list time as a monotonuosly ' %self.filename
983        msg += 'increasing sequence'
984        assert alltrue( T[1:] - T[:-1] > 0 ), msg
985
986        self.T = T     #Time
987        self.Q = Q     #Values
988        self.index = 0 #Initial index
989
990
991    def __repr__(self):
992        return 'File function'
993
994    def __call__(self, t, x=None, y=None, point_id=None):
995        """Evaluate f(t,x,y)
996
997        FIXME: x, y dependency not yet implemented except that
998        result is a vector of same length as x and y
999        FIXME: Naaaa
1000
1001        FIXME: Rethink semantics when x,y are vectors.
1002        """
1003
1004        from math import pi, cos, sin, sqrt
1005
1006
1007        #Find time tau such that
1008        #
1009        # domain.starttime + t == self.starttime + tau
1010
1011        if self.domain is not None:
1012            tau = self.domain.starttime-self.starttime+t
1013        else:
1014            tau = t
1015
1016
1017        msg = 'Time interval derived from file %s (%s:%s) does not match model time: %s'\
1018              %(self.filename, self.T[0], self.T[1], tau)
1019        if tau < self.T[0]: raise msg
1020        if tau > self.T[-1]: raise msg
1021
1022        oldindex = self.index
1023
1024        #Find slot
1025        while tau > self.T[self.index]: self.index += 1
1026        while tau < self.T[self.index]: self.index -= 1
1027
1028        #t is now between index and index+1
1029        ratio = (tau - self.T[self.index])/\
1030                (self.T[self.index+1] - self.T[self.index])
1031
1032        #Compute interpolated values
1033        q = self.Q[self.index,:] +\
1034            ratio*(self.Q[self.index+1,:] - self.Q[self.index,:])
1035
1036        #Return vector of interpolated values
1037        if x == None and y == None:
1038            return q
1039        else:
1040            try:
1041                N = len(x)
1042            except:
1043                return q
1044            else:
1045                from Numeric import ones, Float
1046                #x is a vector - Create one constant column for each value
1047                N = len(x)
1048                assert len(y) == N, 'x and y must have same length'
1049
1050                res = []
1051                for col in q:
1052                    res.append(col*ones(N, Float))
1053
1054                return res
1055
1056
1057#FIXME: TEMP
1058class File_function_copy:
1059    """Read time series from file and return a callable object:
1060    f(t,x,y)
1061    which will return interpolated values based on the input file.
1062
1063    The file format is assumed to be either two fields separated by a comma:
1064
1065        time [DD/MM/YY hh:mm:ss], value0 value1 value2 ...
1066
1067    or four comma separated fields
1068
1069        time [DD/MM/YY hh:mm:ss], x, y, value0 value1 value2 ...
1070
1071    In either case, the callable object will return a tuple of
1072    interpolated values, one each value stated in the file.
1073
1074
1075    E.g
1076
1077      31/08/04 00:00:00, 1.328223 0 0
1078      31/08/04 00:15:00, 1.292912 0 0
1079
1080    will provide a time dependent function f(t,x=None,y=None),
1081    ignoring x and y, which returns three values per call.
1082
1083
1084    NOTE: At this stage the function is assumed to depend on
1085    time only, i.e  no spatial dependency!!!!!
1086    When that is needed we can use the least_squares interpolation.
1087
1088    #FIXME: This should work with netcdf (e.g. sww) and thus render the
1089    #spatio-temporal boundary condition in shallow water fully general
1090    """
1091
1092
1093    def __init__(self, filename, domain=None):
1094        """Initialise callable object from a file.
1095        See docstring for class File_function
1096
1097        If domain is specified, model time (domain,starttime)
1098        will be checked and possibly modified
1099
1100        All times are assumed to be in UTC
1101        """
1102
1103        import time, calendar
1104        from Numeric import array
1105        from config import time_format
1106
1107        assert type(filename) == type(''),\
1108               'First argument to File_function must be a string'
1109
1110
1111        try:
1112            fid = open(filename)
1113        except Exception, e:
1114            msg = 'File "%s" could not be opened: Error="%s"'\
1115                  %(filename, e)
1116            raise msg
1117
1118
1119        line = fid.readline()
1120        fid.close()
1121        fields = line.split(',')
1122        msg = 'File %s must have the format date, value0 value1 value2 ...'
1123        msg += ' or date, x, y, value0 value1 value2 ...'
1124        mode = len(fields)
1125        assert mode in [2,4], msg
1126
1127        try:
1128            starttime = calendar.timegm(time.strptime(fields[0], time_format))
1129        except ValueError:
1130            msg = 'First field in file %s must be' %filename
1131            msg += ' date-time with format %s.\n' %time_format
1132            msg += 'I got %s instead.' %fields[0]
1133            raise msg
1134
1135
1136        #Split values
1137        values = []
1138        for value in fields[mode-1].split():
1139            values.append(float(value))
1140
1141        q = ensure_numeric(values)
1142
1143        msg = 'ERROR: File must contain at least one independent value'
1144        assert len(q.shape) == 1, msg
1145
1146        self.number_of_values = len(q)
1147
1148        self.filename = filename
1149        self.starttime = starttime
1150        self.domain = domain
1151
1152        if domain is not None:
1153            if domain.starttime is None:
1154                domain.starttime = self.starttime
1155            else:
1156                msg = 'WARNING: Start time as specified in domain (%s)'\
1157                      %domain.starttime
1158                msg += ' is earlier than the starttime of file %s: %s.'\
1159                         %(self.filename, self.starttime)
1160                msg += 'Modifying starttime accordingly.'
1161                if self.starttime > domain.starttime:
1162                    #FIXME: Print depending on some verbosity setting
1163                    #print msg
1164                    domain.starttime = self.starttime #Modifying model time
1165
1166        if mode == 2:
1167            self.read_times() #Now read all times in.
1168        else:
1169            raise 'x,y dependency not yet implemented'
1170
1171
1172    def read_times(self):
1173        """Read time and values
1174        """
1175        from Numeric import zeros, Float, alltrue
1176        from config import time_format
1177        import time, calendar
1178
1179        fid = open(self.filename)
1180        lines = fid.readlines()
1181        fid.close()
1182
1183        N = len(lines)
1184        d = self.number_of_values
1185
1186        T = zeros(N, Float)       #Time
1187        Q = zeros((N, d), Float)  #Values
1188
1189        for i, line in enumerate(lines):
1190            fields = line.split(',')
1191            realtime = calendar.timegm(time.strptime(fields[0], time_format))
1192
1193            T[i] = realtime - self.starttime
1194
1195            for j, value in enumerate(fields[1].split()):
1196                Q[i, j] = float(value)
1197
1198        msg = 'File %s must list time as a monotonuosly ' %self.filename
1199        msg += 'increasing sequence'
1200        assert alltrue( T[1:] - T[:-1] > 0 ), msg
1201
1202        self.T = T     #Time
1203        self.Q = Q     #Values
1204        self.index = 0 #Initial index
1205
1206
1207    def __repr__(self):
1208        return 'File function'
1209
1210
1211
1212    def __call__(self, t, x=None, y=None):
1213        """Evaluate f(t,x,y)
1214
1215        FIXME: x, y dependency not yet implemented except that
1216        result is a vector of same length as x and y
1217        FIXME: Naaaa
1218        """
1219
1220        from math import pi, cos, sin, sqrt
1221
1222
1223        #Find time tau such that
1224        #
1225        # domain.starttime + t == self.starttime + tau
1226
1227        if self.domain is not None:
1228            tau = self.domain.starttime-self.starttime+t
1229        else:
1230            tau = t
1231
1232
1233        msg = 'Time interval derived from file %s (%s:%s) does not match model time: %s'\
1234              %(self.filename, self.T[0], self.T[1], tau)
1235        if tau < self.T[0]: raise msg
1236        if tau > self.T[-1]: raise msg
1237
1238        oldindex = self.index
1239
1240        #Find slot
1241        while tau > self.T[self.index]: self.index += 1
1242        while tau < self.T[self.index]: self.index -= 1
1243
1244        #t is now between index and index+1
1245        ratio = (tau - self.T[self.index])/\
1246                (self.T[self.index+1] - self.T[self.index])
1247
1248        #Compute interpolated values
1249        q = self.Q[self.index,:] +\
1250            ratio*(self.Q[self.index+1,:] - self.Q[self.index,:])
1251
1252        #Return vector of interpolated values
1253        if x == None and y == None:
1254            return q
1255        else:
1256            try:
1257                N = len(x)
1258            except:
1259                return q
1260            else:
1261                from Numeric import ones, Float
1262                #x is a vector - Create one constant column for each value
1263                N = len(x)
1264                assert len(y) == N, 'x and y must have same length'
1265
1266                res = []
1267                for col in q:
1268                    res.append(col*ones(N, Float))
1269
1270                return res
1271
1272
1273def read_xya(filename, format = 'netcdf'):
1274    """Read simple xya file, possibly with a header in the first line, just
1275    x y [attributes]
1276    separated by whitespace
1277
1278    Format can be either ASCII or NetCDF
1279
1280    Return list of points, list of attributes and
1281    attribute names if present otherwise None
1282    """
1283    #FIXME: Probably obsoleted by similar function in load_ASCII
1284    #FIXE: Name read_data_points (or see 'load' in pylab)
1285
1286    from Scientific.IO.NetCDF import NetCDFFile
1287
1288    if format.lower() == 'netcdf':
1289        #Get NetCDF
1290
1291        fid = NetCDFFile(filename, 'r')
1292
1293        # Get the variables
1294        points = fid.variables['points']
1295        keys = fid.variables.keys()
1296        attributes = {}
1297        for key in keys:
1298            attributes[key] = fid.variables[key]
1299        #Don't close - arrays are needed outside this function,
1300        #alternatively take a copy here
1301    else:
1302        #Read as ASCII file assuming that it is separated by whitespace
1303        fid = open(filename)
1304        lines = fid.readlines()
1305        fid.close()
1306
1307        #Check if there is a header line
1308        fields = lines[0].strip().split()
1309        try:
1310            float(fields[0])
1311        except:
1312            #This must be a header line
1313            attribute_names = fields
1314            lines = lines[1:]
1315        else:
1316            attribute_names = ['elevation']  #HACK
1317
1318        attributes = {}
1319        for key in attribute_names:
1320            attributes[key] = []
1321
1322        points = []
1323        for line in lines:
1324            fields = line.strip().split()
1325            points.append( (float(fields[0]), float(fields[1])) )
1326            for i, key in enumerate(attribute_names):
1327                attributes[key].append( float(fields[2+i]) )
1328
1329    return points, attributes
1330
1331
1332#####################################
1333#POLYGON STUFF
1334#
1335#FIXME: All these should be put into new module polygon.py
1336
1337
1338
1339#These have been redefined to use separate_by_polygon which will
1340#put all inside indices in the first part of the indices array and
1341#outside indices in the last
1342
1343def inside_polygon(points, polygon, closed = True, verbose = False):
1344    """See separate_points_by_polygon for documentation
1345    """
1346
1347    from Numeric import array, Float, reshape
1348
1349    if verbose: print 'Checking input to inside_polygon'
1350    try:
1351        points = ensure_numeric(points, Float)
1352    except:
1353        msg = 'Points could not be converted to Numeric array'
1354        raise msg
1355
1356    try:
1357        polygon = ensure_numeric(polygon, Float)
1358    except:
1359        msg = 'Polygon could not be converted to Numeric array'
1360        raise msg
1361
1362
1363
1364    if len(points.shape) == 1:
1365        one_point = True
1366        points = reshape(points, (1,2))
1367    else:
1368        one_point = False
1369
1370    indices, count = separate_points_by_polygon(points, polygon,
1371                                                closed, verbose)
1372
1373    if one_point:
1374        return count == 1
1375    else:
1376        return indices[:count]
1377
1378def outside_polygon(points, polygon, closed = True, verbose = False):
1379    """See separate_points_by_polygon for documentation
1380    """
1381
1382    from Numeric import array, Float, reshape
1383
1384    if verbose: print 'Checking input to outside_polygon'
1385    try:
1386        points = ensure_numeric(points, Float)
1387    except:
1388        msg = 'Points could not be converted to Numeric array'
1389        raise msg
1390
1391    try:
1392        polygon = ensure_numeric(polygon, Float)
1393    except:
1394        msg = 'Polygon could not be converted to Numeric array'
1395        raise msg
1396
1397
1398
1399    if len(points.shape) == 1:
1400        one_point = True
1401        points = reshape(points, (1,2))
1402    else:
1403        one_point = False
1404
1405    indices, count = separate_points_by_polygon(points, polygon,
1406                                                closed, verbose)
1407
1408    if one_point:
1409        return count != 1
1410    else:
1411        return indices[count:][::-1]  #return reversed
1412
1413
1414def separate_points_by_polygon(points, polygon,
1415                               closed = True, verbose = False):
1416    """Determine whether points are inside or outside a polygon
1417
1418    Input:
1419       points - Tuple of (x, y) coordinates, or list of tuples
1420       polygon - list of vertices of polygon
1421       closed - (optional) determine whether points on boundary should be
1422       regarded as belonging to the polygon (closed = True)
1423       or not (closed = False)
1424
1425    Outputs:
1426       indices: array of same length as points with indices of points falling
1427       inside the polygon listed from the beginning and indices of points
1428       falling outside listed from the end.
1429
1430       count: count of points falling inside the polygon
1431
1432       The indices of points inside are obtained as indices[:count]
1433       The indices of points outside are obtained as indices[count:]
1434
1435
1436    Examples:
1437       U = [[0,0], [1,0], [1,1], [0,1]] #Unit square
1438
1439       separate_points_by_polygon( [[0.5, 0.5], [1, -0.5], [0.3, 0.2]], U)
1440       will return the indices [0, 2, 1] and count == 2 as only the first
1441       and the last point are inside the unit square
1442
1443    Remarks:
1444       The vertices may be listed clockwise or counterclockwise and
1445       the first point may optionally be repeated.
1446       Polygons do not need to be convex.
1447       Polygons can have holes in them and points inside a hole is
1448       regarded as being outside the polygon.
1449
1450    Algorithm is based on work by Darel Finley,
1451    http://www.alienryderflex.com/polygon/
1452
1453    """
1454
1455    from Numeric import array, Float, reshape, Int, zeros
1456
1457
1458    #Input checks
1459    try:
1460        points = ensure_numeric(points, Float)
1461    except:
1462        msg = 'Points could not be converted to Numeric array'
1463        raise msg
1464
1465    try:
1466        polygon = ensure_numeric(polygon, Float)
1467    except:
1468        msg = 'Polygon could not be converted to Numeric array'
1469        raise msg
1470
1471    assert len(polygon.shape) == 2,\
1472       'Polygon array must be a 2d array of vertices'
1473
1474    assert polygon.shape[1] == 2,\
1475       'Polygon array must have two columns'
1476
1477    assert len(points.shape) == 2,\
1478       'Points array must be a 2d array'
1479
1480    assert points.shape[1] == 2,\
1481       'Points array must have two columns'
1482
1483    N = polygon.shape[0] #Number of vertices in polygon
1484    M = points.shape[0]  #Number of points
1485
1486    px = polygon[:,0]
1487    py = polygon[:,1]
1488
1489    #Used for an optimisation when points are far away from polygon
1490    minpx = min(px); maxpx = max(px)
1491    minpy = min(py); maxpy = max(py)
1492
1493
1494    #Begin main loop
1495    indices = zeros(M, Int)
1496
1497    inside_index = 0    #Keep track of points inside
1498    outside_index = M-1 #Keep track of points outside (starting from end)
1499
1500    for k in range(M):
1501
1502        if verbose:
1503            if k %((M+10)/10)==0: print 'Doing %d of %d' %(k, M)
1504
1505        #for each point
1506        x = points[k, 0]
1507        y = points[k, 1]
1508
1509        inside = False
1510
1511        if not x > maxpx or x < minpx or y > maxpy or y < minpy:
1512            #Check polygon
1513            for i in range(N):
1514                j = (i+1)%N
1515
1516                #Check for case where point is contained in line segment
1517                if point_on_line(x, y, px[i], py[i], px[j], py[j]):
1518                    if closed:
1519                        inside = True
1520                    else:
1521                        inside = False
1522                    break
1523                else:
1524                    #Check if truly inside polygon
1525                    if py[i] < y and py[j] >= y or\
1526                       py[j] < y and py[i] >= y:
1527                        if px[i] + (y-py[i])/(py[j]-py[i])*(px[j]-px[i]) < x:
1528                            inside = not inside
1529
1530        if inside:
1531            indices[inside_index] = k
1532            inside_index += 1
1533        else:
1534            indices[outside_index] = k
1535            outside_index -= 1
1536
1537    return indices, inside_index
1538
1539
1540def separate_points_by_polygon_c(points, polygon,
1541                                 closed = True, verbose = False):
1542    """Determine whether points are inside or outside a polygon
1543
1544    C-wrapper
1545    """
1546
1547    from Numeric import array, Float, reshape, zeros, Int
1548
1549
1550    if verbose: print 'Checking input to separate_points_by_polygon'
1551    #Input checks
1552    try:
1553        points = ensure_numeric(points, Float)
1554    except:
1555        msg = 'Points could not be converted to Numeric array'
1556        raise msg
1557
1558    #if verbose: print 'Checking input to separate_points_by_polygon 2'
1559    try:
1560        polygon = ensure_numeric(polygon, Float)
1561    except:
1562        msg = 'Polygon could not be converted to Numeric array'
1563        raise msg
1564
1565    if verbose: print 'check'
1566
1567    assert len(polygon.shape) == 2,\
1568       'Polygon array must be a 2d array of vertices'
1569
1570    assert polygon.shape[1] == 2,\
1571       'Polygon array must have two columns'
1572
1573    assert len(points.shape) == 2,\
1574       'Points array must be a 2d array'
1575
1576    assert points.shape[1] == 2,\
1577       'Points array must have two columns'
1578
1579    N = polygon.shape[0] #Number of vertices in polygon
1580    M = points.shape[0]  #Number of points
1581
1582    from util_ext import separate_points_by_polygon
1583
1584    if verbose: print 'Allocating array for indices'
1585
1586    indices = zeros( M, Int )
1587
1588    if verbose: print 'Calling C-version of inside poly'
1589    count = separate_points_by_polygon(points, polygon, indices,
1590                                       int(closed), int(verbose))
1591
1592    return indices, count
1593
1594
1595
1596class Polygon_function:
1597    """Create callable object f: x,y -> z, where a,y,z are vectors and
1598    where f will return different values depending on whether x,y belongs
1599    to specified polygons.
1600
1601    To instantiate:
1602
1603       Polygon_function(polygons)
1604
1605    where polygons is a list of tuples of the form
1606
1607      [ (P0, v0), (P1, v1), ...]
1608
1609      with Pi being lists of vertices defining polygons and vi either
1610      constants or functions of x,y to be applied to points with the polygon.
1611
1612    The function takes an optional argument, default which is the value
1613    (or function) to used for points not belonging to any polygon.
1614    For example:
1615
1616       Polygon_function(polygons, default = 0.03)
1617
1618    If omitted the default value will be 0.0
1619
1620    Note: If two polygons overlap, the one last in the list takes precedence
1621
1622    FIXME? : Currently, coordinates specified here are assumed to be relative to the origin (georeference) used by domain. This function is more general than domain so perhaps it'd be an idea to allow the optional argument georeference???
1623
1624    """
1625
1626    def __init__(self, regions, default = 0.0):
1627
1628        try:
1629            len(regions)
1630        except:
1631            msg = 'Polygon_function takes a list of pairs (polygon, value). Got %s' %polygons
1632            raise msg
1633
1634
1635        T = regions[0]
1636        try:
1637            a = len(T)
1638        except:
1639            msg = 'Polygon_function takes a list of pairs (polygon, value). Got %s' %polygons
1640            raise msg
1641
1642        assert a == 2, 'Must have two component each: %s' %T
1643
1644        self.regions = regions
1645        self.default = default
1646
1647
1648    def __call__(self, x, y):
1649        from util import inside_polygon
1650        from Numeric import ones, Float, concatenate, array, reshape, choose
1651
1652        x = array(x).astype(Float)
1653        y = array(y).astype(Float)
1654
1655        N = len(x)
1656        assert len(y) == N
1657
1658        points = concatenate( (reshape(x, (N, 1)),
1659                               reshape(y, (N, 1))), axis=1 )
1660
1661        if callable(self.default):
1662            z = self.default(x,y)
1663        else:
1664            z = ones(N, Float) * self.default
1665
1666        for polygon, value in self.regions:
1667            indices = inside_polygon(points, polygon)
1668
1669            #FIXME: This needs to be vectorised
1670            if callable(value):
1671                for i in indices:
1672                    xx = array([x[i]])
1673                    yy = array([y[i]])
1674                    z[i] = value(xx, yy)[0]
1675            else:
1676                for i in indices:
1677                    z[i] = value
1678
1679        return z
1680
1681def read_polygon(filename):
1682    """Read points assumed to form a polygon
1683       There must be exactly two numbers in each line
1684    """
1685
1686    #Get polygon
1687    fid = open(filename)
1688    lines = fid.readlines()
1689    fid.close()
1690    polygon = []
1691    for line in lines:
1692        fields = line.split(',')
1693        polygon.append( [float(fields[0]), float(fields[1])] )
1694
1695    return polygon
1696
1697def populate_polygon(polygon, number_of_points, seed = None):
1698    """Populate given polygon with uniformly distributed points.
1699
1700    Input:
1701       polygon - list of vertices of polygon
1702       number_of_points - (optional) number of points
1703       seed - seed for random number generator (default=None)
1704
1705    Output:
1706       points - list of points inside polygon
1707
1708    Examples:
1709       populate_polygon( [[0,0], [1,0], [1,1], [0,1]], 5 )
1710       will return five randomly selected points inside the unit square
1711    """
1712
1713    from random import uniform, seed
1714
1715    seed(seed)
1716
1717    points = []
1718
1719    #Find outer extent of polygon
1720    max_x = min_x = polygon[0][0]
1721    max_y = min_y = polygon[0][1]
1722    for point in polygon[1:]:
1723        x = point[0]
1724        if x > max_x: max_x = x
1725        if x < min_x: min_x = x
1726        y = point[1]
1727        if y > max_y: max_y = y
1728        if y < min_y: min_y = y
1729
1730
1731    while len(points) < number_of_points:
1732        x = uniform(min_x, max_x)
1733        y = uniform(min_y, max_y)
1734
1735        if inside_polygon( [x,y], polygon ):
1736            points.append([x,y])
1737
1738    return points
1739
1740def tsh2sww(filename, verbose=True):
1741    """
1742    to check if a tsh/msh file 'looks' good.
1743    """
1744    from shallow_water import Domain
1745    from pmesh2domain import pmesh_to_domain_instance
1746    import time, os
1747    from data_manager import get_dataobject
1748    from os import sep, path
1749    #from util import mean
1750
1751    if verbose == True:print 'Creating domain from', filename
1752    domain = pmesh_to_domain_instance(filename, Domain)
1753    if verbose == True:print "Number of triangles = ", len(domain)
1754
1755    domain.smooth = True
1756    domain.format = 'sww'   #Native netcdf visualisation format
1757    file_path, filename = path.split(filename)
1758    filename, ext = path.splitext(filename)
1759    domain.filename = filename
1760    domain.reduction = mean
1761    if verbose == True:print "file_path",file_path
1762    if file_path == "":file_path = "."
1763    domain.set_datadir(file_path)
1764
1765    if verbose == True:
1766        print "Output written to " + domain.get_datadir() + sep + \
1767              domain.filename + "." + domain.format
1768    sww = get_dataobject(domain)
1769    sww.store_connectivity()
1770    sww.store_timestep('stage')
1771
1772####################################################################
1773#Python versions of function that are also implemented in util_gateway.c
1774#
1775
1776def gradient_python(x0, y0, x1, y1, x2, y2, q0, q1, q2):
1777    """
1778    """
1779
1780    det = (y2-y0)*(x1-x0) - (y1-y0)*(x2-x0)
1781    a = (y2-y0)*(q1-q0) - (y1-y0)*(q2-q0)
1782    a /= det
1783
1784    b = (x1-x0)*(q2-q0) - (x2-x0)*(q1-q0)
1785    b /= det
1786
1787    return a, b
1788
1789
1790def gradient2_python(x0, y0, x1, y1, q0, q1):
1791    """Compute radient based on two points and enforce zero gradient
1792    in the direction orthogonal to (x1-x0), (y1-y0)
1793    """
1794
1795    #Old code
1796    #det = x0*y1 - x1*y0
1797    #if det != 0.0:
1798    #    a = (y1*q0 - y0*q1)/det
1799    #    b = (x0*q1 - x1*q0)/det
1800
1801    #Correct code (ON)
1802    det = (x1-x0)**2 + (y1-y0)**2
1803    if det != 0.0:
1804        a = (x1-x0)*(q1-q0)/det
1805        b = (y1-y0)*(q1-q0)/det
1806       
1807    return a, b       
1808
1809
1810##############################################
1811#Initialise module
1812
1813import compile
1814if compile.can_use_C_extension('util_ext.c'):
1815    from util_ext import gradient, gradient2, point_on_line
1816    separate_points_by_polygon = separate_points_by_polygon_c
1817else:
1818    gradient = gradient_python
1819    gradient2 = gradient2_python   
1820
1821
1822if __name__ == "__main__":
1823    pass
1824
1825
1826
1827
1828
1829
1830#OBSOLETED STUFF
1831def inside_polygon_old(point, polygon, closed = True, verbose = False):
1832    #FIXME Obsoleted
1833    """Determine whether points are inside or outside a polygon
1834
1835    Input:
1836       point - Tuple of (x, y) coordinates, or list of tuples
1837       polygon - list of vertices of polygon
1838       closed - (optional) determine whether points on boundary should be
1839       regarded as belonging to the polygon (closed = True)
1840       or not (closed = False)
1841
1842    Output:
1843       If one point is considered, True or False is returned.
1844       If multiple points are passed in, the function returns indices
1845       of those points that fall inside the polygon
1846
1847    Examples:
1848       U = [[0,0], [1,0], [1,1], [0,1]] #Unit square
1849       inside_polygon( [0.5, 0.5], U)
1850       will evaluate to True as the point 0.5, 0.5 is inside the unit square
1851
1852       inside_polygon( [[0.5, 0.5], [1, -0.5], [0.3, 0.2]], U)
1853       will return the indices [0, 2] as only the first and the last point
1854       is inside the unit square
1855
1856    Remarks:
1857       The vertices may be listed clockwise or counterclockwise and
1858       the first point may optionally be repeated.
1859       Polygons do not need to be convex.
1860       Polygons can have holes in them and points inside a hole is
1861       regarded as being outside the polygon.
1862
1863
1864    Algorithm is based on work by Darel Finley,
1865    http://www.alienryderflex.com/polygon/
1866
1867    """
1868
1869    from Numeric import array, Float, reshape
1870
1871
1872    #Input checks
1873    try:
1874        point = array(point).astype(Float)
1875    except:
1876        msg = 'Point %s could not be converted to Numeric array' %point
1877        raise msg
1878
1879    try:
1880        polygon = array(polygon).astype(Float)
1881    except:
1882        msg = 'Polygon %s could not be converted to Numeric array' %polygon
1883        raise msg
1884
1885    assert len(polygon.shape) == 2,\
1886       'Polygon array must be a 2d array of vertices: %s' %polygon
1887
1888
1889    assert polygon.shape[1] == 2,\
1890       'Polygon array must have two columns: %s' %polygon
1891
1892
1893    if len(point.shape) == 1:
1894        one_point = True
1895        points = reshape(point, (1,2))
1896    else:
1897        one_point = False
1898        points = point
1899
1900    N = polygon.shape[0] #Number of vertices in polygon
1901    M = points.shape[0]  #Number of points
1902
1903    px = polygon[:,0]
1904    py = polygon[:,1]
1905
1906    #Used for an optimisation when points are far away from polygon
1907    minpx = min(px); maxpx = max(px)
1908    minpy = min(py); maxpy = max(py)
1909
1910
1911    #Begin main loop (FIXME: It'd be crunchy to have this written in C:-)
1912    indices = []
1913    for k in range(M):
1914
1915        if verbose:
1916            if k %((M+10)/10)==0: print 'Doing %d of %d' %(k, M)
1917
1918        #for each point
1919        x = points[k, 0]
1920        y = points[k, 1]
1921
1922        inside = False
1923
1924        #Optimisation
1925        if x > maxpx or x < minpx: continue
1926        if y > maxpy or y < minpy: continue
1927
1928        #Check polygon
1929        for i in range(N):
1930            j = (i+1)%N
1931
1932            #Check for case where point is contained in line segment
1933            if point_on_line(x, y, px[i], py[i], px[j], py[j]):
1934                if closed:
1935                    inside = True
1936                else:
1937                    inside = False
1938                break
1939            else:
1940                #Check if truly inside polygon
1941                if py[i] < y and py[j] >= y or\
1942                   py[j] < y and py[i] >= y:
1943                    if px[i] + (y-py[i])/(py[j]-py[i])*(px[j]-px[i]) < x:
1944                        inside = not inside
1945
1946        if inside: indices.append(k)
1947
1948    if one_point:
1949        return inside
1950    else:
1951        return indices
1952
1953
1954#def remove_from(A, B):
1955#    """Assume that A
1956#    """
1957#    from Numeric import search_sorted##
1958#
1959#    ind = search_sorted(A, B)
1960
1961
1962
1963def outside_polygon_old(point, polygon, closed = True, verbose = False):
1964    #OBSOLETED
1965    """Determine whether points are outside a polygon
1966
1967    Input:
1968       point - Tuple of (x, y) coordinates, or list of tuples
1969       polygon - list of vertices of polygon
1970       closed - (optional) determine whether points on boundary should be
1971       regarded as belonging to the polygon (closed = True)
1972       or not (closed = False)
1973
1974    Output:
1975       If one point is considered, True or False is returned.
1976       If multiple points are passed in, the function returns indices
1977       of those points that fall outside the polygon
1978
1979    Examples:
1980       U = [[0,0], [1,0], [1,1], [0,1]] #Unit square
1981       outside_polygon( [0.5, 0.5], U )
1982       will evaluate to False as the point 0.5, 0.5 is inside the unit square
1983
1984       ouside_polygon( [1.5, 0.5], U )
1985       will evaluate to True as the point 1.5, 0.5 is outside the unit square
1986
1987       outside_polygon( [[0.5, 0.5], [1, -0.5], [0.3, 0.2]], U )
1988       will return the indices [1] as only the first and the last point
1989       is inside the unit square
1990    """
1991
1992    #FIXME: This is too slow
1993
1994    res  = inside_polygon(point, polygon, closed, verbose)
1995
1996    if res is True or res is False:
1997        return not res
1998
1999    #Now invert indices
2000    from Numeric import arrayrange, compress
2001    outside_indices = arrayrange(len(point))
2002    for i in res:
2003        outside_indices = compress(outside_indices != i, outside_indices)
2004    return outside_indices
2005
2006def inside_polygon_c(point, polygon, closed = True, verbose = False):
2007    #FIXME: Obsolete
2008    """Determine whether points are inside or outside a polygon
2009
2010    C-wrapper
2011    """
2012
2013    from Numeric import array, Float, reshape, zeros, Int
2014
2015
2016    if verbose: print 'Checking input to inside_polygon'
2017    #Input checks
2018    try:
2019        point = array(point).astype(Float)
2020    except:
2021        msg = 'Point %s could not be converted to Numeric array' %point
2022        raise msg
2023
2024    try:
2025        polygon = array(polygon).astype(Float)
2026    except:
2027        msg = 'Polygon %s could not be converted to Numeric array' %polygon
2028        raise msg
2029
2030    assert len(polygon.shape) == 2,\
2031       'Polygon array must be a 2d array of vertices: %s' %polygon
2032
2033
2034    assert polygon.shape[1] == 2,\
2035       'Polygon array must have two columns: %s' %polygon
2036
2037
2038    if len(point.shape) == 1:
2039        one_point = True
2040        points = reshape(point, (1,2))
2041    else:
2042        one_point = False
2043        points = point
2044
2045    from util_ext import inside_polygon
2046
2047    if verbose: print 'Allocating array for indices'
2048
2049    indices = zeros( points.shape[0], Int )
2050
2051    if verbose: print 'Calling C-version of inside poly'
2052    count = inside_polygon(points, polygon, indices,
2053                           int(closed), int(verbose))
2054
2055    if one_point:
2056        return count == 1 #Return True if the point was inside
2057    else:
2058        if verbose: print 'Got %d points' %count
2059
2060        return indices[:count]   #Return those indices that were inside
Note: See TracBrowser for help on using the repository browser.