codexpast-exam-of-the-short-options-course-of-the-university-of-oxford2021-constrained-brachistochrone.py
past-exam-of-the-short-options-course-of-the-university-of-oxford/2021-constrained-brachistochrone.py
"""Stationary fixed-length arches; Python 3.14, NumPy/Matplotlib per pyproject.toml."""
from pathlib import Path
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
def arch(b, size=16001):
phi = np.linspace(0, 2 * np.pi, size)
s = np.sin(phi / 2)
density = s**2 / (1 - b*s)**3
c = np.trapezoid(density, phi)
dx = (density[1:] + density[:-1]) * np.diff(phi) / (2*c)
x = np.r_[0., np.cumsum(dx)]
y = s**2 / (c*(1-b*s)**2)
length = np.trapezoid(s / (1-b*s)**3, phi) / c
assert b < 1 and c > 0 and np.isclose(x[-1], 1)
assert np.allclose(x + x[::-1], 1) and np.allclose(y, y[::-1])
return x, y, length
def main():
fig, ax = plt.subplots(figsize=(8, 4.8), layout='constrained')
for b, color in [(-4, '#b44e19'), (0, '#176ca4'), (.75, '#23855b')]:
x, y, length = arch(b)
ax.plot(x, -y, lw=2, color=color, label=fr'$b={b:g}$, $L={length:.3f}$')
ax.axhline(0, color='#5f666c', lw=.8)
ax.axvline(.5, color='#9da6ad', ls=':', lw=1)
ax.scatter([0, 1], [0, 0], color='#263238', zorder=4, s=24)
ax.set(xlabel=r'Horizontal coordinate $x$', ylabel=r'Physical height $-y$',
title='Fixed-length brachistochrone: symmetric stationary arches', xlim=(-.025, 1.025))
ax.set_aspect('equal', adjustable='box')
ax.legend(loc='upper center', bbox_to_anchor=(.5, -.16), frameon=False, ncols=3, fontsize=10)
ax.spines[['top', 'right']].set_visible(False)
fig.savefig(Path.cwd() / (Path(__file__).stem + '.png'), dpi=150, facecolor='white', transparent=False)
plt.close(fig)
if __name__ == '__main__':
main()