codexmonod-growth-and-dilution.py
monod-growth-and-dilution.py
"""Plot the stipulated empirical Monod law; save basename PNG to the CWD.
Tested with Python 3.14.4, Matplotlib 3.10.7, NumPy 2.3.5.
Run from _media: MPLCONFIGDIR=/tmp/chemostat-mpl python3 ../monod-growth-and-dilution.py
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
def main():
mu_max, k_s, dilution, feed = 1.0, 1.0, 0.5, 9.0
s = np.linspace(0, 12, 601)
mu = mu_max * s / (k_s + s)
plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
"axes.spines.right": False})
fig, ax = plt.subplots(figsize=(8.8, 4.8), dpi=100, layout="constrained")
fig.patch.set_facecolor("white")
ax.set_facecolor("white")
ax.plot(s, mu, color="#17679b", lw=2.5, label=r"Growth: $\mu(s)=s/(1+s)$")
ax.axhline(mu_max, color="#7b7b7b", ls=":", lw=1.5,
label=r"Limiting rate $\mu_{\max}=1$ h$^{-1}$")
ax.axhline(dilution, color="#c56a16", lw=2, ls="--",
label=r"Dilution $D=0.5$ h$^{-1}$")
ax.vlines(k_s, 0, dilution, color="#c56a16", ls=":", lw=1.5)
ax.scatter([k_s, feed], [dilution, mu_max * feed / (k_s + feed)],
color=["#c56a16", "#17679b"], s=45, zorder=5)
ax.annotate(r"$s_*=K_s=1$ g/L", (k_s, dilution), xytext=(2.0, 0.30),
arrowprops={"arrowstyle": "->", "color": "#c56a16"})
ax.annotate("Feed: 9 g/L permits\n" + r"$\mu(s_{\rm in})=0.9$ h$^{-1}$",
(feed, 0.9), xytext=(6.0, 0.67),
arrowprops={"arrowstyle": "->", "color": "#17679b"})
ax.set(xlim=(0, 12), ylim=(0, 1.12),
xlabel=r"Residual nutrient concentration $s$ (g/L)",
ylabel=r"Growth or dilution rate (h$^{-1}$)",
title="Monod growth: the pump selects a residual nutrient level")
ax.legend(loc="lower right", frameon=True, facecolor="white", framealpha=1)
ax.grid(alpha=0.18)
destination = Path.cwd() / (Path(__file__).stem + ".png")
fig.savefig(destination, facecolor="white", transparent=False)
plt.close(fig)
print(destination)
if __name__ == "__main__":
main()