Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Hydraulische Sprungübung

For interactive reading and executing code blocks Binder, or install Python and JupyterLab locally to run hydraulic-jump.ipynb on your own machine.

Clyde Dam, auf dem Clutha River / Mata-Au in Central Otago, ist Neuseelands größter Beton-Schwerkraftdamm: 490 m breit, bis zu 60 m hoch über dem alten Flussbett und beschlagnahmen Lake Dunstan. Sein gated Überlauf hat vier radiale Tore, jeweils 15 m hoch und 10 m breit, entworfen, um eine Entladung von 3200 m3^3s (0,2% AEP) auf einem Seehöhe von 195.1 m zu führen. Das Wasser, das den Spillway passiert, steigt eine Rutsche auf der Dammseite ab. Der hochenergetische Fluss an der Zehe dieser Rutsche muss in einem stillenden Becken abgeführt werden, damit das Flussbett stromabwärts nicht durchgeschwemmt wird.

Schülerherausforderung: Größe der Beckengeometrie, so dass sich der Hydrauliksprung innerhalb des Beckens bildet und dort gehalten wird, und dadurch das Durchforsten stromabwärts des Damms vermieden wird.

Struktur der Berechnungsvorlage

Übersicht

Wer macht die Arbeit? |----------- | I Der Fluss, der am Becken ankommt: Entladung der Einheit, Energiekopf an der Rutsche und der Tiefe des Schwanzwassers | **gegeben ** - führen Sie die Zellen und lesen Sie die Ergebnisse | | II | Stilling-basin & hydraulisches Sprungdesign | student -- Workflow ist gegeben, hydraulische Sprunggleichungen müssen eingegeben werden |

Einzelheiten

Teil I legt die Größen fest, auf die der Sprung und damit das Becken ausgelegt ist: die Entladung QQ und die Einheitsentladung qq, der Energiekopf H0H_0 oberhalb des unversenkten Beckens invertiert und die Schwanzwassertiefe htwh_{tw} erhalten aus der Manning-Gleichung.

Part II contains five short functions, marked Task 1 to Task 5. Each is a single equation from the lecture. The numerics around them, that is the root finding, the search over the basin depth and the plots, are given, so that an incorrect equation produces an incorrect number rather than a traceback. The equation at the centre of the exercise is the sequent-depth (Bélanger) equation of Task 3; the remaining four establish the state it is applied to and the dimensions that follow from it. On completing Part II you should be able to:

  • die Energie-, Kontinuitäts- und Impulsverhältnisse ausdrücken, die einen hydraulischen Sprung als Code festlegen;

  • Bestimmen, ob ein bestimmter Unterwasserspiegel einen Sprung in einem Becken behält oder stromabwärts streicht;

  • die Tiefe, die Länge und den Scheuerschutz eines Beckens vor diesen Beziehungen zu bestimmen.

Notation

Tiefen werden als hh bezeichnet und Köpfe HH, in der Vorlesung und hier gleichermaßen. Chanson bezeichnet die gleichen Tiefen d1d_1, d2d_2 und dud_u. Das Retentionsverhältnis wird als rtr_{\mathrm{t}} und nicht als ε\varepsilon des deutschen Arbeitsbeispiels bezeichnet, aus dem dieser Workflow angepasst wird, da ε\varepsilon die turbulente Dissipationsrate in der Vorlesung bezeichnet.

| Symbol | Bedeutung | Einheit | |----------- | QQ | spillway design flow | m3^3s | | bb | Breite des Stillbeckens | m | | btwb_{tw} | Tailwater River Breite | m | qq | Entladung pro Einheitsbreite, q=Q/bq = Q/b | m2^2/s | | H0H_0 | Energie Kopf über dem unversenkten Becken invertiert | m | | db\mathbf{d_b} | Tiefe des Beckenbodens unter dem flussabwärts gelegenen Bett | m | | HH | Energie Kopf über dem Beckenboden, H=H0+dbH = H_0 + \mathbf{d_b} | m | | h1h_1, v1v_1, Fr1Fr_1 | Tiefe, Geschwindigkeit und Froude-Zahl am Eingang des Beckens (überkritisch) | m, m/s, -- | | h2h_2 | konjugierte (sequente) Tiefe des Sprungs | m | | htwh_{tw} | Schwanzwassertiefe im flussabwärts gelegenen Bereich | m | | nMn_M | Manning Rauheitskoeffizient der Downstream-Reichweite | s/m1/3^{1/3} | rtr_{\mathrm{t}} | Retention Ratio, (htw+db)/h2(h_{tw} + \mathbf{d_b})/h_2 | -- | | S0,twS_{0,tw} | Längsbettneigung des stromabwärts gelegenen Bereichs | -- | | ΔH\Delta H | Kopfverlust über den Sprung | m | | flf_{l} | Peterka Basin-Längen-Multiplikator, gelesen von Fr1Fr_1 | -- | | lbl_b, lsl_s | Beckenlänge und Scheuerschutzlänge | m |

Die folgende Zelle importiert die Python-Pakete und die für die Figuren verwendeten Farben. Diese Zellen dienen der Funktionsweise der Berechnungsvorlage und tragen keine Lerninhalte.

import math
from dataclasses import dataclass

import matplotlib.pyplot as plt
import numpy as np
from scipy.optimize import brentq

# Physical constants used throughout.
G = 9.81  # gravitational acceleration [m/s2]

# The lecture palette, so that figures and slides read as one piece.
NAVY, BLUE, CYAN, GREEN, WARN, GREY = (
    "#0C0C48", "#00467F", "#00CAEF", "#167D61", "#C74B2A", "#4A4A4C",
)

plt.rcParams.update({
    "figure.figsize": (8.4, 4.2),
    "figure.dpi": 110,
    "axes.spines.top": False,
    "axes.spines.right": False,
    "axes.titleweight": "bold",
    "axes.labelcolor": NAVY,
    "axes.edgecolor": NAVY,
    "text.color": NAVY,
    "xtick.color": NAVY,
    "ytick.color": NAVY,
    "font.size": 9,
})

Part I: boundary conditions for the hydraulic jump design

