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

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

Optimised inside_polygon in C and tested

File size: 36.9 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:'
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       inside_polygon( [0.5, 0.5], [[0,0], [1,0], [1,1], [0,1]] )
921       will evaluate to True as the point 0.5, 0.5 is inside the unit square
922       
923       inside_polygon( [[0.5, 0.5], [1, -0.5], [0.3, 0.2]] )
924       will return the indices [0, 2] as only the first and the last point
925       is inside the unit square
926       
927    Remarks:
928       The vertices may be listed clockwise or counterclockwise and
929       the first point may optionally be repeated.   
930       Polygons do not need to be convex.
931       Polygons can have holes in them and points inside a hole is
932       regarded as being outside the polygon.         
933               
934       
935    Algorithm is based on work by Darel Finley,
936    http://www.alienryderflex.com/polygon/
937   
938    """   
939
940    from Numeric import array, Float, reshape
941   
942   
943    #Input checks
944    try:
945        point = array(point).astype(Float)         
946    except:
947        msg = 'Point %s could not be converted to Numeric array' %point
948        raise msg       
949       
950    try:
951        polygon = array(polygon).astype(Float)             
952    except:
953        msg = 'Polygon %s could not be converted to Numeric array' %polygon
954        raise msg               
955   
956    assert len(polygon.shape) == 2,\
957       'Polygon array must be a 2d array of vertices: %s' %polygon
958
959       
960    assert polygon.shape[1] == 2,\
961       'Polygon array must have two columns: %s' %polygon   
962
963
964    if len(point.shape) == 1:
965        one_point = True
966        points = reshape(point, (1,2))
967    else:       
968        one_point = False
969        points = point
970
971    N = polygon.shape[0] #Number of vertices in polygon
972    M = points.shape[0]  #Number of points
973   
974    px = polygon[:,0]
975    py = polygon[:,1]   
976
977    #Used for an optimisation when points are far away from polygon
978    minpx = min(px); maxpx = max(px)
979    minpy = min(py); maxpy = max(py)   
980
981
982    #Begin main loop (FIXME: It'd be crunchy to have this written in C:-)
983    indices = []
984    for k in range(M):
985
986        if verbose:
987            if k %((M+10)/10)==0: print 'Doing %d of %d' %(k, M)
988       
989        #for each point   
990        x = points[k, 0]
991        y = points[k, 1]       
992
993        inside = False
994
995        #Optimisation
996        if x > maxpx or x < minpx: continue
997        if y > maxpy or y < minpy: continue       
998
999        #Check polygon
1000        for i in range(N):
1001            j = (i+1)%N
1002           
1003            #Check for case where point is contained in line segment   
1004            if point_on_line(x, y, px[i], py[i], px[j], py[j]):
1005                if closed:
1006                    inside = True
1007                else:       
1008                    inside = False
1009                break
1010            else:       
1011                #Check if truly inside polygon             
1012                if py[i] < y and py[j] >= y or\
1013                   py[j] < y and py[i] >= y:
1014                    if px[i] + (y-py[i])/(py[j]-py[i])*(px[j]-px[i]) < x:
1015                        inside = not inside
1016                   
1017        if inside: indices.append(k)
1018
1019    if one_point: 
1020        return inside
1021    else:
1022        return indices
1023             
1024
1025
1026def inside_polygon_c(point, polygon, closed = True, verbose = False):
1027    """Determine whether points are inside or outside a polygon
1028
1029    C-wrapper
1030    """   
1031
1032    from Numeric import array, Float, reshape, zeros, Int
1033   
1034   
1035    #Input checks
1036    try:
1037        point = array(point).astype(Float)         
1038    except:
1039        msg = 'Point %s could not be converted to Numeric array' %point
1040        raise msg       
1041       
1042    try:
1043        polygon = array(polygon).astype(Float)             
1044    except:
1045        msg = 'Polygon %s could not be converted to Numeric array' %polygon
1046        raise msg               
1047   
1048    assert len(polygon.shape) == 2,\
1049       'Polygon array must be a 2d array of vertices: %s' %polygon
1050
1051       
1052    assert polygon.shape[1] == 2,\
1053       'Polygon array must have two columns: %s' %polygon   
1054
1055
1056    if len(point.shape) == 1:
1057        one_point = True
1058        points = reshape(point, (1,2))
1059    else:       
1060        one_point = False
1061        points = point
1062
1063    from util_ext import inside_polygon
1064
1065    indices = zeros( points.shape[0], Int )
1066
1067    count = inside_polygon(points, polygon, indices,
1068                           int(closed), int(verbose))
1069
1070    #print 'O', point, count
1071
1072    if one_point:
1073        return count == 1
1074    else:
1075        return indices[:count]
1076
1077class Polygon_function:
1078    """Create callable object f: x,y -> z, where a,y,z are vectors and
1079    where f will return different values depending on whether x,y belongs
1080    to specified polygons.
1081   
1082    To instantiate:
1083     
1084       Polygon_function(polygons)
1085       
1086    where polygons is a list of tuples of the form
1087   
1088      [ (P0, v0), (P1, v1), ...]
1089     
1090      with Pi being lists of vertices defining polygons and vi either
1091      constants or functions of x,y to be applied to points with the polygon.
1092     
1093    The function takes an optional argument, default which is the value
1094    (or function) to used for points not belonging to any polygon.
1095    For example:
1096   
1097       Polygon_function(polygons, default = 0.03)       
1098     
1099    If omitted the default value will be 0.0 
1100   
1101    Note: If two polygons overlap, the one last in the list takes precedence
1102
1103    """
1104   
1105    def __init__(self, regions, default = 0.0):
1106       
1107        try:
1108            len(regions)
1109        except:
1110            msg = 'Polygon_function takes a list of pairs (polygon, value). Got %s' %polygons           
1111            raise msg
1112
1113
1114        T = regions[0]                                   
1115        try:
1116            a = len(T)
1117        except:   
1118            msg = 'Polygon_function takes a list of pairs (polygon, value). Got %s' %polygons           
1119            raise msg   
1120                   
1121        assert a == 2, 'Must have two component each: %s' %T       
1122
1123        self.regions = regions             
1124        self.default = default
1125         
1126         
1127    def __call__(self, x, y):
1128        from util import inside_polygon
1129        from Numeric import ones, Float, concatenate, array, reshape, choose
1130       
1131        x = array(x).astype(Float)
1132        y = array(y).astype(Float)     
1133       
1134        N = len(x)
1135        assert len(y) == N
1136       
1137        points = concatenate( (reshape(x, (N, 1)), 
1138                               reshape(y, (N, 1))), axis=1 )
1139
1140        if callable(self.default):
1141            z = self.default(x,y)
1142        else:                   
1143            z = ones(N, Float) * self.default
1144           
1145        for polygon, value in self.regions:
1146            indices = inside_polygon(points, polygon)
1147           
1148            #FIXME: This needs to be vectorised
1149            if callable(value):
1150                for i in indices:
1151                    xx = array([x[i]])
1152                    yy = array([y[i]])
1153                    z[i] = value(xx, yy)[0]
1154            else:   
1155                for i in indices:
1156                    z[i] = value
1157
1158        return z                 
1159
1160def read_polygon(filename):
1161    """Read points assumed to form a polygon
1162       There must be exactly two numbers in each line
1163    """             
1164   
1165    #Get polygon
1166    fid = open(filename)
1167    lines = fid.readlines()
1168    fid.close()
1169    polygon = []
1170    for line in lines:
1171        fields = line.split(',')
1172        polygon.append( [float(fields[0]), float(fields[1])] )
1173
1174    return polygon     
1175   
1176def populate_polygon(polygon, number_of_points, seed = None):
1177    """Populate given polygon with uniformly distributed points.
1178
1179    Input:
1180       polygon - list of vertices of polygon
1181       number_of_points - (optional) number of points
1182       seed - seed for random number generator (default=None)
1183       
1184    Output:
1185       points - list of points inside polygon
1186       
1187    Examples:
1188       populate_polygon( [[0,0], [1,0], [1,1], [0,1]], 5 )
1189       will return five randomly selected points inside the unit square
1190    """
1191
1192    from random import uniform, seed
1193
1194    seed(seed)
1195
1196    points = []
1197
1198    #Find outer extent of polygon
1199    max_x = min_x = polygon[0][0]
1200    max_y = min_y = polygon[0][1]
1201    for point in polygon[1:]:
1202        x = point[0] 
1203        if x > max_x: max_x = x
1204        if x < min_x: min_x = x           
1205        y = point[1] 
1206        if y > max_y: max_y = y
1207        if y < min_y: min_y = y           
1208
1209
1210    while len(points) < number_of_points:     
1211        x = uniform(min_x, max_x)
1212        y = uniform(min_y, max_y)       
1213
1214        if inside_polygon( [x,y], polygon ):
1215            points.append([x,y])
1216
1217    return points
1218
1219####################################################################
1220#Python versions of function that are also implemented in util_gateway.c
1221#
1222
1223def gradient_python(x0, y0, x1, y1, x2, y2, q0, q1, q2):
1224    """
1225    """
1226   
1227    det = (y2-y0)*(x1-x0) - (y1-y0)*(x2-x0)           
1228    a = (y2-y0)*(q1-q0) - (y1-y0)*(q2-q0)
1229    a /= det
1230
1231    b = (x1-x0)*(q2-q0) - (x2-x0)*(q1-q0)
1232    b /= det           
1233
1234    return a, b
1235
1236
1237
1238##############################################
1239#Initialise module
1240
1241import compile
1242if compile.can_use_C_extension('util_ext.c'):
1243    from util_ext import gradient, point_on_line
1244    inside_polygon = inside_polygon_c
1245else:
1246    gradient = gradient_python
1247
1248
1249if __name__ == "__main__":
1250    pass
1251
Note: See TracBrowser for help on using the repository browser.