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

Last change on this file since 1043 was 1043, checked in by ole, 20 years ago

Added diagnostics about boundary forcing

File size: 38.5 KB
Line 
1"""This modul contains various auxiliary function used by pyvolution.
2
3It is also a clearing house for function tat may later earn a module
4of their own.
5"""
6
7def angle(v):
8    """Compute angle between e1 (the unit vector in the x-direction)
9    and the specified vector
10    """
11
12    from math import acos, pi, sqrt
13    from Numeric import sum, array
14
15    l = sqrt( sum (array(v)**2))
16    v1 = v[0]/l
17    v2 = v[1]/l
18       
19    theta = acos(v1)
20
21    if v2 < 0:
22        #Quadrant 3 or 4
23        theta = 2*pi-theta
24
25    return theta
26
27
28def anglediff(v0, v1):
29    """Compute difference between angle of vector x0, y0 and x1, y1.
30    This is used for determining the ordering of vertices,
31    e.g. for checking if they are counter clockwise.
32   
33    Always return a positive value
34    """
35       
36    from math import pi
37   
38    a0 = angle(v0)
39    a1 = angle(v1)
40
41    #Ensure that difference will be positive
42    if a0 < a1:
43        a0 += 2*pi
44           
45    return a0-a1
46
47
48def mean(x):
49    from Numeric import sum
50    return sum(x)/len(x)
51
52
53def point_on_line(x, y, x0, y0, x1, y1): 
54    """Determine whether a point is on a line segment
55   
56    Input: x, y, x0, x0, x1, y1: where
57        point is given by x, y
58        line is given by (x0, y0) and (x1, y1)
59       
60    """
61       
62    from Numeric import array, dot, allclose
63    from math import sqrt
64
65    a = array([x - x0, y - y0]) 
66    a_normal = array([a[1], -a[0]])
67                       
68    b = array([x1 - x0, y1 - y0])
69   
70    if dot(a_normal, b) == 0:
71        #Point is somewhere on the infinite extension of the line
72
73        len_a = sqrt(sum(a**2))               
74        len_b = sqrt(sum(b**2))                                 
75        if dot(a, b) >= 0 and len_a <= len_b:
76           return True
77        else:   
78           return False
79    else:
80      return False 
81   
82
83def file_function(filename, domain=None, quantities = None, interpolation_points = None, verbose = False):
84    """If domain is specified, don't specify quantites as they are automatically derived
85    """
86
87
88    #FIXME (OLE): Should check origin of domain against that of file
89    #In fact, this is where origin should be converted to that of domain
90    #Also, check that file covers domain fully.
91   
92    assert type(filename) == type(''),\
93               'First argument to File_function must be a string'
94
95    try:
96        fid = open(filename)
97    except Exception, e:
98        msg = 'File "%s" could not be opened: Error="%s"'\
99                  %(filename, e)
100        raise msg
101
102    line = fid.readline()
103    fid.close()
104
105    if domain is not None:
106        quantities = domain.conserved_quantities   
107    else:
108        quantities = None
109       
110    #Choose format
111    #FIXME: Maybe these can be merged later on
112    if line[:3] == 'CDF':
113        return File_function_NetCDF(filename, domain, quantities, interpolation_points, verbose = verbose)
114    else:
115        return File_function_ASCII(filename, domain, quantities, interpolation_points)   
116           
117             
118class File_function_NetCDF:
119    """Read time history of spatial data from NetCDF sww file and
120    return a callable object f(t,x,y) 
121    which will return interpolated values based on the input file.   
122   
123    x, y may be either scalars or vectors
124   
125    #FIXME: More about format, interpolation  and order of quantities
126   
127    The quantities returned by the callable objects are specified by
128    the list quantities which must contain the names of the
129    quantities to be returned and also reflect the order, e.g. for
130    the shallow water wave equation, on would have
131    quantities = ['stage', 'xmomentum', 'ymomentum']
132   
133    interpolation_points decides at which points interpolated
134    quantities are to be computed whenever object is called.
135   
136   
137   
138    If None, return average value
139    """
140   
141    def  __init__(self, filename, domain=None, quantities=None,
142                  interpolation_points=None, verbose = False):
143        """Initialise callable object from a file.
144
145        If domain is specified, model time (domain.starttime)
146        will be checked and possibly modified
147       
148        All times are assumed to be in UTC
149        """
150
151        #FIXME: Check that model origin is the same as file's origin
152        #(both in UTM coordinates)
153        #If not - modify those from file to match domain
154       
155        import time, calendar
156        from config import time_format
157        from Scientific.IO.NetCDF import NetCDFFile     
158
159        #Open NetCDF file
160        if verbose: print 'Reading', filename
161        fid = NetCDFFile(filename, 'r')
162
163        if quantities is None or len(quantities) < 1:
164            msg = 'ERROR: File must contain at least one independent value'
165            raise msg
166
167        missing = []
168        for quantity in ['x', 'y', 'time'] + quantities:
169             if not fid.variables.has_key(quantity):
170                 missing.append(quantity)
171                 
172        if len(missing) > 0:
173            msg = 'Quantities %s could not be found in file %s'\
174                  %(str(missing), filename)
175            raise msg
176                 
177
178        #Get first timestep
179        try:
180            self.starttime = fid.starttime[0]
181        except ValueError:   
182            msg = 'Could not read starttime from file %s' %filename
183            raise msg
184
185        #Get origin
186        self.xllcorner = fid.xllcorner[0]
187        self.yllcorner = fid.yllcorner[0]           
188   
189        self.number_of_values = len(quantities)
190        self.fid = fid
191        self.filename = filename
192        self.quantities = quantities
193        self.domain = domain
194        self.interpolation_points = interpolation_points
195
196        if domain is not None:
197            msg = 'WARNING: Start time as specified in domain (%s)'\
198                  %domain.starttime
199            msg += ' is earlier than the starttime of file %s: %s.'\
200                     %(self.filename, self.starttime)
201            msg += 'Modifying starttime accordingly.'
202            if self.starttime > domain.starttime:
203                #FIXME: Print depending on some verbosity setting
204                #print msg
205                domain.starttime = self.starttime #Modifying model time
206
207        #Read all data in and produce values for desired data points at each timestep
208        self.spatial_interpolation(interpolation_points, verbose = verbose) 
209        fid.close()
210
211    def spatial_interpolation(self, interpolation_points, verbose = False):
212        """For each timestep in fid: read surface, interpolate to desired points
213        and store values for use when object is called.
214        """
215       
216        from Numeric import array, zeros, Float, alltrue, concatenate, reshape
217
218        from config import time_format
219        from least_squares import Interpolation
220        import time, calendar
221       
222        fid = self.fid
223
224        #Get variables
225        if verbose: print 'Get varibles'
226        x = fid.variables['x'][:]
227        y = fid.variables['y'][:]
228        z = fid.variables['elevation'][:]
229        triangles = fid.variables['volumes'][:]
230        time = fid.variables['time'][:]
231       
232        #Check
233        msg = 'File %s must list time as a monotonuosly ' %self.filename
234        msg += 'increasing sequence'
235        assert alltrue(time[1:] - time[:-1] > 0 ), msg 
236       
237
238        if interpolation_points is not None: 
239       
240            try:
241                P = array(interpolation_points) 
242            except:
243                msg = 'Interpolation points must be an N x 2 Numeric array or a list of points\n'
244                msg += 'I got: %s.' %( str(interpolation_points)[:60] + '...')
245                raise msg
246           
247           
248            self.T = time[:]  #Time assumed to be relative to starttime
249            self.index = 0    #Initial time index       
250
251            self.values = {}               
252            for name in self.quantities:
253                self.values[name] = zeros( (len(self.T), 
254                                            len(interpolation_points)), 
255                                            Float)                 
256
257            #Build interpolator
258            if verbose: print 'Build interpolation matrix'
259            x = reshape(x, (len(x),1))
260            y = reshape(y, (len(y),1))               
261            vertex_coordinates = concatenate((x,y), axis=1) #m x 2 array
262
263            interpol = Interpolation(vertex_coordinates, 
264                                     triangles,
265                                     point_coordinates = P,
266                                     alpha = 0, 
267                                     verbose = verbose)
268
269
270            if verbose: print 'Interpolate'
271            for i, t in enumerate(self.T):
272                #Interpolate quantities at this timestep
273                #print ' time step %d of %d' %(i, len(self.T))
274                for name in self.quantities:
275                    self.values[name][i, :] =\
276                    interpol.interpolate(fid.variables[name][i,:])   
277
278            #Report
279            if verbose:
280                print '------------------------------------------------'
281                print 'File_function statistics:'
282                print '  Name: %s' %self.filename
283                print '  Reference:'
284                print '    Lower left corner: [%f, %f]'\
285                      %(self.xllcorner, self.yllcorner) 
286                print '    Start time: %f' %self.starttime
287                print '  Extent:'
288                print '    x in [%f, %f], len(x) == %d'\
289                      %(min(x.flat), max(x.flat), len(x.flat))
290                print '    y in [%f, %f], len(y) == %d'\
291                      %(min(y.flat), max(y.flat), len(y.flat))
292                print '    t in [%f, %f], len(t) == %d'\
293                      %(min(self.T), max(self.T), len(self.T))
294                print '  Quantities:'
295                for name in self.quantities:
296                    q = fid.variables[name][:].flat
297                    print '    %s in [%f, %f]' %(name, min(q), max(q))
298
299                   
300                print '  Interpolation points (xi, eta):'\
301                      'number of points == %d ' %P.shape[0]
302                print '    xi in [%f, %f]' %(min(P[:,0]), max(P[:,0]))
303                print '    eta in [%f, %f]' %(min(P[:,1]), max(P[:,1]))
304                print '  Interpolated quantities (over all timesteps):'
305                for name in self.quantities:
306                    q = self.values[name][:].flat                   
307                    print '    %s at interpolation points in [%f, %f]'\
308                          %(name, min(q), max(q))               
309               
310               
311               
312
313                print '------------------------------------------------'
314        else:
315            msg = 'File_function_NetCDF must be invoked with '
316            msg += 'a list of interpolation points'
317            raise msg
318
319    def __repr__(self):
320        return 'File function'
321
322    def __call__(self, t, x=None, y=None, point_id = None):
323        """Evaluate f(t, point_id)
324       
325        Inputs:
326          t: time - Model time (tau = domain.starttime-self.starttime+t) 
327                    must lie within existing timesteps   
328          point_id: index of one of the preprocessed points.
329                    If point_id is None all preprocessed points are computed
330
331          FIXME: point_id could also be a slice     
332          FIXME: One could allow arbitrary x, y coordinates
333          (requires computation of a new interpolator)
334          Maybe not,.,.
335          FIXME: What if x and y are vectors?
336        """
337
338        from math import pi, cos, sin, sqrt
339        from Numeric import zeros, Float
340
341
342        if point_id is None:
343            msg = 'NetCDF File function needs a point_id when invoked'
344            raise msg
345
346
347        #Find time tau such that
348        #
349        # domain.starttime + t == self.starttime + tau
350       
351        if self.domain is not None:
352            tau = self.domain.starttime-self.starttime+t
353        else:
354            tau = t
355
356        #print 'D start', self.domain.starttime
357        #print 'S start', self.starttime
358        #print 't', t
359        #print 'tau', tau             
360               
361        msg = 'Time interval derived from file %s [%s:%s]'\
362              %(self.filename, self.T[0], self.T[1])
363        msg += ' does not match model time: %s' %tau
364
365        if tau < self.T[0]: raise msg
366        if tau > self.T[-1]: raise msg       
367
368
369        oldindex = self.index #Time index
370
371        #Find time slot
372        while tau > self.T[self.index]: self.index += 1
373        while tau < self.T[self.index]: self.index -= 1
374
375
376        if tau == self.T[self.index]:
377            #Protect against case where tau == T[-1] (last time)
378            # - also works in general when tau == T[i]
379            ratio = 0 
380        else:
381            #t is now between index and index+1           
382            ratio = (tau - self.T[self.index])/\
383                    (self.T[self.index+1] - self.T[self.index])
384
385
386
387               
388        #Compute interpolated values
389        q = zeros( len(self.quantities), Float)
390
391        for i, name in enumerate(self.quantities):
392            Q = self.values[name]   
393
394            if ratio > 0:
395                q[i] = Q[self.index, point_id] +\
396                       ratio*(Q[self.index+1, point_id] -\
397                              Q[self.index, point_id])
398            else:
399                q[i] = Q[self.index, point_id]
400
401
402
403        #Return vector of interpolated values
404        return q
405               
406             
407class File_function_ASCII:
408    """Read time series from file and return a callable object:
409    f(t,x,y)
410    which will return interpolated values based on the input file.
411
412    The file format is assumed to be either two fields separated by a comma:
413
414        time [DD/MM/YY hh:mm:ss], value0 value1 value2 ...
415
416    or four comma separated fields
417
418        time [DD/MM/YY hh:mm:ss], x, y, value0 value1 value2 ...
419
420    In either case, the callable object will return a tuple of
421    interpolated values, one each value stated in the file.
422
423   
424    E.g
425
426      31/08/04 00:00:00, 1.328223 0 0
427      31/08/04 00:15:00, 1.292912 0 0
428
429    will provide a time dependent function f(t,x=None,y=None),
430    ignoring x and y, which returns three values per call.
431
432   
433    NOTE: At this stage the function is assumed to depend on
434    time only, i.e  no spatial dependency!!!!!
435    When that is needed we can use the least_squares interpolation.
436   
437    #FIXME: This should work with netcdf (e.g. sww) and thus render the
438    #spatio-temporal boundary condition in shallow water fully general
439   
440    #FIXME: Specified quantites not used here -
441    #always return whatever is in the file
442    """
443
444   
445    def __init__(self, filename, domain=None, quantities = None, interpolation_points=None):
446        """Initialise callable object from a file.
447        See docstring for class File_function
448
449        If domain is specified, model time (domain,starttime)
450        will be checked and possibly modified
451
452        All times are assumed to be in UTC
453        """
454
455        import time, calendar
456        from Numeric import array
457        from config import time_format
458
459        fid = open(filename)
460        line = fid.readline()
461        fid.close()
462   
463        fields = line.split(',')
464        msg = 'File %s must have the format date, value0 value1 value2 ...'
465        msg += ' or date, x, y, value0 value1 value2 ...'
466        mode = len(fields)
467        assert mode in [2,4], msg
468
469        try:
470            starttime = calendar.timegm(time.strptime(fields[0], time_format))
471        except ValueError:   
472            msg = 'First field in file %s must be' %filename
473            msg += ' date-time with format %s.\n' %time_format
474            msg += 'I got %s instead.' %fields[0] 
475            raise msg
476
477
478        #Split values
479        values = []
480        for value in fields[mode-1].split():
481            values.append(float(value))
482
483        q = array(values)
484       
485        msg = 'ERROR: File must contain at least one independent value'
486        assert len(q.shape) == 1, msg
487           
488        self.number_of_values = len(q)
489
490        self.filename = filename
491        self.starttime = starttime
492        self.domain = domain
493
494        if domain is not None:
495            msg = 'WARNING: Start time as specified in domain (%s)'\
496                  %domain.starttime
497            msg += ' is earlier than the starttime of file %s: %s.'\
498                     %(self.filename, self.starttime)
499            msg += 'Modifying starttime accordingly.'
500            if self.starttime > domain.starttime:
501                #FIXME: Print depending on some verbosity setting
502                #print msg
503                domain.starttime = self.starttime #Modifying model time
504       
505            #if domain.starttime is None:
506            #    domain.starttime = self.starttime
507            #else:
508            #    msg = 'WARNING: Start time as specified in domain (%s)'\
509            #          %domain.starttime
510            #    msg += ' is earlier than the starttime of file %s: %s.'\
511            #             %(self.filename, self.starttime)
512            #    msg += 'Modifying starttime accordingly.'
513            #    if self.starttime > domain.starttime:
514            #        #FIXME: Print depending on some verbosity setting
515            #        #print msg
516            #        domain.starttime = self.starttime #Modifying model time
517
518        if mode == 2:       
519            self.read_times() #Now read all times in.
520        else:
521            raise 'x,y dependency not yet implemented'
522
523       
524    def read_times(self):
525        """Read time and values
526        """
527        from Numeric import zeros, Float, alltrue
528        from config import time_format
529        import time, calendar
530       
531        fid = open(self.filename)       
532        lines = fid.readlines()
533        fid.close()
534       
535        N = len(lines)
536        d = self.number_of_values
537
538        T = zeros(N, Float)       #Time
539        Q = zeros((N, d), Float)  #Values
540       
541        for i, line in enumerate(lines):
542            fields = line.split(',')
543            realtime = calendar.timegm(time.strptime(fields[0], time_format))
544
545            T[i] = realtime - self.starttime
546
547            for j, value in enumerate(fields[1].split()):
548                Q[i, j] = float(value)
549
550        msg = 'File %s must list time as a monotonuosly ' %self.filename
551        msg += 'increasing sequence'
552        assert alltrue( T[1:] - T[:-1] > 0 ), msg
553
554        self.T = T     #Time
555        self.Q = Q     #Values
556        self.index = 0 #Initial index
557
558       
559    def __repr__(self):
560        return 'File function'
561
562    def __call__(self, t, x=None, y=None, point_id=None):
563        """Evaluate f(t,x,y)
564
565        FIXME: x, y dependency not yet implemented except that
566        result is a vector of same length as x and y
567        FIXME: Naaaa
568       
569        FIXME: Rethink semantics when x,y are vectors.
570        """
571
572        from math import pi, cos, sin, sqrt
573       
574
575        #Find time tau such that
576        #
577        # domain.starttime + t == self.starttime + tau
578       
579        if self.domain is not None:
580            tau = self.domain.starttime-self.starttime+t
581        else:
582            tau = t
583       
584               
585        msg = 'Time interval derived from file %s (%s:%s) does not match model time: %s'\
586              %(self.filename, self.T[0], self.T[1], tau)
587        if tau < self.T[0]: raise msg
588        if tau > self.T[-1]: raise msg       
589
590        oldindex = self.index
591
592        #Find slot
593        while tau > self.T[self.index]: self.index += 1
594        while tau < self.T[self.index]: self.index -= 1
595
596        #t is now between index and index+1
597        ratio = (tau - self.T[self.index])/\
598                (self.T[self.index+1] - self.T[self.index])
599
600        #Compute interpolated values
601        q = self.Q[self.index,:] +\
602            ratio*(self.Q[self.index+1,:] - self.Q[self.index,:]) 
603
604        #Return vector of interpolated values
605        if x == None and y == None:
606            return q
607        else:
608            try:
609                N = len(x)
610            except:
611                return q
612            else:
613                from Numeric import ones, Float
614                #x is a vector - Create one constant column for each value
615                N = len(x)
616                assert len(y) == N, 'x and y must have same length'
617             
618                res = []
619                for col in q:
620                    res.append(col*ones(N, Float))
621           
622                return res
623           
624           
625#FIXME: TEMP       
626class File_function_copy:
627    """Read time series from file and return a callable object:
628    f(t,x,y)
629    which will return interpolated values based on the input file.
630
631    The file format is assumed to be either two fields separated by a comma:
632
633        time [DD/MM/YY hh:mm:ss], value0 value1 value2 ...
634
635    or four comma separated fields
636
637        time [DD/MM/YY hh:mm:ss], x, y, value0 value1 value2 ...
638
639    In either case, the callable object will return a tuple of
640    interpolated values, one each value stated in the file.
641
642   
643    E.g
644
645      31/08/04 00:00:00, 1.328223 0 0
646      31/08/04 00:15:00, 1.292912 0 0
647
648    will provide a time dependent function f(t,x=None,y=None),
649    ignoring x and y, which returns three values per call.
650
651   
652    NOTE: At this stage the function is assumed to depend on
653    time only, i.e  no spatial dependency!!!!!
654    When that is needed we can use the least_squares interpolation.
655   
656    #FIXME: This should work with netcdf (e.g. sww) and thus render the
657    #spatio-temporal boundary condition in shallow water fully general
658    """
659
660   
661    def __init__(self, filename, domain=None):
662        """Initialise callable object from a file.
663        See docstring for class File_function
664
665        If domain is specified, model time (domain,starttime)
666        will be checked and possibly modified
667
668        All times are assumed to be in UTC
669        """
670
671        import time, calendar
672        from Numeric import array
673        from config import time_format
674
675        assert type(filename) == type(''),\
676               'First argument to File_function must be a string'
677
678
679        try:
680            fid = open(filename)
681        except Exception, e:
682            msg = 'File "%s" could not be opened: Error="%s"'\
683                  %(filename, e)
684            raise msg
685
686
687        line = fid.readline()
688        fid.close()
689        fields = line.split(',')
690        msg = 'File %s must have the format date, value0 value1 value2 ...'
691        msg += ' or date, x, y, value0 value1 value2 ...'
692        mode = len(fields)
693        assert mode in [2,4], msg
694
695        try:
696            starttime = calendar.timegm(time.strptime(fields[0], time_format))
697        except ValueError:   
698            msg = 'First field in file %s must be' %filename
699            msg += ' date-time with format %s.\n' %time_format
700            msg += 'I got %s instead.' %fields[0] 
701            raise msg
702
703
704        #Split values
705        values = []
706        for value in fields[mode-1].split():
707            values.append(float(value))
708
709        q = array(values)
710       
711        msg = 'ERROR: File must contain at least one independent value'
712        assert len(q.shape) == 1, msg
713           
714        self.number_of_values = len(q)
715
716        self.filename = filename
717        self.starttime = starttime
718        self.domain = domain
719
720        if domain is not None:
721            if domain.starttime is None:
722                domain.starttime = self.starttime
723            else:
724                msg = 'WARNING: Start time as specified in domain (%s)'\
725                      %domain.starttime
726                msg += ' is earlier than the starttime of file %s: %s.'\
727                         %(self.filename, self.starttime)
728                msg += 'Modifying starttime accordingly.'
729                if self.starttime > domain.starttime:
730                    #FIXME: Print depending on some verbosity setting
731                    #print msg
732                    domain.starttime = self.starttime #Modifying model time
733
734        if mode == 2:       
735            self.read_times() #Now read all times in.
736        else:
737            raise 'x,y dependency not yet implemented'
738
739       
740    def read_times(self):
741        """Read time and values
742        """
743        from Numeric import zeros, Float, alltrue
744        from config import time_format
745        import time, calendar
746       
747        fid = open(self.filename)       
748        lines = fid.readlines()
749        fid.close()
750       
751        N = len(lines)
752        d = self.number_of_values
753
754        T = zeros(N, Float)       #Time
755        Q = zeros((N, d), Float)  #Values
756       
757        for i, line in enumerate(lines):
758            fields = line.split(',')
759            realtime = calendar.timegm(time.strptime(fields[0], time_format))
760
761            T[i] = realtime - self.starttime
762
763            for j, value in enumerate(fields[1].split()):
764                Q[i, j] = float(value)
765
766        msg = 'File %s must list time as a monotonuosly ' %self.filename
767        msg += 'increasing sequence'
768        assert alltrue( T[1:] - T[:-1] > 0 ), msg
769
770        self.T = T     #Time
771        self.Q = Q     #Values
772        self.index = 0 #Initial index
773
774       
775    def __repr__(self):
776        return 'File function'
777
778       
779
780    def __call__(self, t, x=None, y=None):
781        """Evaluate f(t,x,y)
782
783        FIXME: x, y dependency not yet implemented except that
784        result is a vector of same length as x and y
785        FIXME: Naaaa
786        """
787
788        from math import pi, cos, sin, sqrt
789       
790
791        #Find time tau such that
792        #
793        # domain.starttime + t == self.starttime + tau
794       
795        if self.domain is not None:
796            tau = self.domain.starttime-self.starttime+t
797        else:
798            tau = t
799       
800               
801        msg = 'Time interval derived from file %s (%s:%s) does not match model time: %s'\
802              %(self.filename, self.T[0], self.T[1], tau)
803        if tau < self.T[0]: raise msg
804        if tau > self.T[-1]: raise msg       
805
806        oldindex = self.index
807
808        #Find slot
809        while tau > self.T[self.index]: self.index += 1
810        while tau < self.T[self.index]: self.index -= 1
811
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        #Compute interpolated values
817        q = self.Q[self.index,:] +\
818            ratio*(self.Q[self.index+1,:] - self.Q[self.index,:]) 
819
820        #Return vector of interpolated values
821        if x == None and y == None:
822            return q
823        else:
824            try:
825                N = len(x)
826            except:
827                return q
828            else:
829                from Numeric import ones, Float
830                #x is a vector - Create one constant column for each value
831                N = len(x)
832                assert len(y) == N, 'x and y must have same length'
833             
834                res = []
835                for col in q:
836                    res.append(col*ones(N, Float))
837           
838                return res
839           
840
841def read_xya(filename, format = 'netcdf'):
842    """Read simple xya file, possibly with a header in the first line, just
843    x y [attributes]
844    separated by whitespace
845
846    Format can be either ASCII or NetCDF
847   
848    Return list of points, list of attributes and
849    attribute names if present otherwise None
850    """
851    #FIXME: Probably obsoleted by similar function in load_ASCII
852   
853    from Scientific.IO.NetCDF import NetCDFFile
854
855    if format.lower() == 'netcdf': 
856        #Get NetCDF
857       
858        fid = NetCDFFile(filename, 'r') 
859     
860        # Get the variables
861        points = fid.variables['points']
862        keys = fid.variables.keys()
863        attributes = {}
864        for key in keys:
865            attributes[key] = fid.variables[key]
866        #Don't close - arrays are needed outside this function,
867        #alternatively take a copy here
868    else:   
869        #Read as ASCII file assuming that it is separated by whitespace
870        fid = open(filename)
871        lines = fid.readlines()
872        fid.close()
873
874        #Check if there is a header line
875        fields = lines[0].strip().split()
876        try:
877            float(fields[0])
878        except:
879            #This must be a header line
880            attribute_names = fields
881            lines = lines[1:]
882        else:
883            attribute_names = ['elevation']  #HACK
884
885        attributes = {}   
886        for key in attribute_names:
887            attributes[key] = []
888
889        points = []
890        for line in lines:
891            fields = line.strip().split()
892            points.append( (float(fields[0]), float(fields[1])) )
893            for i, key in enumerate(attribute_names):
894                attributes[key].append( float(fields[2+i]) )
895       
896    return points, attributes
897   
898
899#####################################
900#POLYGON STUFF
901#
902#FIXME: ALl these should be put into new module polygon.py
903
904def inside_polygon(point, polygon, closed = True, verbose = False):
905    """Determine whether points are inside or outside a polygon
906   
907    Input:
908       point - Tuple of (x, y) coordinates, or list of tuples
909       polygon - list of vertices of polygon
910       closed - (optional) determine whether points on boundary should be
911       regarded as belonging to the polygon (closed = True)
912       or not (closed = False)
913   
914    Output:
915       If one point is considered, True or False is returned.
916       If multiple points are passed in, the function returns indices
917       of those points that fall inside the polygon     
918       
919    Examples:
920       U = [[0,0], [1,0], [1,1], [0,1]] #Unit square
921       inside_polygon( [0.5, 0.5], U)
922       will evaluate to True as the point 0.5, 0.5 is inside the unit square
923       
924       inside_polygon( [[0.5, 0.5], [1, -0.5], [0.3, 0.2]], U)
925       will return the indices [0, 2] as only the first and the last point
926       is inside the unit square
927       
928    Remarks:
929       The vertices may be listed clockwise or counterclockwise and
930       the first point may optionally be repeated.   
931       Polygons do not need to be convex.
932       Polygons can have holes in them and points inside a hole is
933       regarded as being outside the polygon.         
934               
935       
936    Algorithm is based on work by Darel Finley,
937    http://www.alienryderflex.com/polygon/
938   
939    """   
940
941    from Numeric import array, Float, reshape
942   
943   
944    #Input checks
945    try:
946        point = array(point).astype(Float)         
947    except:
948        msg = 'Point %s could not be converted to Numeric array' %point
949        raise msg       
950       
951    try:
952        polygon = array(polygon).astype(Float)             
953    except:
954        msg = 'Polygon %s could not be converted to Numeric array' %polygon
955        raise msg               
956   
957    assert len(polygon.shape) == 2,\
958       'Polygon array must be a 2d array of vertices: %s' %polygon
959
960       
961    assert polygon.shape[1] == 2,\
962       'Polygon array must have two columns: %s' %polygon   
963
964
965    if len(point.shape) == 1:
966        one_point = True
967        points = reshape(point, (1,2))
968    else:       
969        one_point = False
970        points = point
971
972    N = polygon.shape[0] #Number of vertices in polygon
973    M = points.shape[0]  #Number of points
974   
975    px = polygon[:,0]
976    py = polygon[:,1]   
977
978    #Used for an optimisation when points are far away from polygon
979    minpx = min(px); maxpx = max(px)
980    minpy = min(py); maxpy = max(py)   
981
982
983    #Begin main loop (FIXME: It'd be crunchy to have this written in C:-)
984    indices = []
985    for k in range(M):
986
987        if verbose:
988            if k %((M+10)/10)==0: print 'Doing %d of %d' %(k, M)
989       
990        #for each point   
991        x = points[k, 0]
992        y = points[k, 1]       
993
994        inside = False
995
996        #Optimisation
997        if x > maxpx or x < minpx: continue
998        if y > maxpy or y < minpy: continue       
999
1000        #Check polygon
1001        for i in range(N):
1002            j = (i+1)%N
1003           
1004            #Check for case where point is contained in line segment   
1005            if point_on_line(x, y, px[i], py[i], px[j], py[j]):
1006                if closed:
1007                    inside = True
1008                else:       
1009                    inside = False
1010                break
1011            else:       
1012                #Check if truly inside polygon             
1013                if py[i] < y and py[j] >= y or\
1014                   py[j] < y and py[i] >= y:
1015                    if px[i] + (y-py[i])/(py[j]-py[i])*(px[j]-px[i]) < x:
1016                        inside = not inside
1017                   
1018        if inside: indices.append(k)
1019
1020    if one_point: 
1021        return inside
1022    else:
1023        return indices
1024             
1025
1026
1027def inside_polygon_c(point, polygon, closed = True, verbose = False):
1028    """Determine whether points are inside or outside a polygon
1029
1030    C-wrapper
1031    """   
1032
1033    from Numeric import array, Float, reshape, zeros, Int
1034   
1035   
1036    #Input checks
1037    try:
1038        point = array(point).astype(Float)         
1039    except:
1040        msg = 'Point %s could not be converted to Numeric array' %point
1041        raise msg       
1042       
1043    try:
1044        polygon = array(polygon).astype(Float)             
1045    except:
1046        msg = 'Polygon %s could not be converted to Numeric array' %polygon
1047        raise msg               
1048   
1049    assert len(polygon.shape) == 2,\
1050       'Polygon array must be a 2d array of vertices: %s' %polygon
1051
1052       
1053    assert polygon.shape[1] == 2,\
1054       'Polygon array must have two columns: %s' %polygon   
1055
1056
1057    if len(point.shape) == 1:
1058        one_point = True
1059        points = reshape(point, (1,2))
1060    else:       
1061        one_point = False
1062        points = point
1063
1064    from util_ext import inside_polygon
1065
1066    indices = zeros( points.shape[0], Int )
1067
1068    count = inside_polygon(points, polygon, indices,
1069                           int(closed), int(verbose))
1070
1071    #print 'O', point, count
1072
1073    if one_point:
1074        return count == 1
1075    else:
1076        return indices[:count]
1077
1078
1079#def remove_from(A, B):
1080#    """Assume that A
1081#    """
1082#    from Numeric import search_sorted##
1083#
1084#    ind = search_sorted(A, B)
1085
1086   
1087
1088def outside_polygon(point, polygon, closed = True, verbose = False):
1089    """Determine whether points are outside a polygon
1090   
1091    Input:
1092       point - Tuple of (x, y) coordinates, or list of tuples
1093       polygon - list of vertices of polygon
1094       closed - (optional) determine whether points on boundary should be
1095       regarded as belonging to the polygon (closed = True)
1096       or not (closed = False)
1097   
1098    Output:
1099       If one point is considered, True or False is returned.
1100       If multiple points are passed in, the function returns indices
1101       of those points that fall outside the polygon     
1102       
1103    Examples:
1104       U = [[0,0], [1,0], [1,1], [0,1]] #Unit square   
1105       outside_polygon( [0.5, 0.5], U )
1106       will evaluate to False as the point 0.5, 0.5 is inside the unit square
1107
1108       ouside_polygon( [1.5, 0.5], U )
1109       will evaluate to True as the point 1.5, 0.5 is outside the unit square
1110       
1111       outside_polygon( [[0.5, 0.5], [1, -0.5], [0.3, 0.2]], U )
1112       will return the indices [1] as only the first and the last point
1113       is inside the unit square
1114    """
1115
1116    res  = inside_polygon(point, polygon, closed, verbose)
1117
1118    if res is True or res is False:
1119        return not res
1120
1121    #Now invert indices
1122    from Numeric import arrayrange, compress
1123    outside_indices = arrayrange(len(point))
1124    for i in res:
1125        outside_indices = compress(outside_indices != i, outside_indices)
1126    return outside_indices
1127
1128
1129
1130class Polygon_function:
1131    """Create callable object f: x,y -> z, where a,y,z are vectors and
1132    where f will return different values depending on whether x,y belongs
1133    to specified polygons.
1134   
1135    To instantiate:
1136     
1137       Polygon_function(polygons)
1138       
1139    where polygons is a list of tuples of the form
1140   
1141      [ (P0, v0), (P1, v1), ...]
1142     
1143      with Pi being lists of vertices defining polygons and vi either
1144      constants or functions of x,y to be applied to points with the polygon.
1145     
1146    The function takes an optional argument, default which is the value
1147    (or function) to used for points not belonging to any polygon.
1148    For example:
1149   
1150       Polygon_function(polygons, default = 0.03)       
1151     
1152    If omitted the default value will be 0.0 
1153   
1154    Note: If two polygons overlap, the one last in the list takes precedence
1155
1156    """
1157   
1158    def __init__(self, regions, default = 0.0):
1159       
1160        try:
1161            len(regions)
1162        except:
1163            msg = 'Polygon_function takes a list of pairs (polygon, value). Got %s' %polygons           
1164            raise msg
1165
1166
1167        T = regions[0]                                   
1168        try:
1169            a = len(T)
1170        except:   
1171            msg = 'Polygon_function takes a list of pairs (polygon, value). Got %s' %polygons           
1172            raise msg   
1173                   
1174        assert a == 2, 'Must have two component each: %s' %T       
1175
1176        self.regions = regions             
1177        self.default = default
1178         
1179         
1180    def __call__(self, x, y):
1181        from util import inside_polygon
1182        from Numeric import ones, Float, concatenate, array, reshape, choose
1183       
1184        x = array(x).astype(Float)
1185        y = array(y).astype(Float)     
1186       
1187        N = len(x)
1188        assert len(y) == N
1189       
1190        points = concatenate( (reshape(x, (N, 1)), 
1191                               reshape(y, (N, 1))), axis=1 )
1192
1193        if callable(self.default):
1194            z = self.default(x,y)
1195        else:                   
1196            z = ones(N, Float) * self.default
1197           
1198        for polygon, value in self.regions:
1199            indices = inside_polygon(points, polygon)
1200           
1201            #FIXME: This needs to be vectorised
1202            if callable(value):
1203                for i in indices:
1204                    xx = array([x[i]])
1205                    yy = array([y[i]])
1206                    z[i] = value(xx, yy)[0]
1207            else:   
1208                for i in indices:
1209                    z[i] = value
1210
1211        return z                 
1212
1213def read_polygon(filename):
1214    """Read points assumed to form a polygon
1215       There must be exactly two numbers in each line
1216    """             
1217   
1218    #Get polygon
1219    fid = open(filename)
1220    lines = fid.readlines()
1221    fid.close()
1222    polygon = []
1223    for line in lines:
1224        fields = line.split(',')
1225        polygon.append( [float(fields[0]), float(fields[1])] )
1226
1227    return polygon     
1228   
1229def populate_polygon(polygon, number_of_points, seed = None):
1230    """Populate given polygon with uniformly distributed points.
1231
1232    Input:
1233       polygon - list of vertices of polygon
1234       number_of_points - (optional) number of points
1235       seed - seed for random number generator (default=None)
1236       
1237    Output:
1238       points - list of points inside polygon
1239       
1240    Examples:
1241       populate_polygon( [[0,0], [1,0], [1,1], [0,1]], 5 )
1242       will return five randomly selected points inside the unit square
1243    """
1244
1245    from random import uniform, seed
1246
1247    seed(seed)
1248
1249    points = []
1250
1251    #Find outer extent of polygon
1252    max_x = min_x = polygon[0][0]
1253    max_y = min_y = polygon[0][1]
1254    for point in polygon[1:]:
1255        x = point[0] 
1256        if x > max_x: max_x = x
1257        if x < min_x: min_x = x           
1258        y = point[1] 
1259        if y > max_y: max_y = y
1260        if y < min_y: min_y = y           
1261
1262
1263    while len(points) < number_of_points:     
1264        x = uniform(min_x, max_x)
1265        y = uniform(min_y, max_y)       
1266
1267        if inside_polygon( [x,y], polygon ):
1268            points.append([x,y])
1269
1270    return points
1271
1272####################################################################
1273#Python versions of function that are also implemented in util_gateway.c
1274#
1275
1276def gradient_python(x0, y0, x1, y1, x2, y2, q0, q1, q2):
1277    """
1278    """
1279   
1280    det = (y2-y0)*(x1-x0) - (y1-y0)*(x2-x0)           
1281    a = (y2-y0)*(q1-q0) - (y1-y0)*(q2-q0)
1282    a /= det
1283
1284    b = (x1-x0)*(q2-q0) - (x2-x0)*(q1-q0)
1285    b /= det           
1286
1287    return a, b
1288
1289
1290
1291##############################################
1292#Initialise module
1293
1294import compile
1295if compile.can_use_C_extension('util_ext.c'):
1296    from util_ext import gradient, point_on_line
1297    inside_polygon = inside_polygon_c
1298else:
1299    gradient = gradient_python
1300
1301
1302if __name__ == "__main__":
1303    pass
1304
Note: See TracBrowser for help on using the repository browser.