{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Python Refresher — Lab (solutions)\n", "\n", "**AI for Hydraulics · Part 1 · Pre-course**\n", "\n", "This lab reviews the Python you need for the course, on small hydraulic problems: pipe flow, rainfall records, gauging stations, flow regimes, friction losses and messy sensor data.\n", "\n", "**How to work**\n", "- Run the cells from top to bottom (`Shift + Enter`).\n", "- Replace every `...` with your code.\n", "- Each exercise ends with a ✅ **Check** giving the values you should obtain.\n", "- Use plain Python only (no NumPy): that's the point of this refresher.\n", "- Before you finish: `Runtime → Restart and run all` to make sure the notebook runs from top to bottom." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import math" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercise 1 — Variables, arithmetic and f-strings\n", "\n", "Water flows at $Q = 0.08$ m³/s in a circular pipe of diameter $D = 0.25$ m. The kinematic viscosity of water at 20 °C is $\\nu = 1.0 \\times 10^{-6}$ m²/s.\n", "\n", "**a)** Compute the flow area $A = \\pi D^2/4$, the mean velocity $V = Q/A$ and the Reynolds number $Re = V D / \\nu$.\n", "\n", "**b)** Print them on one line with an f-string: area with 4 decimals, velocity with 3 decimals, Reynolds number with a thousands separator and no decimals, each followed by its unit." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "Q = 0.08 # discharge (m³/s)\n", "D = 0.25 # diameter (m)\n", "nu = 1.0e-6 # kinematic viscosity (m²/s)\n", "\n", "A = math.pi * D**2 / 4\n", "V = Q / A\n", "Re = V * D / nu\n", "\n", "print(f\"A = {A:.4f} m², V = {V:.3f} m/s, Re = {Re:,.0f}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**c)** Classify the flow with `if` / `elif` / `else`: laminar if $Re < 2300$, transitional if $2300 \\le Re < 4000$, turbulent otherwise.\n", "\n", "✅ **Check:** `A = 0.0491 m², V = 1.630 m/s, Re = 407,437`, turbulent." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "if Re < 2300:\n", " flow_type = \"laminar\"\n", "elif Re < 4000:\n", " flow_type = \"transitional\"\n", "else:\n", " flow_type = \"turbulent\"\n", "\n", "print(flow_type)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercise 2 — Lists and loops: a month of rainfall\n", "\n", "`rain` contains the daily rainfall (mm) of one month at a rain gauge; `rain[0]` is day 1." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rain = [0.0, 0.0, 2.4, 15.8, 32.1, 6.2, 0.0, 0.0, 0.0, 0.3,\n", " 0.0, 0.0, 0.0, 0.0, 11.5, 4.8, 0.0, 0.0, 0.0, 0.0,\n", " 0.0, 0.0, 1.2, 22.7, 8.9, 0.0, 0.0, 0.0, 0.0, 0.0]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**a)** Compute the number of days, the monthly total (rounded to 1 decimal), the maximum daily rainfall and the **day number** on which it occurred (hint: `list.index`).\n", "\n", "**b)** A *rainy day* is a day with at least 1 mm. Using a list comprehension, build the list of rainy-day amounts and count them." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "n_days = len(rain)\n", "total = round(sum(rain), 1)\n", "max_rain = max(rain)\n", "max_day = rain.index(max_rain) + 1\n", "print(n_days, total, max_rain, max_day)\n", "\n", "rainy = [r for r in rain if r >= 1.0]\n", "n_rainy = len(rainy)\n", "print(n_rainy)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**c)** Find the **longest dry spell**: the largest number of consecutive days with less than 1 mm. Use a `for` loop and two counters (the current spell and the longest so far).\n", "\n", "✅ **Check:** 30 days, total 105.9 mm, max 32.1 mm on day 5, 9 rainy days, longest dry spell 8 days." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "longest = 0\n", "current = 0\n", "for r in rain:\n", " if r < 1.0:\n", " current += 1\n", " longest = max(longest, current)\n", " else:\n", " current = 0\n", "\n", "print(longest)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercise 3 — Dictionaries: gauging stations\n", "\n", "Each station is a dictionary; together they form a list of dictionaries (a small table). The values are illustrative." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "stations = [\n", " {\"code\": \"S01\", \"river\": \"Oued A\", \"area_km2\": 3600, \"mean_Q\": 7.2},\n", " {\"code\": \"S02\", \"river\": \"Oued B\", \"area_km2\": 850, \"mean_Q\": 2.9},\n", " {\"code\": \"S03\", \"river\": \"Oued C\", \"area_km2\": 1240, \"mean_Q\": 2.1},\n", " {\"code\": \"S04\", \"river\": \"Oued D\", \"area_km2\": 420, \"mean_Q\": 1.6},\n", "]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**a)** For each station, add a new key `\"q_Ls_km2\"`: the **specific discharge** in L/s/km² (mean discharge converted to L/s, divided by the catchment area). Print the code and the specific discharge (2 decimals) of every station.\n", "\n", "**b)** Which station has the largest specific discharge? (Hint: `max` with `key=lambda s: ...`.)\n", "\n", "**c)** What is the total catchment area of the four stations? (Use a generator expression inside `sum`.)\n", "\n", "**d)** Build a dictionary `by_code` mapping each station code to its dictionary, then use it to print the river of station `\"S03\"`.\n", "\n", "✅ **Check:** specific discharges 2.00, 3.41, 1.69, 3.81 L/s/km²; largest S04; total area 6110 km²; S03 is on Oued C." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for s in stations:\n", " s[\"q_Ls_km2\"] = s[\"mean_Q\"] * 1000 / s[\"area_km2\"]\n", " print(f\"{s['code']}: {s['q_Ls_km2']:.2f} L/s/km²\")\n", "\n", "best = max(stations, key=lambda s: s[\"q_Ls_km2\"])\n", "print(\"largest:\", best[\"code\"])\n", "\n", "total_area = sum(s[\"area_km2\"] for s in stations)\n", "print(\"total area:\", total_area, \"km²\")\n", "\n", "by_code = {s[\"code\"]: s for s in stations}\n", "print(by_code[\"S03\"][\"river\"])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercise 4 — Functions: flow regimes along a channel\n", "\n", "**a)** Write a function `froude(V, h, g=9.81)` returning the Froude number $Fr = V / \\sqrt{g\\,h}$ of a flow of mean velocity $V$ (m/s) and depth $h$ (m), with a docstring.\n", "\n", "**b)** Write a function `regime(Fr, tol=0.02)` returning `\"subcritical\"` if $Fr < 1 - tol$, `\"supercritical\"` if $Fr > 1 + tol$, and `\"critical\"` otherwise." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def froude(V, h, g=9.81):\n", " \"\"\"Froude number for mean velocity V (m/s) and depth h (m).\"\"\"\n", " return V / math.sqrt(g * h)\n", "\n", "\n", "def regime(Fr, tol=0.02):\n", " \"\"\"Flow regime from the Froude number, with a tolerance around Fr = 1.\"\"\"\n", " if Fr < 1 - tol:\n", " return \"subcritical\"\n", " elif Fr > 1 + tol:\n", " return \"supercritical\"\n", " return \"critical\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**c)** `sections` lists measurements along a spillway as tuples `(name, V, h)`. Loop over it, unpacking each tuple, and print a line per section: name, Froude number (3 decimals) and regime.\n", "\n", "✅ **Check:** upstream 0.286 subcritical · weir crest 0.995 critical · chute 3.281 supercritical · stilling basin 0.182 subcritical." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sections = [\n", " (\"upstream\", 1.2, 1.8),\n", " (\"weir crest\", 2.7, 0.75),\n", " (\"chute\", 6.5, 0.4),\n", " (\"stilling basin\", 0.9, 2.5),\n", "]\n", "\n", "for name, V_s, h_s in sections:\n", " Fr = froude(V_s, h_s)\n", " print(f\"{name:<15} Fr = {Fr:.3f} {regime(Fr)}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercise 5 — Iteration: friction losses in a pipe\n", "\n", "We return to the pipe of Exercise 1 (reuse `D`, `V`, `Re`). It is a commercial steel pipe of length $L = 850$ m with absolute roughness $\\varepsilon = 0.15$ mm.\n", "\n", "**a)** Write `colebrook(Re, rel_rough, f0=0.02, tol=1e-8, max_iter=100)` solving the Colebrook–White equation by fixed-point iteration. It returns the friction factor **and** the number of iterations, and raises a `RuntimeError` if it does not converge.\n", "\n", "$$\\frac{1}{\\sqrt{f}} = -2 \\log_{10}\\!\\left( \\frac{\\varepsilon / D}{3.7} + \\frac{2.51}{Re\\,\\sqrt{f}} \\right)$$\n", "\n", "Rearranged for iteration: $f_{new} = \\left[-2 \\log_{10}\\!\\left( \\dfrac{\\varepsilon / D}{3.7} + \\dfrac{2.51}{Re\\,\\sqrt{f}} \\right)\\right]^{-2}$" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def colebrook(Re, rel_rough, f0=0.02, tol=1e-8, max_iter=100):\n", " \"\"\"Darcy friction factor from Colebrook-White. Returns (f, iterations).\"\"\"\n", " f = f0\n", " for iteration in range(1, max_iter + 1):\n", " f_new = (-2 * math.log10(rel_rough / 3.7 + 2.51 / (Re * math.sqrt(f)))) ** -2\n", " if abs(f_new - f) < tol:\n", " return f_new, iteration\n", " f = f_new\n", " raise RuntimeError(\"Colebrook did not converge\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**b)** Compute the friction factor of the pipe (careful: $\\varepsilon$ is given in **mm**).\n", "\n", "**c)** Compare it with the explicit **Swamee–Jain** approximation and give the relative difference in %:\n", "\n", "$$f_{SJ} = \\frac{0.25}{\\left[\\log_{10}\\!\\left(\\dfrac{\\varepsilon/D}{3.7} + \\dfrac{5.74}{Re^{0.9}}\\right)\\right]^2}$$\n", "\n", "**d)** Compute the head loss with the Darcy–Weisbach equation $h_f = f \\, \\dfrac{L}{D} \\, \\dfrac{V^2}{2g}$.\n", "\n", "✅ **Check:** $f$ ≈ 0.01843 after 5 iterations; Swamee–Jain ≈ 0.01855 (≈ 0.66 % higher); $h_f$ ≈ 8.48 m." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "L = 850.0 # pipe length (m)\n", "eps_mm = 0.15 # absolute roughness (mm)\n", "g = 9.81\n", "\n", "rel_rough = (eps_mm / 1000) / D\n", "f, n_iter = colebrook(Re, rel_rough)\n", "print(f\"f = {f:.5f} after {n_iter} iterations\")\n", "\n", "f_sj = 0.25 / math.log10(rel_rough / 3.7 + 5.74 / Re**0.9) ** 2\n", "diff_pct = abs(f_sj - f) / f * 100\n", "print(f\"Swamee-Jain: {f_sj:.5f} ({diff_pct:.2f} % difference)\")\n", "\n", "h_f = f * L / D * V**2 / (2 * g)\n", "print(f\"head loss: {h_f:.2f} m\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercise 6 — Errors: cleaning sensor readings\n", "\n", "A water-level logger exported its readings as **text**. Some are invalid: missing (`\"n/a\"`, `\"\"`), error codes (`\"ERR\"`), or physically impossible for this station (outside 0–5 m).\n", "\n", "**a)** Write `parse_level(text, h_min=0.0, h_max=5.0)` that converts `text` to a float and **raises a `ValueError`** if the value is outside `[h_min, h_max]`. (Note that `float(\"n/a\")` already raises a `ValueError` by itself.)\n", "\n", "**b)** Loop over `raw`, using `try` / `except ValueError` to put valid levels in `valid` and rejected strings in `rejected`. Print the rejected readings and the mean of the valid ones (3 decimals).\n", "\n", "✅ **Check:** 7 valid readings; rejected `['n/a', '', '-1.0', 'ERR', '12.4']`; mean ≈ 2.473 m." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "raw = [\"2.31\", \"2.35\", \"n/a\", \"2.42\", \"\", \"2.58\", \"-1.0\",\n", " \"2.61\", \"ERR\", \"2.55\", \"12.4\", \"2.49\"]\n", "\n", "\n", "def parse_level(text, h_min=0.0, h_max=5.0):\n", " \"\"\"Convert a logger reading to a level (m); reject impossible values.\"\"\"\n", " h = float(text)\n", " if not (h_min <= h <= h_max):\n", " raise ValueError(f\"level out of range: {h}\")\n", " return h\n", "\n", "\n", "valid = []\n", "rejected = []\n", "for r in raw:\n", " try:\n", " valid.append(parse_level(r))\n", " except ValueError:\n", " rejected.append(r)\n", "\n", "print(len(valid), \"valid | rejected:\", rejected)\n", "print(f\"mean level: {sum(valid) / len(valid):.3f} m\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercise 7 — Files: saving the clean data\n", "\n", "**a)** Write the valid readings to a file `levels_clean.txt`, one line per reading in the form `index,level` (index starting at 1). Use a `with` block.\n", "\n", "**b)** Read the file back, split each line on the comma, convert the levels to floats, and print the number of lines and the maximum level.\n", "\n", "✅ **Check:** 7 lines, maximum 2.61 m." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with open(\"levels_clean.txt\", \"w\") as fh:\n", " for i, h in enumerate(valid, start=1):\n", " fh.write(f\"{i},{h}\\n\")\n", "\n", "with open(\"levels_clean.txt\") as fh:\n", " rows = [line.strip().split(\",\") for line in fh if line.strip()]\n", "\n", "levels_read = [float(level) for _, level in rows]\n", "print(len(rows), \"lines | max:\", max(levels_read), \"m\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## You're ready for Week 1\n", "\n", "If you completed these exercises without difficulty, you're ready for NumPy. If some parts were hard, re-read the corresponding sections of the lecture and the [official Python tutorial](https://docs.python.org/3/tutorial/) before the first session." ] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }