source: trunk/anuga_core/anuga/structures/internal_boundary_functions.py @ 9657

Last change on this file since 9657 was 9657, checked in by davies, 10 years ago

Allowing limited facility to enforce constant inlet elevations through structures (to make well balanced)

File size: 16.9 KB
Line 
1import numpy
2from matplotlib import pyplot
3import scipy
4from scipy.interpolate import interp1d
5
6
7class 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
356def 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
391class 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
Note: See TracBrowser for help on using the repository browser.