codex/carnot-engine-efficiency.py

carnot-engine-efficiency.py
"""Draw a reversible 600 K / 300 K engine and its temperature-entropy cycle.

Run from _media with MPLBACKEND=Agg MPLCONFIGDIR=/tmp.
Tested with Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
The PNG is written to the current working directory.
"""

import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch, Rectangle
import numpy as np


def main():
    hot, cold, heat_in = 600.0, 300.0, 1000.0
    efficiency = 1.0 - cold / hot
    work = heat_in * efficiency
    heat_out = heat_in - work
    entropy_width = heat_in / hot
    assert np.isclose(heat_in / hot, heat_out / cold)
    assert np.isclose((hot - cold) * entropy_width, work)

    plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
                         "axes.spines.right": False, "figure.facecolor": "white",
                         "axes.facecolor": "white", "savefig.facecolor": "white"})
    fig = plt.figure(figsize=(10.5, 5.2), dpi=120)
    layout = fig.add_gridspec(1, 2, width_ratios=[1.12, 1], wspace=0.30)
    flow = fig.add_subplot(layout[0, 0])
    cycle = fig.add_subplot(layout[0, 1])
    fig.suptitle("Carnot engine: energy conserved, total entropy unchanged", fontsize=15)
    fig.subplots_adjust(left=0.04, right=0.97, bottom=0.19, top=0.82)
    hot_color, cold_color, work_color = "#b44324", "#226a9e", "#247657"
    flow.set_xlim(0, 10)
    flow.set_ylim(0, 10)
    flow.axis("off")
    flow.set_title("One reversible cycle", pad=16)

    def box(x, y, width, height, label, color, fill):
        flow.add_patch(FancyBboxPatch((x, y), width, height,
                       boxstyle="round,pad=0.08,rounding_size=0.15",
                       facecolor=fill, edgecolor=color, linewidth=1.8))
        flow.text(x + width / 2, y + height / 2, label,
                  ha="center", va="center", color=color)

    def arrow(start, end, color):
        flow.annotate("", xy=end, xytext=start,
                      arrowprops={"arrowstyle": "-|>", "color": color,
                                  "lw": 3, "mutation_scale": 18})

    box(0.8, 8.0, 6.6, 1.4, "Hot reservoir: 600 K", hot_color, "#fff1e9")
    box(1.7, 4.0, 3.7, 1.6, "Reversible engine\n" + r"$\Delta U=\Delta S=0$",
        "#3e4754", "#f3f5f7")
    box(0.8, 0.2, 6.6, 1.4, "Cold reservoir: 300 K", cold_color, "#edf5fc")
    arrow((3.55, 7.95), (3.55, 5.65), hot_color)
    flow.text(3.9, 6.8, rf"$Q_h={heat_in:.0f}$ J", color=hot_color, va="center")
    arrow((3.55, 3.95), (3.55, 1.65), cold_color)
    flow.text(3.9, 2.8, rf"$Q_c={heat_out:.0f}$ J", color=cold_color, va="center")
    arrow((5.5, 4.8), (9.45, 4.8), work_color)
    flow.text(7.45, 5.35, rf"$W={work:.0f}$ J", color=work_color, ha="center")
    flow.text(7.45, 4.0, "Work output", color=work_color, ha="center")

    cycle.set_title("Temperature-entropy cycle", pad=16)
    cycle.add_patch(Rectangle((0, cold), entropy_width, hot - cold,
                             facecolor="#e6f2ec", edgecolor="none"))
    vertices = [(0, hot), (entropy_width, hot), (entropy_width, cold),
                (0, cold), (0, hot)]
    colors = [hot_color, "#545d69", cold_color, "#545d69"]
    labels = ["1", "2", "3", "4"]
    for start, end, color in zip(vertices[:-1], vertices[1:], colors):
        cycle.plot([start[0], end[0]], [start[1], end[1]], color=color, lw=2.2)
        middle = np.array(start) * 0.35 + np.array(end) * 0.65
        before = np.array(start) * 0.65 + np.array(end) * 0.35
        cycle.annotate("", xy=middle, xytext=before,
                       arrowprops={"arrowstyle": "-|>", "lw": 2,
                                   "color": color, "mutation_scale": 15})
    for (x, y), label in zip(vertices[:-1], labels):
        cycle.plot(x, y, "o", color="#39424e", ms=4)
        cycle.annotate(label, (x, y), xytext=(-12 if x == 0 else 7,
                       7 if y == hot else -16), textcoords="offset points")
    cycle.text(entropy_width / 2, 450, "Enclosed area\n" + rf"$W={work:.0f}$ J",
               ha="center", va="center", color=work_color)
    cycle.text(entropy_width / 2, 645, "Heat absorbed", ha="center", color=hot_color)
    cycle.text(entropy_width / 2, 248, "Heat rejected", ha="center", color=cold_color)
    cycle.set_xlim(-0.25, entropy_width + 0.3)
    cycle.set_ylim(205, 695)
    cycle.set_yticks([300, 450, 600])
    cycle.set_xticks([0, entropy_width], ["0", "5/3"])
    cycle.set_xlabel(r"Entropy relative to state 1 (J K$^{-1}$)", labelpad=10)
    cycle.set_ylabel("Temperature (K)")
    cycle.grid(axis="y", alpha=0.18)
    fig.text(0.5, 0.06, r"$\eta_C=1-300/600=50\%$     |     "
             r"$\Delta S_h=-5/3$ J/K,  $\Delta S_c=+5/3$ J/K", ha="center")
    fig.savefig("carnot-engine-efficiency.png", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()