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