Everything in this part is given. Run the cells and note the quantities passed on to Part II. No hydraulic-jump calculation is performed here.

Note: only the discharge, the associated lake level and the gate dimensions are published figures; the remainder are stated assumptions. The real structure is not this simple. A gate-controlled sluice rated at 1500 m3^3/s adjoins the spillway, and the two together pass the largest anticipated flood of 6820 m3^3/s. The energy dissipation at the toe was developed on physical models by the Ministry of Works and Development rather than from a closed-form calculation. This exercise therefore represents the first step that engineers typically complete before a detailed design study with physical or numerical models; it is not a substitute for such a study.

quantityvalueremark
QQ, spillway design flow3200 m3^3/spublished, at a lake level of 195.1 m
bb, width of the chute and basin40 mfour 10 m gate bays, idealised as one rectangular chute with the piers ignored
H0H_0, energy head above the un-sunk basin invert50.00 massumed: the design flood level stands approximately 60 m above the riverbed, of which some 10 m is taken as lost down the chute
btwb_{tw}, nMn_M, S0,twS_{0,tw} of the Clutha downstream of the dam60 m, 0.0357 s/m1/3^{1/3}, 0.0006assumed

In New Zealand practice, catchment design floods are quoted as an annual exceedance probability, so the 100-year flood is the 1% AEP event and the 500-year flood quoted for Clyde is the 0.2% AEP event; the older average recurrence interval (ARI) denotes the same thing. A dam spillway, however, is not designed to a catchment AEP at all: under the NZSOLD New Zealand Dam Safety Guidelines the inflow design flood follows from the dam’s Potential Impact Classification, and for a high-consequence dam it extends to the probable maximum flood.

The following cell defines these hydrological, hydraulic and geometric boundary conditions.

# --- the design discharge ----------------------------------------------------
Q_design = 3200.0  # m3/s   Clyde Dam spillway design flow at lake level 195.1 m
                   #        (published; the 500-year, 0.2% AEP flood)

# --- the chute toe and the stilling basin below it ---------------------------
b_chute = 40.0     # m      four 10 m gate bays, idealised as one rectangle
H0 = 50.00         # m      energy head above the un-sunk basin invert (assumed)

# --- the Clutha River below the dam (assumed) --------------------------------
n_manning = 0.0357  # s/m^(1/3)  Manning roughness of the downstream reach
slope = 6.0e-4      # -          bed slope of the downstream reach
b_tw = 60.0         # m          width of the downstream reach (rectangular idealisation)

print(f"spillway design flow Q  = {Q_design:7.1f} m3/s")
print(f"chute and basin width b = {b_chute:7.2f} m")
print(f"head at the invert   H0 = {H0:7.2f} m")
spillway design flow Q  =  3200.0 m3/s
chute and basin width b =   40.00 m
head at the invert   H0 =   50.00 m

Grenzberechnung

Einheitsentladung

Für die Berechnung des hydraulischen Sprungs ist eine Entladung pro Meter Breite erforderlich:

q=Qbq = \frac{Q}{b}

The chute delivers this flow supercritically, which is the condition for a jump to form. The critical depth printed below is the upper bound on the entry depth h1h_1, and Part II uses it as the bracket when solving the energy equation.

q_unit = Q_design / b_chute
h_crit = (q_unit**2 / G)**(1 / 3)

print(f"unit discharge   q = Q / b   = {q_unit:7.3f} m2/s")
print(f"critical depth   h_c         = {h_crit:7.3f} m")
print(f"critical velocity            = {q_unit / h_crit:7.3f} m/s")
unit discharge   q = Q / b   =  80.000 m2/s
critical depth   h_c         =   8.673 m
critical velocity            =   9.224 m/s

Wassertiefe htwh_{tw}

Die Tiefe in der stromabwärtigen Reichweite bestimmt, ob der Sprung zurückgehalten oder gefegt wird, und wird durch die Reichweite selbst und nicht durch den Überlauf eingestellt. Für einen rechteckigen Kanal in gleichmäßigem Fluss gibt die Manning-Gleichung

Q=1nMAR2/3S0,tw,A=btwhtw,R=btwhtwbtw+2htwQ = \frac{1}{n_M}\,A\,R^{2/3}\,\sqrt{S_{0,tw}}, \qquad A = b_{tw}\,h_{tw}, \qquad R = \frac{b_{tw}\,h_{tw}}{b_{tw} + 2\,h_{tw}}

This is solved for htwh_{tw} numerically.

Note: Natural channels are not rectangular, and htwh_{tw} enters the retention ratio directly. In practice the tailwater rating curve is obtained from gauging, a numerical model, or terrain data, and a range of tailwater levels is tested rather than a single value.

Die Zelle unten druckt auch die kritische Tiefe des stromabwärts gelegenen Bereichs, da ein Sprung nur im Unterwasser enden kann, während dieser Bereich unterkritisch ist.

def manning(h, b, n, S0):
    '''Discharge of a rectangular channel in uniform flow [m3/s].'''
    area = b * h
    hydraulic_radius = (b * h) / (b + 2 * h)
    return (1 / n) * area * hydraulic_radius**(2 / 3) * math.sqrt(S0)


h_tw = brentq(lambda h: manning(h, b_tw, n_manning, slope) - Q_design, 0.01, 40.0)
h_crit_tw = ((Q_design / b_tw)**2 / G)**(1 / 3)

print(f"tailwater depth h_tw = {h_tw:.3f} m")
print(f"check: Q(h_tw) = {manning(h_tw, b_tw, n_manning, slope):.1f} m3/s"
      f"  (target {Q_design:.0f} m3/s)")
print(f"critical depth of the reach = {h_crit_tw:.3f} m"
      f"   -> {'subcritical' if h_tw > h_crit_tw else 'SUPERCRITICAL'}")
tailwater depth h_tw = 16.193 m
check: Q(h_tw) = 3200.0 m3/s  (target 3200 m3/s)
critical depth of the reach = 6.619 m   -> subcritical

Definieren von Datenstrukturen und Standardwerten

