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

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

Implemented xya2rectangular and tested it

File size: 15.7 KB
Line 
1
2
3def angle(v):
4    """Compute angle between e1 (the unit vector in the x-direction)
5    and the specified vector
6    """
7
8    from math import acos, pi, sqrt
9    from Numeric import sum, array
10
11    l = sqrt( sum (array(v)**2))
12    v1 = v[0]/l
13    v2 = v[1]/l
14       
15    theta = acos(v1)
16
17    if v2 < 0:
18        #Quadrant 3 or 4
19        theta = 2*pi-theta
20
21    return theta
22
23
24def anglediff(v0, v1):
25    """Compute difference between angle of vector x0, y0 and x1, y1.
26    This is used for determining the ordering of vertices,
27    e.g. for checking if they are counter clockwise.
28   
29    Always return a positive value
30    """
31       
32    from math import pi
33   
34    a0 = angle(v0)
35    a1 = angle(v1)
36
37    #Ensure that difference will be positive
38    if a0 < a1:
39        a0 += 2*pi
40           
41    return a0-a1
42
43
44def mean(x):
45    from Numeric import sum
46    return sum(x)/len(x)
47
48
49
50#def point_on_line(point, line):
51def point_on_line(x, y, x0, y0, x1, y1): 
52    """Determine whether a point is on a line segment
53   
54    Input: x, y, x0, x0, x1, y1: where
55        point is given by x, y
56        line is given by (x0, y0) and (x1, y1)
57       
58    """
59   
60    #FIXME: Could do with some C-optimisation         
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             
83class File_function:
84    """Read time series from file and return a callable object:
85    f(t,x,y)
86    which will return interpolated values based on the input file.
87
88    The file format is assumed to be either two fields separated by a comma:
89
90        time [DD/MM/YY hh:mm:ss], value0 value1 value2 ...
91
92    or four comma separated fields
93
94        time [DD/MM/YY hh:mm:ss], x, y, value0 value1 value2 ...
95
96    In either case, the callable object will return a tuple of
97    interpolated values, one each value stated in the file.
98
99   
100    E.g
101
102      31/08/04 00:00:00, 1.328223 0 0
103      31/08/04 00:15:00, 1.292912 0 0
104
105    will provide a time dependent function f(t,x=None,y=None),
106    ignoring x and y, which returns three values per call.
107
108   
109    NOTE: At this stage the function is assumed to depend on
110    time only, i.e  no spatial dependency!!!!!
111    When that is need we can use the least_squares interpolation.
112    """
113
114   
115    def __init__(self, filename, domain=None):
116        """Initialise callable object from a file.
117        See docstring for class File_function
118
119        If domain is specified, model time (domain,starttime)
120        will be checked and possibly modified
121
122        ALl times are assumed to be in UTC
123        """
124
125        import time, calendar
126        from Numeric import array
127        from config import time_format
128
129        assert type(filename) == type(''),\
130               'First argument to File_function must be a string'
131
132
133        try:
134            fid = open(filename)
135        except Exception, e:
136            msg = 'File "%s" could not be opened: Error="%s"'\
137                  %(filename, e)
138            raise msg
139
140
141        line = fid.readline()
142        fid.close()
143        fields = line.split(',')
144        msg = 'File %s must have the format date, value0 value1 value2 ...'
145        msg += ' or date, x, y, value0 value1 value2 ...'
146        mode = len(fields)
147        assert mode in [2,4], msg
148
149        try:
150            starttime = calendar.timegm(time.strptime(fields[0], time_format))
151        except ValueError:   
152            msg = 'First field in file %s must be' %filename
153            msg += ' date-time with format %s.\n' %time_format
154            msg += 'I got %s instead.' %fields[0] 
155            raise msg
156
157
158        #Split values
159        values = []
160        for value in fields[mode-1].split():
161            values.append(float(value))
162
163        q = array(values)
164       
165        msg = 'ERROR: File must contain at least one independent value'
166        assert len(q.shape) == 1, msg
167           
168        self.number_of_values = len(q)
169
170        self.filename = filename
171        self.starttime = starttime
172        self.domain = domain
173
174        if domain is not None:
175            if domain.starttime is None:
176                domain.starttime = self.starttime
177            else:
178                msg = 'WARNING: Start time as specified in domain (%s)'\
179                      %domain.starttime
180                msg += ' is earlier than the starttime of file %s: %s.'\
181                         %(self.filename, self.starttime)
182                msg += 'Modifying starttime accordingly.'
183                if self.starttime > domain.starttime:
184                    #FIXME: Print depending on some verbosity setting
185                    #print msg
186                    domain.starttime = self.starttime #Modifying model time
187
188        if mode == 2:       
189            self.read_times() #Now read all times in.
190        else:
191            raise 'x,y dependecy not yet implemented'
192
193       
194    def read_times(self):
195        """Read time and values
196        """
197        from Numeric import zeros, Float, alltrue
198        from config import time_format
199        import time, calendar
200       
201        fid = open(self.filename)       
202        lines = fid.readlines()
203        fid.close()
204       
205        N = len(lines)
206        d = self.number_of_values
207
208        T = zeros(N, Float)       #Time
209        Q = zeros((N, d), Float)  #Values
210       
211        for i, line in enumerate(lines):
212            fields = line.split(',')
213            realtime = calendar.timegm(time.strptime(fields[0], time_format))
214
215            T[i] = realtime - self.starttime
216
217            for j, value in enumerate(fields[1].split()):
218                Q[i, j] = float(value)
219
220        msg = 'File %s must list time as a monotonuosly ' %self.filename
221        msg += 'increasing sequence'
222        assert alltrue( T[1:] - T[:-1] > 0 ), msg
223
224        self.T = T     #Time
225        self.Q = Q     #Values
226        self.index = 0 #Initial index
227
228       
229    def __repr__(self):
230        return 'File function'
231
232       
233
234    def __call__(self, t, x=None, y=None):
235        """Evaluate f(t,x,y)
236
237        FIXME: x, y dependency not yet implemented except that
238        result is a vector of same length as x and y
239        """
240
241        from math import pi, cos, sin, sqrt
242       
243
244        #Find time tau such that
245        #
246        # domain.starttime + t == self.starttime + tau
247       
248        if self.domain is not None:
249            tau = self.domain.starttime-self.starttime+t
250        else:
251            tau = t
252       
253               
254        msg = 'Time interval derived from file %s (%s:%s) does not match model time: %s'\
255              %(self.filename, self.T[0], self.T[1], tau)
256        if tau < self.T[0]: raise msg
257        if tau > self.T[-1]: raise msg       
258
259        oldindex = self.index
260
261        #Find slot
262        while tau > self.T[self.index]: self.index += 1
263        while tau < self.T[self.index]: self.index -= 1
264
265        #t is now between index and index+1
266        ratio = (tau - self.T[self.index])/\
267                (self.T[self.index+1] - self.T[self.index])
268
269        #Compute interpolated values
270        q = self.Q[self.index,:] +\
271            ratio*(self.Q[self.index+1,:] - self.Q[self.index,:]) 
272
273        #Return vector of interpolated values
274        if x == None and y == None:
275            return q
276        else:
277            from Numeric import ones, Float
278            #Create one constant column for each value
279            N = len(x)
280            assert len(y) == N, 'x and y must have same length'
281
282            res = []
283            for col in q:
284                res.append(col*ones(N, Float))
285           
286            return res
287
288def read_xya(file_name):
289    """Read simple xya file, no header, just
290    x y [attributes]
291    separated by whitespace
292   
293    Return list of points and list of attributes
294    """
295   
296    fid = open(file_name)
297    lines = fid.readlines()
298   
299    points = [] 
300    attributes = []
301    for line in lines:
302        fields = line.strip().split()
303       
304        points.append( (float(fields[0]),  float(fields[1])) )
305        attributes.append( [float(z) for z in fields[2:] ] )
306       
307    return points, attributes
308   
309
310#####################################
311#POLYFON STUFF
312#
313#FIXME: ALl these should be put into new module polygon.py
314
315def inside_polygon(point, polygon, closed = True):
316    """Determine whether points are inside or outside a polygon
317   
318    Input:
319       point - Tuple of (x, y) coordinates, or list of tuples
320       polygon - list of vertices of polygon
321       closed - (optional) determine whether points on boundary should be
322       regarded as belonging to the polygon (closed = True)
323       or not (closed = False)
324   
325    Output:
326       If one point is considered, True or False is returned.
327       If multiple points are passed in, the function returns indices
328       of those points that fall inside the polygon     
329       
330    Examples:
331       inside_polygon( [0.5, 0.5], [[0,0], [1,0], [1,1], [0,1]] )
332       will evaluate to True as the point 0.5, 0.5 is inside the unit square
333       
334       inside_polygon( [[0.5, 0.5], [1, -0.5], [0.3, 0.2]] )
335       will return the indices [0, 2] as only the first and the last point
336       is inside the unit square
337       
338    Remarks:
339       The vertices may be listed clockwise or counterclockwise and
340       the first point may optionally be repeated.   
341       Polygons do not need to be convex.
342       Polygons can have holes in them and points inside a hole is
343       regarded as being outside the polygon.         
344               
345       
346    Algorithm is based on work by Darel Finley,
347    http://www.alienryderflex.com/polygon/
348   
349    """   
350
351    from Numeric import array, Float, reshape
352   
353   
354    #Input checks
355    try:
356        point = array(point).astype(Float)         
357    except:
358        msg = 'Point %s could not be converted to Numeric array' %point
359        raise msg       
360       
361    try:
362        polygon = array(polygon).astype(Float)             
363    except:
364        msg = 'Polygon %s could not be converted to Numeric array' %polygon
365        raise msg               
366   
367    assert len(polygon.shape) == 2,\
368       'Polygon array must be a 2d array of vertices: %s' %polygon
369
370       
371    assert polygon.shape[1] == 2,\
372       'Polygon array must have two columns: %s' %polygon   
373
374
375    if len(point.shape) == 1:
376        one_point = True
377        points = reshape(point, (1,2))
378    else:       
379        one_point = False
380        points = point
381
382    N = polygon.shape[0] #Number of vertices in polygon
383
384    px = polygon[:,0]
385    py = polygon[:,1]   
386
387   
388    #Begin algorithm
389    indices = []
390    for k in range(points.shape[0]):
391        #for each point   
392        x = points[k, 0]
393        y = points[k, 1]       
394
395        inside = False
396        for i in range(N):
397            j = (i+1)%N
398           
399            #Check for case where point is contained in line segment   
400            ##if point_on_line( (x,y), [ [px[i], py[i]], [px[j], py[j]] ]):
401            if point_on_line(x, y, px[i], py[i], px[j], py[j]):
402                if closed:
403                    inside = True
404                else:       
405                    inside = False
406                break
407            else:       
408                #Check if truly inside polygon             
409                if py[i] < y and py[j] >= y or\
410                   py[j] < y and py[i] >= y:
411                    if px[i] + (y-py[i])/(py[j]-py[i])*(px[j]-px[i]) < x:
412                        inside = not inside
413                   
414        if inside: indices.append(k)
415
416    if one_point: 
417        return inside
418    else:
419        return indices
420             
421
422class Polygon_function:
423    """Create callable object f: x,y -> z, where a,y,z are vectors and
424    where f will return different values depending on whether x,y belongs
425    to specified polygons.
426   
427    To instantiate:
428     
429       Polygon_function(polygons)
430       
431    where polygons is a dictionary of the form
432   
433      {P0: v0, P1: v1, ...}
434     
435      with Pi being lists of vertices defining polygons and vi either
436      constants or functions of x,y to be applied to points with the polygon.
437     
438    The function takes an optional argument, default which is the value
439    (or function) to used for points not belonging to any polygon.
440    For example:
441   
442       Polygon_function(polygons, default = 0.03)       
443     
444    If omitted the default value will be 0.0 
445   
446    Note: If two polygons overlap, the one last in the list takes precedence
447
448    """
449   
450    def __init__(self, regions, default = 0.0):
451       
452        try:
453            len(regions)
454        except:
455            msg = 'Polygon_function takes a list of pairs (polygon, value). Got %s' %polygons           
456            raise msg
457
458
459        T = regions[0]                                   
460        try:
461            a = len(T)
462        except:   
463            msg = 'Polygon_function takes a list of pairs (polygon, value). Got %s' %polygons           
464            raise msg   
465                   
466        assert a == 2, 'Must have two component each: %s' %T       
467
468        self.regions = regions             
469        self.default = default
470         
471         
472    def __call__(self, x, y):
473        from util import inside_polygon
474        from Numeric import ones, Float, concatenate, array, reshape, choose
475       
476        x = array(x).astype(Float)
477        y = array(y).astype(Float)     
478       
479        N = len(x)
480        assert len(y) == N
481       
482        points = concatenate( (reshape(x, (N, 1)), 
483                               reshape(y, (N, 1))), axis=1 )
484
485        if callable(self.default):
486            z = self.default(x,y)
487        else:                   
488            z = ones(N, Float) * self.default
489           
490        for polygon, value in self.regions:
491            indices = inside_polygon(points, polygon)
492           
493            #FIXME: This needs to be vectorised
494            if callable(value):
495                for i in indices:
496                    xx = array([x[i]])
497                    yy = array([y[i]])
498                    z[i] = value(xx, yy)[0]
499            else:   
500                for i in indices:
501                    z[i] = value
502
503        return z                 
504
505def read_polygon(filename):
506    """Read points assumed to form a polygon
507       There must be exactly two numbers in each line
508    """             
509   
510    #Get polygon
511    fid = open(filename)
512    lines = fid.readlines()
513    fid.close()
514    polygon = []
515    for line in lines:
516        fields = line.split(',')
517        polygon.append( [float(fields[0]), float(fields[1])] )
518
519    return polygon     
520   
521def populate_polygon(polygon, number_of_points, seed = None):
522    """Populate given polygon with uniformly distributed points.
523
524    Input:
525       polygon - list of vertices of polygon
526       number_of_points - (optional) number of points
527       seed - seed for random number generator (default=None)
528       
529    Output:
530       points - list of points inside polygon
531       
532    Examples:
533       populate_polygon( [[0,0], [1,0], [1,1], [0,1]], 5 )
534       will return five randomly sleected points inside the unit square
535    """
536
537    from random import uniform, seed
538
539    seed(seed)
540
541    points = []
542
543    #Find outer extent of polygon
544    max_x = min_x = polygon[0][0]
545    max_y = min_y = polygon[0][1]
546    for point in polygon[1:]:
547        x = point[0] 
548        if x > max_x: max_x = x
549        if x < min_x: min_x = x           
550        y = point[1] 
551        if y > max_y: max_y = y
552        if y < min_y: min_y = y           
553
554
555    while len(points) < number_of_points:     
556        x = uniform(min_x, max_x)
557        y = uniform(min_y, max_y)       
558
559        if inside_polygon( [x,y], polygon ):
560            points.append([x,y])
561
562    return points
563
564####################################################################
565#Python versions of function that are also implemented in util_gateway.c
566#
567
568def gradient_python(x0, y0, x1, y1, x2, y2, q0, q1, q2):
569    """
570    """
571   
572    det = (y2-y0)*(x1-x0) - (y1-y0)*(x2-x0)           
573    a = (y2-y0)*(q1-q0) - (y1-y0)*(q2-q0)
574    a /= det
575
576    b = (x1-x0)*(q2-q0) - (x2-x0)*(q1-q0)
577    b /= det           
578
579    return a, b
580
581
582
583
584
585##############################################
586#Initialise module
587
588import compile
589if compile.can_use_C_extension('util_ext.c'):
590    from util_ext import gradient, point_on_line
591else:
592    gradient = gradient_python
593
594
595if __name__ == "__main__":
596    pass
597
Note: See TracBrowser for help on using the repository browser.