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

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

First experiment with new limiter (near bed)

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