Der folgende Codeblock baut die erforderlichen Datenstrukturen auf und setzt Standardwerte für den Kernteil II.

@dataclass(frozen=True)
class DesignData:
    Q: float       # spillway design flow [m3/s]
    q: float       # discharge per unit width [m2/s]
    H0: float      # energy head above the un-sunk basin invert [m]
    h_tw: float    # tailwater depth [m]
    b: float       # width the basin has to cover [m]


DESIGN = DesignData(
    Q=Q_design,
    q=q_unit,
    H0=H0,
    h_tw=h_tw,
    b=b_chute,
)

print(f"Q      = {DESIGN.Q:8.1f} m3/s   spillway design flow")
print(f"q      = {DESIGN.q:8.3f} m2/s   unit discharge")
print(f"H0     = {DESIGN.H0:8.2f} m      energy head above the un-sunk invert")
print(f"h_tw   = {DESIGN.h_tw:8.3f} m      tailwater depth")
print(f"b      = {DESIGN.b:8.2f} m      width the basin has to cover")
Q      =   3200.0 m3/s   spillway design flow
q      =   80.000 m2/s   unit discharge
H0     =    50.00 m      energy head above the un-sunk invert
h_tw   =   16.193 m      tailwater depth
b      =    40.00 m      width the basin has to cover

Part II: Stilling basin & hydraulic jump design

The sequent depth (Bélanger) equation

For a horizontal, rectangular channel, the position and size of the jump follow from momentum conservation, the hydrostatic pressure forces and continuity. Given that the specific force is equal upstream (1) and downstream (2) of the jump, that is, M1=M2M_1 = M_2 with M=q2/(gh)+h2/2M = q^2/(g\,h) + h^2/2, the depth ratio is calculated by the sequent-depth equation of Bélanger:

  h2h1=12(1+8Fr121)  \boxed{\;\frac{h_2}{h_1} = \frac{1}{2}\left(\sqrt{1 + 8\,Fr_1^{2}} - 1\right)\;}

Notably, h1h_1 and h2h_2 are the sequent (conjugate) depths. Every subsequent quantity in this exercise is obtained from h2h_2: the retention ratio that indicates whether the jump stays in the basin, the basin length, and the required length of downstream scour protection.

Design problem

The position of the jump is not fixed by the spillway. It is controlled by the tailwater depth: a jump forms where the downstream depth matches the conjugate depth h2h_2 belonging to the incoming supercritical state. Where the tailwater is too shallow the jump is swept out onto unprotected downstream riverbed; where it is too deep the jump is drowned and dissipates less.

The single design freedom is the basin depth db\mathbf{d_b}, by which the basin floor is set below the downstream riverbed. Increasing db\mathbf{d_b} acts in two opposing directions:

  1. it increases the energy head H=H0+dbH = H_0 + \mathbf{d_b} above the floor, so the entry flow is faster and shallower, which raises Fr1Fr_1 and hence h2h_2;

  2. it increases the depth available over the floor, namely htw+dbh_{tw} + \mathbf{d_b}.

An acceptable design is a basin depth at which both acceptance windows are satisfied simultaneously:

4.5Fr1<9.0and1.05rt=htw+dbh21.154.5 \le Fr_1 < 9.0 \qquad\text{and}\qquad 1.05 \le r_{\mathrm{t}} = \frac{h_{tw} + \mathbf{d_b}}{h_2} \le 1.15

The first acceptance window is the steady jump that leads to 45% to 70% head loss, and no oscillating surge to fatigue the structure. The second acceptance window requires the basin to hold slightly more water than the jump needs, so that the jump is retained with a small margin without being drowned.

The workflow

stepcontenthint
Achoose a trial basin depth db\mathbf{d_b}here: 2.0 m (given)
Bh1h_1 and v1v_1 from the energy equation at the entranceTask 1 + given solver
CFr1=v1/gh1Fr_1 = v_1/\sqrt{g\,h_1}Task 2
Dis 4.5Fr1<9.04.5 \le Fr_1 < 9.0? if not, adjust db\mathbf{d_b}given
Etailwater depth htwh_{tw}given (Part I)
Fconjugate depth h2h_2 from the Bélanger equationTask 3
Gis 1.05rt1.151.05 \le r_{\mathrm{t}} \le 1.15? if not, adjust db\mathbf{d_b}Task 4 + given
+basin length lbl_b and scour-protection length lsl_sTask 5
+head loss ΔH\Delta Hgiven

The five tasks

Each task is one equation from the lecture.

Replace the raise NotImplementedError(...) line with a return statement.

The function names and their arguments must not be changed, because the workflow below calls them.

Important: On completing each task, run the self-check cell. It marks each function [OK] or [XX] against the lecture values.

Aufgabe 1: Energiekopf über dem Beckenboden

Neglecting the approach velocity, the energy head available above the basin floor is H=H0+dbH = H_0 + \mathbf{d_b}. At the basin entrance, that is, cross section 1, that head is split between depth and velocity head. With continuity v=q/hv = q/h for a rectangular section,

H=h+v22g=h+q22gh2H = h + \frac{v^2}{2g} = h + \frac{q^2}{2g\,h^2}

Implement the right-hand side as a function of hh and qq. The given solver entry_depth below selects the shallow, supercritical root of energy_head(h, q) == H, which is the branch delivered by the chute.

def energy_head(h, q, g=G):
    '''
    Task 1: specific energy head of a rectangular section [m].

    Parameters
    ----------
    h : flow depth [m]
    q : discharge per unit width [m2/s]

    Returns
    -------
    the energy head h + q^2 / (2 g h^2) [m]
    '''
    # >>> YOUR CODE HERE
    raise NotImplementedError("Task 1: return the specific energy head")

Aufgabe 2: Froude-Nummer

The Froude number compares the flow velocity with the shallow-water wave speed c=ghc = \sqrt{g h}:

Fr=vghFr = \frac{v}{\sqrt{g\,h}}

Recall that Fr>1Fr > 1 denotes supercritical and Fr<1Fr < 1 subcritical flow. So step D of the workflow only calculates the value of the Froude number (here: Fr1Fr_1) for the later comparison against the steady-jump window:

