codexfluid-flow-constriction.py
fluid-flow-constriction.py
"""Draw the section-mean continuity example and its conditional Bernoulli limit.
Run from the desired output directory; writes fluid-flow-constriction.png there.
Tested with Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
The pipe outline is schematic; no local velocity field is computed.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
def main():
radii = np.array([0.010, 0.005])
density = 1000.0
areas = np.pi * radii**2
discharge = areas[0] * 0.20
speeds = discharge / areas
kinetic = density * speeds**2 / 2
pressure_drop = kinetic[1] - kinetic[0]
np.testing.assert_allclose(discharge, 2 * np.pi * 1e-5)
np.testing.assert_allclose(speeds, [0.20, 0.80])
np.testing.assert_allclose(areas * speeds, discharge)
np.testing.assert_allclose(pressure_drop, 300.0)
np.testing.assert_allclose(density * speeds * (2 * radii) / 0.001, [4000, 8000])
np.testing.assert_allclose(density * speeds * (2 * radii) / 0.10, [40, 80])
plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
"axes.spines.right": False})
fig = plt.figure(figsize=(10, 6.4), dpi=130, layout="constrained")
fig.patch.set_facecolor("white")
grid = fig.add_gridspec(2, 2, height_ratios=[1, 1.15])
pipe = fig.add_subplot(grid[0, :])
flow = fig.add_subplot(grid[1, 0])
energy = fig.add_subplot(grid[1, 1])
fig.suptitle("A narrowing pipe: one flow rate, two mean speeds", fontsize=16)
x = np.linspace(0, 8, 501)
fraction = np.clip((x - 3) / 2, 0, 1)
wall = 1 - 0.5 * (3 * fraction**2 - 2 * fraction**3)
pipe.fill_between(x, -wall, wall, color="#e6f2fa")
pipe.plot(x, wall, color="#374854", linewidth=2)
pipe.plot(x, -wall, color="#374854", linewidth=2)
pipe.axhline(0, color="#92a4af", linestyle="--", linewidth=0.8)
for pos, radius, speed in zip([1.2, 6.0], radii, speeds):
half_height = radius / radii[0]
pipe.plot([pos, pos], [-half_height, half_height], color="#72828d",
linestyle=":", linewidth=1.5)
for level in [-0.32, 0, 0.32]:
pipe.annotate("", xy=(pos + 2 * speed, level), xytext=(pos, level),
arrowprops={"arrowstyle": "->", "lw": 2,
"color": "#216ba5"})
pipe.text(1.6, 1.45, "Inlet: radius 10 mm", ha="center", fontsize=12)
pipe.text(6.6, 1.45, "Outlet: radius 5 mm", ha="center", fontsize=12)
pipe.text(1.6, -1.36, r"$\overline{U}_1=0.20\ \mathrm{m/s}$", ha="center")
pipe.text(6.6, -1.36, r"$\overline{U}_2=0.80\ \mathrm{m/s}$", ha="center")
pipe.set(xlim=(-0.2, 8.2), ylim=(-1.65, 1.85))
pipe.set_axis_off()
pipe.set_title("Radius ratio to scale; length schematic; arrows show section means", fontsize=10)
positions = np.array([0, 1])
names = ["Inlet", "Outlet"]
flow.bar(positions, np.full(2, discharge * 1e6), width=0.55, color="#216ba5")
flow.set(xticks=positions, xticklabels=names, ylim=(0, 82),
ylabel=r"Volume per second ($\mathrm{cm^3/s}$)",
title="Continuity: equal discharge")
for pos in positions:
flow.text(pos, discharge * 1e6 + 2, r"$20\pi\approx62.83$", ha="center")
flow.text(0.5, 0.92, r"$Q=A\overline{U}=2\pi\times10^{-5}\ \mathrm{m^3/s}$",
transform=flow.transAxes, ha="center", fontsize=11)
flow.grid(axis="y", alpha=0.15)
flow.set_axisbelow(True)
pressure = np.array([pressure_drop, 0])
energy.bar(positions, pressure, width=0.55, color="#dba348",
label=r"Pressure relative to $p_2$")
energy.bar(positions, kinetic, bottom=pressure, width=0.55, color="#3d8d76",
label=r"Kinetic: $\rho\overline{U}^2/2$")
energy.set(xticks=positions, xticklabels=names, ylim=(0, 370),
ylabel=r"Energy per volume ($\mathrm{J/m^3}=\mathrm{Pa}$)",
title="Ideal Bernoulli: equal totals")
energy.text(0, 150, "300", ha="center", va="center", color="#30230e")
energy.text(0, 340, "300 + 20 = 320", ha="center", fontsize=10)
energy.text(1, 160, "320", ha="center", va="center", color="white")
energy.text(1, 340, "0 + 320 = 320", ha="center", fontsize=10)
energy.grid(axis="y", alpha=0.15)
energy.set_axisbelow(True)
energy.legend(loc="lower center", bbox_to_anchor=(0.5, -0.35), fontsize=10,
frameon=False)
fig.supxlabel("Pressure estimate assumes steady, inviscid flow, equal heights and uniform endpoint speeds.\n"
"The 300 Pa drop is an ideal limit; real pipe losses and profiles require further information.",
fontsize=10)
fig.savefig(Path.cwd() / "fluid-flow-constriction.png", facecolor="white",
transparent=False)
plt.close(fig)
print(f"Q={discharge:.12g} m^3/s; U2={speeds[1]:.2f} m/s; ideal pressure drop={pressure_drop:.0f} Pa")
if __name__ == "__main__":
main()