codexbuoyancy-floating-and-sinking.py
buoyancy-floating-and-sinking.py
"""Compare floating, neutral and sinking cubes; save an opaque PNG to cwd.
Tested with Python 3.14.4, numpy 2.3.5 and matplotlib 3.10.7.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import numpy as np
rho_water, g, side = 1000.0, 9.81, 0.10
volume = side**3
rho_body = np.array([600.0, 1000.0, 1200.0])
mass = rho_body * volume
weight = mass * g
fraction = rho_body[0] / rho_water
floating_depth = fraction * side
fully_immersed_buoyancy = rho_water * g * volume
buoyancy = np.array([weight[0], fully_immersed_buoyancy, fully_immersed_buoyancy])
assert np.allclose(mass, [0.6, 1.0, 1.2])
assert np.allclose(weight, [5.886, 9.81, 11.772])
assert np.isclose(fraction, 0.6) and np.isclose(floating_depth, 0.06)
assert np.allclose(weight-buoyancy, [0, 0, 1.962])
assert np.isclose(fully_immersed_buoyancy-weight[0], 3.924)
plt.rcParams.update({"font.size": 12, "figure.facecolor": "white", "axes.facecolor": "white"})
blue, orange, ink = "#156a9c", "#b54720", "#223543"
fig, axes = plt.subplots(1, 3, figsize=(10, 5.6), dpi=120)
fig.subplots_adjust(left=0.03, right=0.97, top=0.74, bottom=0.36, wspace=0.24)
fig.suptitle("Equal-size cubes: floating, neutral, sinking", fontsize=18, y=0.96)
fig.text(0.5, 0.875, r"Each side = 0.10 m; water $\rho_f=1000\ \mathrm{kg/m^3}$; $g=9.81\ \mathrm{m/s^2}$",
ha="center", fontsize=13)
fig.text(0.5, 0.84, "Blue arrows: buoyancy upward Orange arrows: weight downward", ha="center", fontsize=11)
for i, (ax, density, fb, mg, title) in enumerate(zip(axes, rho_body, buoyancy, weight,
["Floating at rest", "Fully immersed, neutral", "Fully immersed, released"])):
ax.set_xlim(-0.08, 0.24)
ax.set_ylim(-0.22, 0.10)
ax.axis("off")
ax.set_aspect("equal")
ax.set_title(title, fontsize=12, pad=24)
ax.text(0.5, 1.03, rf"$\rho_{{\mathrm{{body}}}}={density:.0f}\ \mathrm{{kg/m^3}}$",
transform=ax.transAxes, ha="center", fontsize=12, color=ink)
ax.add_patch(Rectangle((-0.08, -0.22), 0.32, 0.22, facecolor="#edf6fb", edgecolor="none"))
ax.plot([-0.08, 0.24], [0, 0], color=blue, lw=2)
bottom = -floating_depth if i == 0 else -0.15
ax.add_patch(Rectangle((0.03, bottom), side, side, facecolor="#f5d5a1", edgecolor=ink, lw=2, zorder=3))
center = bottom + side/2
for x, signed_length, color in [(-0.005, fb*0.009, blue), (0.165, -mg*0.009, orange)]:
ax.annotate("", xy=(x, center+signed_length/2), xytext=(x, center-signed_length/2),
arrowprops={"arrowstyle": "-|>", "lw": 2.8, "color": color, "mutation_scale": 16}, zorder=4)
if i == 0:
ax.annotate("", xy=(0.215, bottom), xytext=(0.215, 0),
arrowprops={"arrowstyle": "|-|", "color": ink, "lw": 1.2})
ax.text(0.228, bottom/2, "0.060 m", fontsize=10, va="center", rotation=90)
ax.text(0.08, -0.16, "60% of volume\nbelow the surface", ha="center", va="center", fontsize=11, color=ink)
else:
ax.text(0.08, -0.205, "Entire volume immersed", ha="center", va="center", fontsize=10, color=ink)
xpos = [0.173, 0.50, 0.827][i]
fig.text(xpos, 0.29, f"Buoyancy: {fb:.3f} N upward", color=blue, fontsize=11, ha="center")
fig.text(xpos, 0.245, f"Weight: {mg:.3f} N downward", color=orange, fontsize=11, ha="center")
result = "Net force = 0" if i < 2 else "Initial net: 1.962 N down"
fig.text(xpos, 0.193, result, color=ink, fontsize=12, ha="center", weight="bold")
fig.text(0.5, 0.115, "A neutral, rigid cube has no vertical restoring force at any fully immersed depth in uniform water.",
ha="center", fontsize=11)
fig.text(0.5, 0.053, "The floating cube, if held fully immersed, needs an extra 3.924 N downward holding force.",
ha="center", fontsize=11)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white", transparent=False)
plt.close(fig)