def froude_number(v, h, g=G):
    '''
    Task 2: Froude number of a rectangular section [-].

    Parameters
    ----------
    v : depth-averaged velocity [m/s]
    h : flow depth [m]
    '''
    # >>> YOUR CODE HERE
    raise NotImplementedError("Task 2: return the Froude number")

Aufgabe 3: Gleichung für die sequentielle Tiefe (Bélanger)

** Dies ist die zentrale Gleichung der Übung. ** Implementieren Sie das Tiefenverhältnis an den Querschnitten 2 (h2h_2) und 1 (h1h_1):

h2h1=12(1+8Fr121)\frac{h_2}{h_1} = \frac{1}{2}\left(\sqrt{1 + 8\,Fr_1^{2}} - 1\right)

Note: This equation describes neither the roller between the cross sections nor the internal structure of the jump. Everything the design requires after this point, that is the retention ratio, the basin length and the scour-protection length, follows from the h2h_2 sequent depth equation returns.

def conjugate_depth(h1, Fr1):
    '''
    Task 3: conjugate (sequent) depth downstream of the jump [m].

    Parameters
    ----------
    h1  : supercritical depth upstream of the jump [m]
    Fr1 : Froude number at section 1 [-]
    '''
    # >>> YOUR CODE HERE
    raise NotImplementedError("Task 3: return the conjugate depth h2")

Aufgabe 4: Beibehaltungsquote

Die Wassertiefe über dem Beckenboden ist die Unterwassertiefe plus die Beckentiefe. Vergleicht man es mit der Tiefe, die der Sprung fordert, ergibt sich das Retentionsverhältnis:

rt=htw+dbh2r_{\mathrm{t}} = \frac{h_{tw} + \mathbf{d_b}}{h_2}

rt<1r_{\mathrm{t}} < 1 means the basin cannot supply the conjugate depth and the jump is swept downstream; rtr_{\mathrm{t}} well above 1 means the jump is drowned. Step G asks for 1.05rt1.151.05 \le r_{\mathrm{t}} \le 1.15.

def retention_ratio(h_tw, d_b, h2):
    '''
    Task 4: retention ratio of the basin [-].

    Parameters
    ----------
    h_tw : tailwater depth in the downstream reach [m]
    d_b  : depth of the basin floor below the downstream bed [m]
    h2   : conjugate depth required by the jump [m]
    '''
    # >>> YOUR CODE HERE
    raise NotImplementedError("Task 4: return the retention ratio")

Aufgabe 5: Becken- und Scheuerschutzlängen

To empirically derive the required stilling basin length lbl_b, Peterka (USBR Engineering Monograph 25) measured the length of the jump in six test flumes and plotted it against the Froude number as a ready reckoner (his Figure 7, free jump on a horizontal apron). The curve gives a factor (here: flf_l), which is read off at the Froude number of cross section 1 (Fr1Fr_1) and multiplied by the conjugate depth (h2h_2):

| Fr1Fr_1 | 2.4 | 4 | 5 | 6 bis 11 | 14

| flf_{l} | 4.8 | 5.8 | 6.0 | 6.13 | 6.0

Die Funktion peterka_f unten liest diese Tabelle (keine Intervention erforderlich) und gibt flf_{l} zurück. Es passt nichts: np.interp interpoliert linear zwischen den tabellarisierten Punkten einer gemessenen Kurve, und diese Kurve ist keine gerade Linie.

def peterka_f(Fr1):
    '''Given: basin-length multiplier after Peterka (USBR EM 25), interpolated [-].'''
    knots_Fr = [2.4, 4.0, 5.0, 6.0, 11.0, 14.0]
    knots_f = [4.8, 5.8, 6.0, 6.13, 6.13, 6.0]
    return float(np.interp(Fr1, knots_Fr, knots_f))


print(f"f_l at Fr1 = 5.0 : {peterka_f(5.0):.3f}")
print(f"f_l at Fr1 = 8.0 : {peterka_f(8.0):.3f}")
f_l at Fr1 = 5.0 : 6.000
f_l at Fr1 = 8.0 : 6.130

Next, calculate the required basin length lbl_b to contain the jump with the peterka_f function. Because the flow leaving the jump (and the basin end sill) is still turbulent, the riverbed immediately downstream of the basin has to be armoured over a length lsl_s:

lb=fl(Fr1)h2,ls=3.5lbl_b = f_{l}(Fr_1) h_2, \qquad l_s = 3.5 l_b

Aufgabe 5: Verwenden Sie peterka_f für den Multiplikator und geben Sie lbl_b und lsl_s zurück.

def basin_lengths(h2, Fr1):
    '''
    Task 5: basin length and scour-protection length [m].

    Parameters
    ----------
    h2  : conjugate depth downstream of the jump [m]
    Fr1 : Froude number at section 1, which selects the multiplier [-]

    Returns
    -------
    (l_b, l_s) : basin length and scour-protection length [m]
    '''
    # >>> YOUR CODE HERE
    raise NotImplementedError("Task 5: return the basin and scour lengths")

Hinweis: Für einen stetigen Sprung ist lbl_b in der Nähe der Faustregel ljl_j \approx6 h2h_2, und die Zellen in diesem Notizbuch drucken sowohl ljl_j als auch lbl_b. Andere Designhandbücher würden jedoch unterschiedliche Faktoren angeben, was einer der Gründe ist, warum ein detailliertes Design mit einem physikalischen oder numerischen Modell entscheidend ist.

Selbstkontrolle

Run this cell after completing each task. The reference values are the approximate results presented in the lecture slides, so [OK] on every line indicates that the five tasks reproduce the slides.

CHECKS = [
    ("Task 1  energy_head(2.4061, 80.000)",
     lambda: energy_head(2.4061, 80.000), (58.751,), "m"),
    ("Task 2  froude_number(33.249, 2.4061)",
     lambda: froude_number(33.249, 2.4061), (6.844,), "-"),
    ("Task 3  conjugate_depth(2.4061, 6.844)",
     lambda: conjugate_depth(2.4061, 6.844), (22.116,), "m"),
    ("Task 4  retention_ratio(16.193, 8.75, 22.115)",
     lambda: retention_ratio(16.193, 8.75, 22.115), (1.128,), "-"),
    ("Task 5  basin_lengths(22.115, 6.844)",
     lambda: basin_lengths(22.115, 6.844), (135.565, 474.477), "m"),
]


