1 | import numpy |
---|
2 | from matplotlib import pyplot |
---|
3 | import scipy |
---|
4 | from scipy.interpolate import interp1d |
---|
5 | |
---|
6 | |
---|
7 | class hecras_internal_boundary_function: |
---|
8 | |
---|
9 | """ Read internal boundary curves for a bridge / culvert from hecras and |
---|
10 | convert to a function which can be input as a structure in ANUGA |
---|
11 | |
---|
12 | If using hec-ras for only part of the structure (e.g. under the bridge), |
---|
13 | be careful to get the tables from an adjusted hecras model where only |
---|
14 | flow under the bridge is allowed (and ineffective flow areas etc are |
---|
15 | adjusted to reflect this). Also, the curves will change as manning's n |
---|
16 | is adjusted so you might want to make a number of curves with different |
---|
17 | n values to make calibration easier |
---|
18 | |
---|
19 | Note also that when used in an ANUGA structure, this can still give a |
---|
20 | different result to hecras. |
---|
21 | * One reason is that the water surface elevation in hecras is |
---|
22 | assumed constant over a cross-section, whereas substantial |
---|
23 | cross-channel variations can occur in ANUGA, especialy near a |
---|
24 | bridge opening. The enquiry points used by ANUGA are a single |
---|
25 | location, so won't 'average-out' the cross-sectional variation |
---|
26 | * ANUGA doesn't include side-wall friction |
---|
27 | |
---|
28 | |
---|
29 | """ |
---|
30 | |
---|
31 | def __init__(self, internal_boundary_curves_file, skip_header_rows=4, |
---|
32 | skip_columns=1, allow_sign_reversal=False, verbose=True, |
---|
33 | vertical_datum_offset = 0.): |
---|
34 | """ Use a csv file containing the htab-curves from hecras to create an |
---|
35 | interpolation function for the structure. |
---|
36 | |
---|
37 | The header / initial column probably need to be skipped (hecras |
---|
38 | format) |
---|
39 | |
---|
40 | It is assumed that the first 2 columns are the free-flow curve, |
---|
41 | and the others are Q vs headwater rating curves for various |
---|
42 | tailwater values |
---|
43 | |
---|
44 | @param internal_boundary_curves_file A csv file with the format |
---|
45 | copied from hecras's internal boundary curves table for |
---|
46 | the structure. This is obtained from (in hecras 5.00) the |
---|
47 | menu via View-Hydraulic Property Tables, then click on |
---|
48 | the table tab, copy the table, paste into a spreadsheet |
---|
49 | and then save as csv. |
---|
50 | @param skip_header_rows Number of header rows in |
---|
51 | internal_boundary_curves_file (4 in the examples I have seen) |
---|
52 | @param skip_columns Number of columns to ignore (on the |
---|
53 | left-hand-side of the table), 1 in the examples I have seen |
---|
54 | @param allow_sign_reversal If False, then if the _call_ method is invoked |
---|
55 | with the headwater < tailwater, it produces an exception. |
---|
56 | If True, then in the same situation the code reverses the |
---|
57 | headwater and tailwater, and also reverses the sign of the |
---|
58 | resulting discharge. |
---|
59 | @param verbose True/False more messages |
---|
60 | |
---|
61 | """ |
---|
62 | |
---|
63 | if verbose: |
---|
64 | print '########################################' |
---|
65 | print 'HECRAS INTERNAL BOUNDARY FUNCTION' |
---|
66 | print 'THIS IS EXPERIMENTAL' |
---|
67 | print 'SUBJECT TO CHANGE WITHOUT NOTICE' |
---|
68 | print '########################################' |
---|
69 | |
---|
70 | |
---|
71 | internal_boundary_curves = numpy.genfromtxt( |
---|
72 | internal_boundary_curves_file, delimiter=',', |
---|
73 | skip_header=skip_header_rows) |
---|
74 | |
---|
75 | internal_boundary_curves = internal_boundary_curves[:, skip_columns:] |
---|
76 | |
---|
77 | # Do we use the table for flows from upstream to downstream, and the reverse? |
---|
78 | # If so, then reverse flow has a negative sign |
---|
79 | self.allow_sign_reversal = allow_sign_reversal |
---|
80 | |
---|
81 | # Adjust HW/TW curves by vertical datum offset |
---|
82 | for i in range(1, internal_boundary_curves.shape[1], 2): |
---|
83 | internal_boundary_curves[:,i] = internal_boundary_curves[:,i] +\ |
---|
84 | vertical_datum_offset |
---|
85 | |
---|
86 | # The first 2 columns consist of the free overflow curve (Q, HW). This |
---|
87 | # is apparently used when, for a given tail-water, the head-water is |
---|
88 | # not on the corresponding curve. |
---|
89 | free_flow_data = remove_repeated_curve_points( |
---|
90 | internal_boundary_curves[:, 1], |
---|
91 | internal_boundary_curves[:, 0]) |
---|
92 | self.free_flow_curve = interp1d( |
---|
93 | free_flow_data[:, 0], free_flow_data[:, 1], kind='linear') |
---|
94 | self.free_flow_data = free_flow_data |
---|
95 | |
---|
96 | self.free_flow_hw_range = numpy.array( |
---|
97 | [internal_boundary_curves[:, 1].min(), |
---|
98 | internal_boundary_curves[:, 1].max()]) |
---|
99 | |
---|
100 | # Aside from the first 2 columns, the file consists of column-stacked |
---|
101 | # Q,HW curves defining the discharge Q when TW = HW[0]. Hence Q[0] = 0 |
---|
102 | # always. |
---|
103 | # |
---|
104 | # We store these as a list of interpolation functions for each TW, with |
---|
105 | # other arrays which give the TW, and corresponding maximum HW |
---|
106 | # |
---|
107 | |
---|
108 | ncol_ibc = internal_boundary_curves.shape[1] |
---|
109 | self.nonfree_flow_tw = \ |
---|
110 | internal_boundary_curves[0, range(3, ncol_ibc + 1, 2)] |
---|
111 | |
---|
112 | # Ensure the nonfree_flow_tw is monotonic increasing |
---|
113 | assert numpy.all(self.nonfree_flow_tw[0:-1] < self.nonfree_flow_tw[1:]) |
---|
114 | |
---|
115 | self.nonfree_flow_tw_range = numpy.array( |
---|
116 | [self.nonfree_flow_tw.min(), self.nonfree_flow_tw.max()]) |
---|
117 | |
---|
118 | # Populate in loop |
---|
119 | self.hw_max_given_tw = self.nonfree_flow_tw * numpy.nan |
---|
120 | curve_counter = 0 |
---|
121 | self.nonfree_flow_curves = list() |
---|
122 | self.nonfree_flow_data = list() |
---|
123 | for i in range(2, internal_boundary_curves.shape[1], 2): |
---|
124 | Q = internal_boundary_curves[:, i] |
---|
125 | HW = internal_boundary_curves[:, i + 1] |
---|
126 | TW = HW[0] |
---|
127 | |
---|
128 | assert Q[0] == 0. |
---|
129 | |
---|
130 | HW_max = numpy.nanmax(HW) |
---|
131 | Q_max = numpy.nanmax(Q) |
---|
132 | self.hw_max_given_tw[curve_counter] = HW_max |
---|
133 | |
---|
134 | if(HW_max == HW[0]): |
---|
135 | # Curve with no range in HW |
---|
136 | # Can happen for the extreme curve |
---|
137 | if i == internal_boundary_curves.shape[1] - 2: |
---|
138 | if verbose: |
---|
139 | print 'Skipping final rating curve with no HW range' |
---|
140 | |
---|
141 | self.nonfree_flow_tw = self.nonfree_flow_tw[0:-1] |
---|
142 | self.nonfree_flow_tw_range[1] = self.nonfree_flow_tw[-1] |
---|
143 | self.hw_max_given_tw = self.hw_max_given_tw[0:-1] |
---|
144 | continue |
---|
145 | else: |
---|
146 | print i, internal_boundary_curves.shape[1], HW_max |
---|
147 | raise Exception('Rating curve with no HW range') |
---|
148 | |
---|
149 | curve_data = remove_repeated_curve_points(HW, Q) |
---|
150 | |
---|
151 | self.nonfree_flow_data.append(curve_data) |
---|
152 | |
---|
153 | self.nonfree_flow_curves.append( |
---|
154 | interp1d(curve_data[:, 0], curve_data[:, 1], kind='linear')) |
---|
155 | curve_counter += 1 |
---|
156 | |
---|
157 | self.internal_boundary_curves = internal_boundary_curves |
---|
158 | self.name = internal_boundary_curves_file |
---|
159 | |
---|
160 | return |
---|
161 | |
---|
162 | |
---|
163 | |
---|
164 | def __call__(self, hw_in, tw_in): |
---|
165 | """ |
---|
166 | Compute the discharge from the headwater / tailwater |
---|
167 | |
---|
168 | The basic approach is: |
---|
169 | 1) Identify the 2 HW-Q curves corresponding to TW levels just |
---|
170 | above/below tw. Say TW_l is the lower TW level, and TW_u is |
---|
171 | the upper one |
---|
172 | 2) Evaluate Q_l, Q_u from each of the l and u curves, |
---|
173 | interpolating at "(hw - tw) + TW_l" and "(hw-tw) + TW_u" |
---|
174 | respectively |
---|
175 | - For each curve, if the adjusted 'hw' value exceeds the hw |
---|
176 | range of the curve, then use the free overflow value. |
---|
177 | - NOTE: This means that we may have to evaluate the function |
---|
178 | at values above hw -- which can lead to errors if the |
---|
179 | table does not cover enough hw range. |
---|
180 | 3) Take a weighted average of each Q, with weights |
---|
181 | inversely proportional to (tw - TW_l) and (TW_u-tw) |
---|
182 | |
---|
183 | This weighting method is nice since it preserves steady states. |
---|
184 | Note the upper/lower curves are evaluated with the 'correct head |
---|
185 | loss', rather than the correct hw value (but incorrect tw value). |
---|
186 | This seems a smoother approach. |
---|
187 | |
---|
188 | If self.allow_sign_reversal is True, then if hw < tw, we swap |
---|
189 | hw and tw, and return the resultant Q with sign reversed |
---|
190 | |
---|
191 | @param hw_in the input headwater |
---|
192 | @param tw_in the input tailwater |
---|
193 | |
---|
194 | """ |
---|
195 | |
---|
196 | # Usually hw >= tw. If not, see if we should reverse the sign of Q |
---|
197 | if ((hw_in < tw_in) and self.allow_sign_reversal): |
---|
198 | tw = 1.0*hw_in |
---|
199 | hw = 1.0*tw_in |
---|
200 | sign_multiplier = -1.0 |
---|
201 | else: |
---|
202 | tw = 1.0*tw_in |
---|
203 | hw = 1.0*hw_in |
---|
204 | sign_multiplier = 1.0 |
---|
205 | |
---|
206 | # Logical checks |
---|
207 | |
---|
208 | if hw < tw: |
---|
209 | msg = 'HW: ' + str(hw) + ' < TW: ' + str(tw) + ' in ' + self.name |
---|
210 | raise Exception(msg) |
---|
211 | |
---|
212 | # Quick exit |
---|
213 | if hw < self.free_flow_hw_range[0]: |
---|
214 | return 0.0 |
---|
215 | |
---|
216 | if hw > self.free_flow_hw_range[1]: |
---|
217 | msg = 'HW: ' + str(hw) + ' is outside free_flow_hw_range ' +\ |
---|
218 | str(self.free_flow_hw_range) + ' in ' + self.name |
---|
219 | raise Exception(msg) |
---|
220 | |
---|
221 | if tw > self.nonfree_flow_tw.max(): |
---|
222 | msg = 'TW: ' + str(tw) + ' exceeds nonfree_flow_tw_max ' +\ |
---|
223 | str(self.nonfree_flow_tw.max()) + ' in ' + self.name |
---|
224 | raise Exception(msg) |
---|
225 | |
---|
226 | # Compute discharge |
---|
227 | if tw < self.nonfree_flow_tw[0]: |
---|
228 | # Use free flow curve |
---|
229 | # This could induce a discontinuity in the flow as |
---|
230 | # tw crosses the minimum |
---|
231 | Q = self.free_flow_curve(hw) |
---|
232 | |
---|
233 | else: |
---|
234 | # Try to use nonfree flow curves |
---|
235 | tw_lower_index = (self.nonfree_flow_tw <= tw).sum() - 1 |
---|
236 | max_allowed_tw_index = len(self.nonfree_flow_tw) - 1 |
---|
237 | |
---|
238 | # Interpolate along the 'lower-tw' curve, with hw adjusted |
---|
239 | # to get the head-loss right. This will preserve stationary states |
---|
240 | lower_tw = self.nonfree_flow_tw[tw_lower_index] |
---|
241 | lower_hw = (hw - tw) + lower_tw |
---|
242 | # Interpolation weight |
---|
243 | w0 = tw - lower_tw |
---|
244 | |
---|
245 | if tw_lower_index < max_allowed_tw_index: |
---|
246 | # Get upper curve variables |
---|
247 | tw_upper_index = tw_lower_index + 1 |
---|
248 | upper_tw = self.nonfree_flow_tw[tw_upper_index] |
---|
249 | upper_hw = (hw - tw) + upper_tw |
---|
250 | w1 = upper_tw - tw |
---|
251 | else: |
---|
252 | # tw_lower_index corresponds to the highest curve |
---|
253 | # In that case just use the lower curve |
---|
254 | tw_upper_index = tw_lower_index |
---|
255 | upper_tw = lower_tw |
---|
256 | upper_hw = lower_hw |
---|
257 | w1 = 1. |
---|
258 | w0 = 0. |
---|
259 | |
---|
260 | assert ((w0 >= 0.) & (w1 >= 0.)), str(w0) + ' ' + str(w1) |
---|
261 | |
---|
262 | # Check whether lower_hw or upper_hw exceed the allowed rating |
---|
263 | # curve range |
---|
264 | max_possible_lower_hw = \ |
---|
265 | max(self.hw_max_given_tw[tw_lower_index], |
---|
266 | self.free_flow_hw_range[1]) |
---|
267 | if lower_hw > max_possible_lower_hw: |
---|
268 | msg = 'lower_hw ' + str(lower_hw) + \ |
---|
269 | ' < max_possible_lower_hw: ' +\ |
---|
270 | str(max_possible_lower_hw) + \ |
---|
271 | ' at structure ' + self.name + ' \n'+\ |
---|
272 | ' This may exceed hw: ' + str(hw) +\ |
---|
273 | ' but is required for our interpolation method.' +\ |
---|
274 | ' Fix by extending the range of the rating curves' |
---|
275 | raise Exception(msg) |
---|
276 | |
---|
277 | max_possible_upper_hw = \ |
---|
278 | max(self.hw_max_given_tw[tw_upper_index], |
---|
279 | self.free_flow_hw_range[1]) |
---|
280 | if(upper_hw > max_possible_upper_hw): |
---|
281 | msg = 'upper_hw ' + str(upper_hw) + \ |
---|
282 | ' < max_possible_upper_hw: ' +\ |
---|
283 | str(max_possible_upper_hw) +\ |
---|
284 | ' at structure ' + self.name + ' \n'+\ |
---|
285 | ' This may exceed hw: ' + str(hw) +\ |
---|
286 | ' but is required for our interpolation method.' +\ |
---|
287 | ' Fix by extending the range of the rating curves' |
---|
288 | |
---|
289 | raise Exception(msg) |
---|
290 | |
---|
291 | # Get lower curve value |
---|
292 | # NOTE: This could introduce a (probably small) discontinuity |
---|
293 | # in the interpolation if the tabulated curves do not exactly agree |
---|
294 | # (which seems to be the case for hecras output) |
---|
295 | if lower_hw <= self.hw_max_given_tw[tw_lower_index]: |
---|
296 | lower_curve_Q = self.nonfree_flow_curves[ |
---|
297 | tw_lower_index](lower_hw) |
---|
298 | else: |
---|
299 | # Use free flow curve. The 'real' Q is hw |
---|
300 | lower_curve_Q = self.free_flow_curve(hw) |
---|
301 | |
---|
302 | # Get upper curve value |
---|
303 | if upper_hw < self.hw_max_given_tw[tw_upper_index]: |
---|
304 | upper_curve_Q = self.nonfree_flow_curves[ |
---|
305 | tw_upper_index](upper_hw) |
---|
306 | else: |
---|
307 | # Use free flow curve. |
---|
308 | upper_curve_Q = self.free_flow_curve(hw) |
---|
309 | |
---|
310 | Q = (w0 * upper_curve_Q + w1 * lower_curve_Q) / (w0 + w1) |
---|
311 | |
---|
312 | #print 'Q: ', Q , ' HW: ', hw, ' TW:', tw |
---|
313 | |
---|
314 | return(Q*sign_multiplier) |
---|
315 | |
---|
316 | |
---|
317 | def grid_function(self, interactive_plot=True): |
---|
318 | """ Compute Q for each valid HW / TW combination |
---|
319 | Optionally plot it. |
---|
320 | Return a list with [HW_values, TW_values, Q]. |
---|
321 | |
---|
322 | """ |
---|
323 | |
---|
324 | HW_min = self.free_flow_hw_range[0] |
---|
325 | HW_max = self.free_flow_hw_range[1] |
---|
326 | HW_range = scipy.linspace(HW_min, HW_max, num=101) |
---|
327 | TW_range = scipy.linspace(HW_min, self.nonfree_flow_tw.max(), num=101) |
---|
328 | |
---|
329 | Q_store = numpy.array((101 ** 2) * [0.]) |
---|
330 | Q_store = Q_store.reshape((101, 101)) |
---|
331 | |
---|
332 | for h, hw in enumerate(HW_range): |
---|
333 | for t, tw in enumerate(TW_range): |
---|
334 | if tw > hw: |
---|
335 | continue |
---|
336 | try: |
---|
337 | Q_store[h, t] = self(hw, tw) |
---|
338 | except: |
---|
339 | Q_store[h, t] = numpy.nan |
---|
340 | |
---|
341 | if interactive_plot: |
---|
342 | pyplot.ion() |
---|
343 | pyplot.contourf(HW_range, TW_range, Q_store, 100) |
---|
344 | pyplot.colorbar() |
---|
345 | pyplot.title('Q given headwater and tailwater: ' + self.name) |
---|
346 | pyplot.xlabel('Tailwater (m)') |
---|
347 | pyplot.ylabel('Headwater (m)') |
---|
348 | |
---|
349 | # Be nice to use the grid as a quick lookup function. |
---|
350 | # But it fails when nan is present |
---|
351 | # self.gridded_interpolation_function = scipy.interpolate.RectBivariateSpline(HW_range, TW_range, Q_store) |
---|
352 | |
---|
353 | return [HW_range, TW_range, Q_store] |
---|
354 | |
---|
355 | |
---|
356 | def remove_repeated_curve_points(HW, Q): |
---|
357 | """Utility function to get rid of round-off-induced points with repeated |
---|
358 | HW values from the HW-Q interpolation curves |
---|
359 | |
---|
360 | Return a 2-column array with HW, Q but repeated HW values removed. We |
---|
361 | retain the smallest Q for each HW, except at the highest HW value where the |
---|
362 | largest Q is used. |
---|
363 | |
---|
364 | """ |
---|
365 | |
---|
366 | HW_max = numpy.nanmax(HW) |
---|
367 | Q_max = numpy.nanmax(Q) |
---|
368 | |
---|
369 | points2keep = list() |
---|
370 | for j in range(Q.shape[0]): |
---|
371 | # Check for nan |
---|
372 | if Q[j] != Q[j]: |
---|
373 | continue |
---|
374 | |
---|
375 | # Because of rounding, sometimes a single curve contains |
---|
376 | # multiple HW |
---|
377 | # Exclude curve points which repeat earlier HW |
---|
378 | # Don't to this for the first or last points on the curve |
---|
379 | if ((j > 0) & (HW[j] == HW[j - 1]) & (HW[j] != HW_max)): |
---|
380 | continue |
---|
381 | |
---|
382 | if((HW[j] == HW_max) & (Q[j] < Q_max)): |
---|
383 | continue |
---|
384 | |
---|
385 | points2keep.append([HW[j], Q[j]]) |
---|
386 | |
---|
387 | return numpy.array(points2keep) |
---|
388 | |
---|
389 | |
---|
390 | |
---|
391 | class pumping_station_function: |
---|
392 | """ Transfer water from one site to another at a given rate, |
---|
393 | based on the pump capacities and observed headwater/tailwater. |
---|
394 | |
---|
395 | This class returns a callable object which can compute the rate |
---|
396 | of pumping. This can be passed to an internal_boundary_operator. |
---|
397 | |
---|
398 | The locations of the latter are defined based on the enquiry points |
---|
399 | of the structure. The positive pump direction (positive Q) is from |
---|
400 | headwater to tailwater. |
---|
401 | """ |
---|
402 | |
---|
403 | def __init__(self, pump_capacity, hw_to_start_pumping, verbose=True): |
---|
404 | """ |
---|
405 | @param pump_capacity m^3/s |
---|
406 | |
---|
407 | """ |
---|
408 | self.pump_capacity = pump_capacity |
---|
409 | self.hw_to_start_pumping = hw_to_start_pumping |
---|
410 | |
---|
411 | if verbose: |
---|
412 | print '########################################' |
---|
413 | print 'PUMPING STATION FUNCTION' |
---|
414 | print 'THIS IS EXPERIMENTAL' |
---|
415 | print 'SUBJECT TO CHANGE WITHOUT NOTICE' |
---|
416 | print '########################################' |
---|
417 | |
---|
418 | |
---|
419 | def __call__(self, hw_in, tw_in): |
---|
420 | """ |
---|
421 | @param hw_in The stage (or energy) at the headwater site |
---|
422 | @param tw_in The stage (or energy) at the tailwater site |
---|
423 | """ |
---|
424 | |
---|
425 | if(hw_in > self.hw_to_start_pumping): |
---|
426 | Q = self.pump_capacity |
---|
427 | else: |
---|
428 | Q = 0. |
---|
429 | |
---|
430 | return Q |
---|
431 | |
---|