codexmolar-mass-vapor-measurement-model.py
molar-mass-vapor-measurement-model.py
"""Draw a vapor-mass measurement schematic into the current directory.
Apparatus inspired by Santa Monica College's teaching method; readout numbers
are synthetic ideal-gas model inputs, not measured experimental results.
Tested with Python 3.14.4, Matplotlib 3.10.7, NumPy 2.3.5.
Run from _media with MPLBACKEND=Agg MPLCONFIGDIR=/tmp/codex-wiki-matplotlib.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, FancyBboxPatch, Polygon, Rectangle
def main():
plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 13})
fig, ax = plt.subplots(figsize=(11, 7.2), dpi=110)
fig.patch.set_facecolor("white")
ax.set(xlim=(0, 1), ylim=(0, 1))
ax.axis("off")
ink, blue, warm = "#243241", "#236aa0", "#b65a26"
ax.text(.5, .956, "Inferring molar mass from vapor", ha="center", fontsize=21, weight="bold")
ax.text(.5, .898, "Original apparatus schematic + synthetic ideal-gas calculation",
ha="center", fontsize=13, color=ink)
# Water surrounds the vessel but does not enter its pinhole.
ax.add_patch(Rectangle((.045, .365), .35, .315, facecolor="#dfedf7", edgecolor="none"))
ax.plot([.045, .045, .395, .395], [.75, .365, .365, .75], color=ink, lw=2)
ax.plot([.045, .395], [.68, .68], color=blue, lw=1.5)
ax.add_patch(Polygon([(.12, .41), (.29, .41), (.30, .44), (.239, .64),
(.239, .745), (.18, .745), (.18, .64), (.108, .44)],
closed=True, facecolor="white", edgecolor=ink, lw=2))
ax.plot([.17, .205], [.75, .75], color="#8a929b", lw=4)
ax.plot([.213, .25], [.75, .75], color="#8a929b", lw=4)
ax.annotate("pinhole foil cap", xy=(.209, .75), xytext=(.075, .812),
fontsize=11, arrowprops={"arrowstyle": "->", "color": ink})
ax.annotate("", xy=(.27, .797), xytext=(.209, .755),
arrowprops={"arrowstyle": "->", "color": blue, "lw": 1.5})
ax.text(.283, .80, "excess vapor exits", fontsize=10)
for x, y in [(.17, .465), (.235, .475), (.2, .54), (.207, .604)]:
ax.add_patch(Circle((x, y), .007, facecolor=blue))
ax.text(.204, .43, "vapor", ha="center", fontsize=11)
# Bath temperature probe next to flask.
ax.plot([.335, .335], [.46, .765], lw=5, color="#adb6be")
ax.plot([.335, .335], [.46, .72], lw=2, color=warm)
ax.add_patch(Circle((.335, .465), .013, facecolor=warm))
ax.text(.045, .332, "Water bath + temperature probe", fontsize=12)
ax.add_patch(Rectangle((.055, .278), .33, .034, facecolor="#ece9e7", edgecolor=ink))
ax.text(.22, .294, "hot plate", ha="center", va="center", fontsize=11)
ax.text(.22, .228, "Cool, dry exterior, reweigh", ha="center", fontsize=12, color=ink)
def panel(y, height, title, lines, face="#eef5fa"):
ax.add_patch(FancyBboxPatch((.505, y), .462, height, boxstyle="round,pad=0.009",
facecolor=face, edgecolor=blue))
ax.text(.527, y+height-.042, title, weight="bold", color=blue, va="center", fontsize=14)
for k, line in enumerate(lines):
size = 11 if line.startswith("These numbers") else 13
ax.text(.527, y+height-.095-.052*k, line, fontsize=size, va="center")
panel(.542, .29, "Independent readouts (chosen values)", [
r"Balance difference: $m=0.3000\ \mathrm{g}$",
r"Bath probe: $T=373.15\ \mathrm{K}$",
r"Barometer: $p=101325\ \mathrm{Pa}$",
r"Water calibration: $V=125.0\ \mathrm{mL}$"])
ax.annotate("", xy=(.735, .466), xytext=(.735, .531),
arrowprops={"arrowstyle": "->", "color": blue, "lw": 2})
panel(.237, .218, "Model inference", [
r"$n=pV/(RT)\approx0.00408234\ \mathrm{mol}$",
r"$M=m/n\approx73.4873\ \mathrm{g\,mol^{-1}}$",
"These numbers are not experimental observations."], face="#f4f7ea")
ax.plot([.04, .967], [.186, .186], color="#cbd5df")
ax.text(.5, .143, "Required: pure vapor • bath equilibrium • mass recovery • ideal gas",
ha="center", fontsize=13, weight="bold", color=ink)
ax.text(.5, .087, r"Residual air fraction $f$: $M_{\rm inferred}=(1-f)M_{\rm true}$ (stated model).",
ha="center", fontsize=12)
ax.text(.5, .039, r"Nonideal pure vapor: $Z=pV/(nRT)$ gives $M_{\rm inferred}=M_{\rm true}/Z$.",
ha="center", fontsize=12)
fig.subplots_adjust(left=.015, right=.985, bottom=.015, top=.99)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white")
plt.close(fig)
if __name__ == "__main__":
main()