def check_tasks(tolerance=5e-3, verbose=True):
    '''Compare each task against the worked values from the lecture.'''
    passed = 0
    for label, call, expected, unit in CHECKS:
        try:
            value = call()
        except NotImplementedError:
            if verbose:
                print(f"[ ] {label:<46} not implemented yet")
            continue
        got = value if isinstance(value, tuple) else (value,)
        ok = (len(got) == len(expected) and
              all(abs(g - e) <= tolerance * max(1.0, abs(e))
                  for g, e in zip(got, expected)))
        passed += ok
        if verbose:
            mark = "OK" if ok else "XX"
            shown = ", ".join(f"{g:.3f}" for g in got)
            wanted = ", ".join(f"{e:.3f}" for e in expected)
            print(f"[{mark}] {label:<46} {shown:>19} {unit:<3}"
                  f" (expected {wanted})")
    if verbose:
        print("-" * 78)
        print(f"{passed} of {len(CHECKS)} tasks correct.")
    return passed == len(CHECKS)


TASKS_DONE = check_tasks()
[ ] Task 1  energy_head(2.4061, 80.000)            not implemented yet
[ ] Task 2  froude_number(33.249, 2.4061)          not implemented yet
[ ] Task 3  conjugate_depth(2.4061, 6.844)         not implemented yet
[ ] Task 4  retention_ratio(16.193, 8.75, 22.115)  not implemented yet
[ ] Task 5  basin_lengths(22.115, 6.844)           not implemented yet
------------------------------------------------------------------------------
0 of 5 tasks correct.

Workflow-Implementierung: die Suche über die Beckentiefe

**Der Rest der Berechnungen von Teil II wird angegeben (die Antworten auf die Folgefragen sind nicht). **

The function basin_state evaluates steps B, C, F and G for one trial basin depth and calls the functions written above; search_basin_depth performs steps A and D, adjusting db\mathbf{d_b} by bisection until both acceptance windows “close”. The bisection logic should be read before the cell is run.

Fr1Fr_1 and rtr_{\mathrm{t}} increase with db\mathbf{d_b}, which is what allows one to use a single bracket for both tests: too shallow a basin does not supply the depth the jump demands and sweeps it downstream, but too deep a basin supplies more than the jump requires and drowns it. Each failed trial therefore indicates which half of the bracket to retain, and in the present case the search fails in both directions before converging.

Observe: The search workflow starts from a first trial of db\mathbf{d_b} = 2.0 m. That trial satisfies step D, Fr1Fr_1 = 6.2 already being a steady jump, and fails step G, the jump being swept out. The bisection then deepens the basin, overshoots into a drowned jump, returns too far, and converges on the sequence 2.0, 11.0, 6.5, 8.75 m. It terminates at the first basin depth satisfying the two acceptance windows, so the result depends on the starting value and on the width of the bracket; a report should state which bracket produced it.

FR_WINDOW = (4.5, 9.0)     # steady-jump range, step D
RT_WINDOW = (1.05, 1.15)   # retention window, step G


@dataclass(frozen=True)
class BasinState:
    d_b: float     # trial basin depth [m]
    H: float       # energy head above the basin floor [m]
    h1: float      # supercritical entry depth [m]
    v1: float      # entry velocity [m/s]
    Fr1: float     # entry Froude number [-]
    h2: float      # conjugate depth [m]
    Rt: float      # retention ratio [-]

    @property
    def fr_ok(self):
        return FR_WINDOW[0] <= self.Fr1 < FR_WINDOW[1]

    @property
    def rt_ok(self):
        return RT_WINDOW[0] <= self.Rt <= RT_WINDOW[1]


def critical_depth(q, g=G):
    '''Critical depth of a rectangular section [m]; the bracket for step B.'''
    return (q**2 / g)**(1 / 3)


def entry_depth(H, q):
    '''
    Step B: the shallow, supercritical root of energy_head(h, q) = H [m].

    Both roots satisfy the energy equation. The supercritical one lies below the
    critical depth, and it is the branch the spillway delivers, so the bracket is
    closed at the critical depth.
    '''
    return brentq(lambda h: energy_head(h, q) - H, 1e-4, critical_depth(q))


def basin_state(d_b, design=None):
    '''Steps B, C, F and G of the workflow for one trial basin depth d_b.'''
    design = design or DESIGN
    H = design.H0 + d_b                          # step B: head above the floor
    h1 = entry_depth(H, design.q)                # step B: supercritical root
    v1 = design.q / h1                           #         continuity
    Fr1 = froude_number(v1, h1)                  # step C
    h2 = conjugate_depth(h1, Fr1)                # step F
    Rt = retention_ratio(design.h_tw, d_b, h2)   # step G
    return BasinState(d_b=d_b, H=H, h1=h1, v1=v1, Fr1=Fr1, h2=h2, Rt=Rt)
