codex/joule-paddle-wheel-calorimeter.py

joule-paddle-wheel-calorimeter.py
"""Draw an original conceptual Joule paddle-wheel apparatus and energy balance.

Tested with Python 3.14, matplotlib 3.10.7, and numpy 2.3.5.
This is not a reconstruction to scale, and contains no experimental data.
"""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import Circle, FancyArrowPatch, FancyBboxPatch, Rectangle


def draw():
    plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 11})
    fig = plt.figure(figsize=(10, 6.8), dpi=120, facecolor="white")
    ax = fig.add_axes((0, 0, 1, 1), facecolor="white")
    ax.set(xlim=(0, 10), ylim=(0, 6.8))
    ax.axis("off")
    ink, blue, orange = "#203447", "#206a9b", "#ad531b"

    def arrow(start, end, color=ink, lw=1.7):
        ax.add_patch(FancyArrowPatch(start, end, arrowstyle="-|>",
                                    mutation_scale=14, linewidth=lw, color=color))

    def label(text, target, pos, align="left"):
        ax.annotate(text, target, xytext=pos, ha=align, va="center", color=ink,
                    arrowprops={"arrowstyle": "-", "color": ink, "lw": 1},
                    bbox={"facecolor": "white", "edgecolor": "none", "pad": 2})

    ax.text(5, 6.43, "Joule's paddle-wheel experiment", ha="center", fontsize=18,
            weight="bold", color=ink)
    ax.text(5, 6.06, "Original explanatory schematic • not to scale • no measured data",
            ha="center", fontsize=10.5, color="#556777")

    # An illustrative pair of weights and external pulleys drive the vertical shaft.
    for x in (1.15, 8.85):
        ax.add_patch(Circle((x, 5.05), 0.27, edgecolor=ink, facecolor="#eeeeee", lw=1.7))
    ax.plot([1.15, 1.15, 4.75, 5.25, 8.85, 8.85],
            [3.75, 5.32, 5.32, 5.32, 5.32, 3.75], color=ink, lw=1.5)
    for x in (0.83, 8.53):
        ax.add_patch(Rectangle((x, 3.10), 0.64, 0.65, facecolor="#e1e5e9", edgecolor=ink, lw=1.5))
        ax.text(x + 0.32, 3.43, "mass", ha="center", va="center", fontsize=10, color=ink)
    arrow((0.52, 3.71), (0.52, 2.67))
    arrow((9.48, 3.71), (9.48, 2.67))
    ax.text(0.39, 3.20, "h", ha="right", fontsize=13, color=ink)
    ax.text(9.61, 3.20, "h", ha="left", fontsize=13, color=ink)
    ax.text(5, 5.68, "Measure mass and fall distance; disconnect drive to rewind",
            ha="center", fontsize=10.5, color=ink)

    # Dashed system boundary includes water, copper vessel, and immersed paddle.
    ax.add_patch(FancyBboxPatch((3.27, 1.89), 3.46, 2.32, boxstyle="round,pad=0.12",
                               edgecolor=blue, facecolor="none", linestyle="--", lw=1.8))
    ax.add_patch(Rectangle((3.52, 2.05), 2.96, 1.95, facecolor="#eef7fc", edgecolor=orange, lw=3))
    ax.plot([3.52, 6.48], [3.74, 3.74], color=blue, lw=1.2)
    ax.text(3.85, 3.47, "water", color=blue)
    ax.plot([5, 5], [2.22, 5.32], color=ink, lw=3)
    ax.plot([5, 5], [4.56, 4.87], color="#bf9164", lw=5)
    for y in (2.62, 3.05):
        ax.add_patch(Rectangle((4.42, y), 1.16, 0.13, facecolor=orange, edgecolor=orange))
        ax.add_patch(Rectangle((3.53, y + 0.19), 0.49, 0.12, facecolor=ink))
        ax.add_patch(Rectangle((5.98, y + 0.19), 0.49, 0.12, facecolor=ink))
    ax.plot([6.13, 6.13], [2.98, 4.68], color="#a3303a", lw=2)
    ax.add_patch(Circle((6.13, 2.96), 0.09, facecolor="#a3303a"))
    label("Thermometer\ninitial / final T", (6.13, 4.48), (7.15, 4.43))
    label("Boxwood segment\nreduces conduction", (5.00, 4.72), (2.87, 4.66), "right")
    label("Stationary vanes\noppose co-rotation", (3.83, 3.30), (2.69, 2.42), "right")
    label("Rotating paddle", (5.33, 2.69), (7.15, 2.51))
    arrow((5.42, 4.50), (5.42, 4.02), blue)
    ax.text(5.71, 4.25, "shaft work", color=blue, fontsize=10)
    arrow((6.61, 3.40), (7.43, 3.40), orange)
    ax.text(7.18, 3.00, "Heat exchange\nwith room", color=orange, ha="left", fontsize=10)
    ax.text(5, 1.62, "Chosen system: water + vessel + immersed paddle", ha="center", color=blue,
            weight="bold", fontsize=11)
    ax.text(5, 1.13,
            r"$NMgh=C_{\rm eff}\Delta T+W_{\rm loss,out}+Q_{\rm room,out}$",
            ha="center", fontsize=16, color=ink)
    ax.text(5, 0.73, "Simplified balance: negligible endpoint motion and elastic storage",
            ha="center", fontsize=10.5, color=ink)
    ax.text(5, 0.31, "Resting controls estimate thermal drift; vessel and paddle also store energy",
            ha="center", fontsize=10.5, color=ink)
    out = Path.cwd() / (Path(__file__).stem + ".png")
    fig.savefig(out, facecolor="white", transparent=False)
    plt.close(fig)
    return out


if __name__ == "__main__":
    print(draw())