def search_basin_depth(db_start=2.0, db_min=0.0, db_max=20.0, max_iter=15, verbose=True):
    '''
    Steps A and D: bisection on the basin depth d_b until both windows close.

    Returns the accepted BasinState and the full iteration history.
    '''
    d_b = db_start
    history = []
    if verbose:
        print(f"{'iter':>4}{'d_b [m]':>9}{'Fr1':>8}{'h1 [m]':>9}{'h2 [m]':>9}"
              f"{'Rt':>8}   verdict")
        print("-" * 66)

    for i in range(1, max_iter + 1):
        state = basin_state(d_b)
        history.append(state)
        if verbose:
            flags = []
            if not state.fr_ok:
                flags.append("Fr1 low" if state.Fr1 < FR_WINDOW[0] else "Fr1 high")
            if not state.rt_ok:
                flags.append("swept out" if state.Rt < RT_WINDOW[0] else "drowned")
            print(f"{i:>4}{state.d_b:>9.3f}{state.Fr1:>8.2f}{state.h1:>9.4f}"
                  f"{state.h2:>9.4f}{state.Rt:>8.3f}   "
                  f"{'accepted' if not flags else ', '.join(flags)}")

        if state.fr_ok and state.rt_ok:
            return state, history

        # A deeper basin raises H, so the entry flow is shallower and faster: Fr1 and Rt increase monotonically with d_b. One bracket therefore serves both criteria, and each verdict says which way to move: a jump that is too gentle or swept out wants a deeper basin, one that is too fierce or drowned wants a shallower one.
        if not state.fr_ok:
            if state.Fr1 < FR_WINDOW[0]:
                db_min = d_b  # jump too gentle: deepen the basin
            else:
                db_max = d_b  # jump too fierce: raise the floor
        elif state.Rt > RT_WINDOW[1]:
            db_max = d_b      # drowned: shallower basin
        else:
            db_min = d_b      # swept out: deeper basin

        d_b = 0.5 * (db_min + db_max)

    raise RuntimeError("no acceptable basin depth found in the search range")


if TASKS_DONE:
    DESIGN_STATE, HISTORY = search_basin_depth()
    print(f"\naccepted basin depth d_b = {DESIGN_STATE.d_b:.3f} m")
else:
    DESIGN_STATE, HISTORY = None, []
    print("Complete Tasks 1 to 5, then re-run this cell.")
Complete Tasks 1 to 5, then re-run this cell.

Kopfverlust über den Sprung

Der Kopfverlust quantifiziert die Arbeit, die das Becken leistet. Dies ist keine Aufgabe: Die Designentscheidung wurde bereits von den beiden Annahmefenstern oben getroffen. Der Verlust wird hier berechnet, weil es der Grund ist, warum das Becken existiert. Die Kombination der Energiegleichung mit der konjugierten Tiefenbeziehung auf einem horizontalen Bett ergibt den Kopfverlust in geschlossener Form,

ΔH=H1H2=(h2h1)34h1h2>0\Delta H = H_1 - H_2 = \frac{(h_2 - h_1)^3}{4 h_1 h_2} > 0

ΔH\Delta H ist ein Verlust von Kopf und nicht von Energie: Es stellt die irreversible Umwandlung von mechanischer Mittelflussenergie in Turbulenzen und letztendlich in innere Energie dar, wobei die Gesamtenergie durchweg konserviert wird.

def head_loss(h1, h2):
    '''Given: head loss across the jump [m].'''
    return (h2 - h1)**3 / (4 * h1 * h2)


if TASKS_DONE:
    delta_H = head_loss(DESIGN_STATE.h1, DESIGN_STATE.h2)
    H1 = energy_head(DESIGN_STATE.h1, DESIGN.q)

    print(f"head loss           dH        = {delta_H:8.3f} m")
    print(f"energy head at 1    H1        = {H1:8.3f} m")
    print(f"relative loss       dH / H1   = {delta_H / H1:8.1%}")
else:
    delta_H = H1 = None
    print("Complete Tasks 1 to 4, then re-run this cell.")
Complete Tasks 1 to 4, then re-run this cell.

Länge des Beckens und Scheuerschutz

Aufgabe 5, bewertet am akzeptierten Design, neben der Faustregel (ljl_j \approx6h2h_2), gegen die die Beckenlänge überprüft werden kann.

if TASKS_DONE:
    f_peterka = peterka_f(DESIGN_STATE.Fr1)
    l_b, l_s = basin_lengths(DESIGN_STATE.h2, DESIGN_STATE.Fr1)

    print(f"Peterka multiplier  f_l  = {f_peterka:7.2f}  (at Fr1 = {DESIGN_STATE.Fr1:.2f})")
    print(f"basin length        l_b  = {l_b:7.2f} m   <- Task 5")
    print(f"rule of thumb     6 h_2  = {6 * DESIGN_STATE.h2:7.2f} m"
          f"   ({abs(6 * DESIGN_STATE.h2 / l_b - 1):.1%} from l_b)")
    print(f"scour protection    l_s  = {l_s:7.2f} m   <- Task 5")
else:
    f_peterka = l_b = l_s = None
    print("Complete Tasks 1 to 5, then re-run this cell.")
Complete Tasks 1 to 5, then re-run this cell.

Ergebnisbewertungsflächen

Entwurfsfensterprüfung

Die beiden Akzeptanzfenster werden dabei grafisch in Abhängigkeit von der Beckentiefe db\mathbf{d_b} ausgewertet. Die schattierten Bänder in den Plots sind die Fenster und die gestrichelte vertikale Linie ist das akzeptierte Design. Nur ein Kriterium ist entscheidend: Fr1Fr_1 bleibt über alle Tests hinweg in seinem Fenster, aber rtr_{\mathrm{t}} tritt in sein Akzeptanzfenster ein und lässt es innerhalb von etwa 2,5 m Beckentiefe, zwischen 6,79 m und 9,31 m, wieder. Das Retentionskriterium wählt daher db\mathbf{d_b} aus, und es ist das Kriterium, das von der gegebenen Unterwassertiefe htwh_{tw} abhängt.

if TASKS_DONE:
    db_range = np.linspace(0.2, 20.0, 240)
    states = [basin_state(float(x)) for x in db_range]
    Fr_curve = np.array([s.Fr1 for s in states])
    Rt_curve = np.array([s.Rt for s in states])

    fig, axes = plt.subplots(1, 2, figsize=(9.4, 3.6), sharex=True)

    axes[0].axhspan(*FR_WINDOW, color=CYAN, alpha=0.18, lw=0)
    axes[0].plot(db_range, Fr_curve, color=BLUE, lw=2.2)
    axes[0].set_ylabel("$Fr_1$ at the basin entrance [-]")
    axes[0].set_title("Step D: steady-jump window")
    axes[0].set_ylim(4.3, 9.3)
    axes[0].annotate("steady jump\n$4.5 \\leq Fr_1 < 9$", (0.8, 8.4),
                     color=BLUE, fontsize=8.5)

    axes[1].axhspan(Rt_curve.min(), RT_WINDOW[0], color=WARN, alpha=0.10, lw=0)
    axes[1].axhspan(*RT_WINDOW, color=CYAN, alpha=0.18, lw=0)
    axes[1].axhspan(RT_WINDOW[1], Rt_curve.max(), color=GREY, alpha=0.10, lw=0)
    axes[1].axhline(1.0, color=GREY, lw=0.8, ls=":")
    axes[1].plot(db_range, Rt_curve, color=BLUE, lw=2.2)
    axes[1].set_ylabel("retention ratio $r_\\mathrm{t}$ [-]")
    axes[1].set_title("Step G: retention window")
    axes[1].set_ylim(Rt_curve.min(), Rt_curve.max())
    axes[1].annotate("jump swept out", (4.2, 0.83), color=WARN, fontsize=8.5)
    axes[1].annotate("retained", (0.8, 1.09), color=BLUE, fontsize=8.5)
    axes[1].annotate("jump drowned", (0.8, 1.42), color=GREY, fontsize=8.5)

    for ax in axes:
        ax.axvline(DESIGN_STATE.d_b, color=WARN, lw=1.4, ls="--")
        # the design variable is the one symbol set apart from the notation:
        # bold and in the warn colour, on the slides and here alike
        ax.set_xlabel("basin depth $\\mathbf{d_b}$ [m]", color=WARN)
        ax.set_xlim(0.2, 20.0)

    axes[0].plot([DESIGN_STATE.d_b], [DESIGN_STATE.Fr1], "o", color=WARN, ms=6, zorder=3)
    axes[1].plot([DESIGN_STATE.d_b], [DESIGN_STATE.Rt], "o", color=WARN, ms=6, zorder=3)
    axes[0].annotate(f"design\n$\\mathbf{{d_b}} = {DESIGN_STATE.d_b:.2f}$ m,"
                     f"  $Fr_1 = {DESIGN_STATE.Fr1:.2f}$",
                     (DESIGN_STATE.d_b, DESIGN_STATE.Fr1), textcoords="offset points",
                     xytext=(-104, 34), color=WARN, fontsize=8.5,
                     arrowprops=dict(arrowstyle="-", color=WARN, lw=0.8))
    axes[1].annotate(f"$r_\\mathrm{{t}} = {DESIGN_STATE.Rt:.3f}$",
                     (DESIGN_STATE.d_b, DESIGN_STATE.Rt), textcoords="offset points",
                     xytext=(40, -58), color=WARN, fontsize=8.5,
                     arrowprops=dict(arrowstyle="-", color=WARN, lw=0.8))

    fig.tight_layout()
    plt.show()
else:
    print("Complete Tasks 1 to 5, then re-run this cell.")
Complete Tasks 1 to 5, then re-run this cell.

Längsabschnitt des Beruhigungsbeckens

Das akzeptierte Design wird im folgenden Codeblock maßstäblich gezeichnet, wobei das nachgelagerte Flussbett als Datum dient. Über dem Boden des Beruhigungsbeckens sind zwei Tiefen markiert, und der Unterschied zwischen ihnen ist Gegenstand von Schritt G:

  • Verfügbar ist die Tiefe, die durch das Versenkwasser und die Tiefe des Beckens bereitgestellt wird, htw+dbh_{tw} + \mathbf{d_b}.

  • Erforderlich ist die für den Sprung erforderliche Tiefe, d.h. die konjugierte Tiefe h2h_2.

Their ratio is rtr_{\mathrm{t}}, and the surplus is the margin by which the jump is retained within the basin rather than on the bed downstream. Only the end depths of the jump follow from the 1d relations, so the surface drawn between cross sections 1 and 2 is indicative.

if TASKS_DONE:
    s = DESIGN_STATE
    floor = -s.d_b                       # basin floor, below the downstream riverbed
    h_avail = DESIGN.h_tw + s.d_b        # depth available over the floor
    top = DESIGN.H0                      # energy head above the un-sunk invert
    u = l_b / 24.8                       # one drawing unit, so the layout scales

    x_face, x_toe, x_end = -7.0 * u, 0.0, l_b
    x_tail = x_end + 0.6 * l_b
    x_j0, x_j1 = 0.26 * l_b, 0.62 * l_b

    fig, ax = plt.subplots(figsize=(9.4, 3.4))

    # --- structure and bed ---------------------------------------------------
    # the chute face descends from the spillway crest to the basin floor,
    # then the floor runs to the end sill and the river bed continues downstream
    ax.plot([x_face - 2.0 * u, x_face, x_face + 3.2 * u, x_toe, x_end, x_end, x_tail],
            [top, top, top, floor, floor, 0.0, 0.0],
            color=NAVY, lw=2.4, solid_joinstyle="round")

    # --- water body ----------------------------------------------------------
    xs = np.linspace(x_toe, x_tail, 500)
    bed = np.where(xs <= x_end, floor, 0.0)
    ramp = np.clip((xs - x_j0) / (x_j1 - x_j0), 0.0, 1.0)
    surface = (floor + s.h1) + (DESIGN.h_tw - floor - s.h1) * (
        0.5 - 0.5 * np.cos(math.pi * ramp))

    ax.fill_between(xs, surface, bed, color=CYAN, alpha=0.30, lw=0)
    ax.plot(xs, surface, color=BLUE, lw=2.0)

    # --- dimensions ----------------------------------------------------------
    def dim(x, y0, y1, label, colour=NAVY, dx=0.7 * u, ha="left"):
        ax.annotate("", (x, y0), (x, y1),
                    arrowprops=dict(arrowstyle="<->", color=colour, lw=1.1))
        ax.text(x + dx, 0.5 * (y0 + y1), label, color=colour, fontsize=8.2,
                va="center", ha=ha)

    ax.plot([0.42 * x_end, x_end], [floor + s.h2] * 2, color=WARN, lw=1.0, ls="--")
    dim(0.50 * x_end, floor, floor + s.h2,
        f"required\n$h_2$ = {s.h2:.2f} m", colour=WARN, dx=-0.7 * u, ha="right")
    dim(0.86 * x_end, floor, DESIGN.h_tw,
        f"available\n$h_{{tw}}+\\mathbf{{d_b}}$ = {h_avail:.2f} m",
        dx=-0.7 * u, ha="right")
    ax.annotate(f"$h_1$ = {s.h1:.2f} m",
                (x_toe + 3.2 * u, floor + 0.5 * s.h1), textcoords="offset points",
                xytext=(2, 54), color=NAVY, fontsize=8.2,
                arrowprops=dict(arrowstyle="->", color=NAVY, lw=0.9))
    dim(x_end + 2.4 * u, floor, 0.0,
        f"$\\mathbf{{d_b}}$ = {s.d_b:.2f} m", colour=WARN)
    dim(x_end + 9.0 * u, 0.0, DESIGN.h_tw, f"$h_{{tw}}$ = {DESIGN.h_tw:.2f} m")

    ax.annotate("", (x_toe, floor - 0.365 * s.h2), (x_end, floor - 0.365 * s.h2),
                arrowprops=dict(arrowstyle="<->", color=NAVY, lw=1.1))
    ax.text(0.5 * x_end, floor - 0.62 * s.h2,
            f"basin length $l_b$ = {l_b:.1f} m",
            color=NAVY, fontsize=8.2, ha="center")

    # --- labels --------------------------------------------------------------
    ax.axhline(0.0, color=GREY, lw=0.7, ls=":")
    ax.text(0.5 * (x_j0 + x_j1), 0.62 * top, "hydraulic jump",
            color=NAVY, fontsize=9, ha="center", style="italic")
    ax.annotate("", (0.5 * (x_j0 + x_j1), 0.32 * top),
                (0.5 * (x_j0 + x_j1), 0.55 * top),
                arrowprops=dict(arrowstyle="->", color=NAVY, lw=0.9))
    ax.text(x_face - 1.6 * u, top + 0.12 * s.h2,
            f"from the spillway chute,  $H_0$ = {top:.2f} m",
            color=NAVY, fontsize=8.2)

    ax.set_xlim(x_face - 2.5 * u, x_tail + 1.0 * u)
    ax.set_ylim(floor - 0.75 * s.h2, top + 0.55 * s.h2)
    ax.set_aspect("equal")
    ax.set_xlabel("distance along the basin [m]")
    ax.set_ylabel("level above the\ndownstream bed [m]")
    ax.set_title(f"Stilling basin below Clyde Dam, accepted design "
                 f"($r_\\mathrm{{t}}$ = {h_avail:.2f} / {s.h2:.2f} = {s.Rt:.3f})")
    ax.spines["left"].set_visible(True)
    fig.tight_layout()
    plt.show()
else:
    print("Complete Tasks 1 to 5, then re-run this cell.")
Complete Tasks 1 to 5, then re-run this cell.

Entwurfszusammenfassung

if TASKS_DONE:
    s = DESIGN_STATE
    rows = [
        ("given", "spillway design flow", "Q", DESIGN.Q, "m3/s"),
        ("given", "unit discharge", "q", DESIGN.q, "m2/s"),
        ("given", "energy head above the invert", "H0", DESIGN.H0, "m"),
        ("given", "tailwater depth", "h_tw", DESIGN.h_tw, "m"),
        ("design", "basin depth", "d_b", s.d_b, "m"),
        ("design", "energy head above the floor", "H", s.H, "m"),
        ("design", "entry depth", "h1", s.h1, "m"),
        ("design", "entry velocity", "v1", s.v1, "m/s"),
        ("design", "entry Froude number", "Fr1", s.Fr1, "-"),
        ("design", "conjugate depth", "h2", s.h2, "m"),
        ("design", "retention ratio", "Rt", s.Rt, "-"),
        ("result", "head loss", "dH", delta_H, "m"),
        ("result", "relative head loss", "dH/H1", 100 * delta_H / H1, "%"),
        ("result", "basin length", "l_b", l_b, "m"),
        ("result", "scour protection length", "l_s", l_s, "m"),
    ]

    print(f"{'':8}{'quantity':<34}{'symbol':<14}{'value':>10}  unit")
    group = None
    for kind, name, symbol, value, unit in rows:
        if kind != group:
            print("-" * 70)
            group = kind
        print(f"{kind:<8}{name:<34}{symbol:<14}{value:>10.3f}  {unit}")

    print("=" * 70)
    print(f"windows: Fr1 in [{FR_WINDOW[0]}, {FR_WINDOW[1]}) -> "
          f"{'met' if s.fr_ok else 'NOT met'};  "
          f"Rt in [{RT_WINDOW[0]}, {RT_WINDOW[1]}] -> "
          f"{'met' if s.rt_ok else 'NOT met'}")
else:
    print("Complete Tasks 1 to 5, then re-run this cell.")
Complete Tasks 1 to 5, then re-run this cell.

Follow-up questions

Each of the following requires a single change to one input of the code written above, to explore how uncertainties and changes in the boundary conditions act on the hydraulic jump, and therefore on the stilling basin design.

  1. What happens if the tailwater depth is lower? Recompute with the tailwater depth reduced by 2 m and everything else unchanged, that is, hand basin_state a DesignData whose h_tw is 2 m smaller. Determine the effect on rtr_{\mathrm{t}} and its consequence for the downstream riverbed, and identify how the jump position changes.

  2. How does the jump behave under different discharge scenarios? The basin is designed for a 500-year flood but operates mostly at much lower discharges. So re-evaluate basin_state with a DesignData whose q is halved, and determine whether the jump remains steady and remains retained.

Limits of the simplification

This calculation template uses simplified 1d equations, and its results are therefore subject to considerable uncertainty, related to (but not limited to):

  • hydrology, that is the derivation of the design flood;

  • the tailwater rating curve, instead of the rectangular Manning idealisation;

  • load and failure scenarios, including floods exceeding the spillway design discharge, partial-gate operation, and the sluice gates beside the spillway;

  • cavitation, air entrainment, uplift, fluctuating pressures and fatigue of the structure;

  • scour and riverbed-stability assessment downstream of the protected length;

  • geotechnical analysis and structural design;

  • fish passage and sediment management.

Sources