codex/past-exam-of-the-physics-of-atmospheres-and-oceans-course-of-the-university-of-oxford/2022-carbon-reservoir-response.py

past-exam-of-the-physics-of-atmospheres-and-oceans-course-of-the-university-of-oxford/2022-carbon-reservoir-response.py
"""Original solution illustration. Tested with Python 3.14, NumPy 2.3.5, Matplotlib 3.10.7.
Run from the desired output directory; this script writes only its PNG basename there.
"""
from pathlib import Path
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
                     "axes.spines.right": False, "figure.facecolor": "white",
                     "axes.facecolor": "white", "savefig.facecolor": "white"})

R=np.array([7.8,7.8,78.]);r,s=.4,.04
J=np.array([[-r/R[0],r/R[0],0],[r/R[1],-(r+s)/R[1],s/R[1]],[0,s/R[2],-s/R[2]]])
S=np.diag(np.sqrt(R))@J@np.diag(1/np.sqrt(R))
lam,Q=np.linalg.eigh(S);lam[abs(lam)<1e-12]=0;weights=Q[0]**2/R[0]
t=np.linspace(0,400,4001)
def response(q,rate=False):
 q=np.maximum(q,0);out=np.zeros_like(q)
 for w,l in zip(weights,lam):
  if l==0:term=q if rate else q*q/2
  else:term=np.expm1(l*q)/l if rate else (np.expm1(l*q)-l*q)/(l*l)
  out+=w*term
 return out
C=.5*(response(t)-2*response(t-100)+response(t-200))
rate=.5*(response(t,True)-2*response(t-100,True)+response(t-200,True))
E=.5*(t-2*np.maximum(t-100,0)+np.maximum(t-200,0))
fig,(ax,bx)=plt.subplots(2,1,figsize=(8,6.1),sharex=True,layout="constrained",height_ratios=[1.5,1])
ax.plot(t,C,lw=2.5,color="#326f9e",label=r"Atmospheric anomaly $C_a$")
ax.axhline(5000/sum(R),color="0.45",ls=":",label="Infinite-time anomaly: 53.4 ppmv")
for year in [200,400]:
 val=np.interp(year,t,C);ax.scatter([year],[val],color="#326f9e",zorder=4)
 ax.annotate(f"Year {year}: {val:.1f} ppmv",(year,val),xytext=((250,300) if year==200 else (year-105,val+32)),arrowprops={"arrowstyle":"->"},fontsize=10)
i=C.argmax();ax.annotate(f"Peak: {C[i]:.1f} ppmv\nat year {t[i]:.0f}",(t[i],C[i]),xytext=(35,293),arrowprops={"arrowstyle":"->"},fontsize=10)
ax.set(ylabel="Concentration anomaly (ppmv)",ylim=(0,340),title="Atmospheric response to a finite emissions pulse")
ax.legend(loc="lower right",frameon=False,fontsize=9)
bx.plot(t,E,color="0.5",ls="--",label="Emissions")
bx.set(ylabel=r"Emissions (GtCO$_2$/yr)",xlabel="Years since emissions begin",xlim=(0,400),ylim=(-21,58))
cx=bx.twinx();cx.spines["right"].set_visible(True)
cx.plot(t,rate,color="#bc4d27",lw=2,label=r"Rate $dC_a/dt$")
cx.axhline(0,color="#bc4d27",lw=.6,alpha=.4)
cx.set(ylabel="Concentration rate (ppmv/yr)",ylim=(-1.5,4.15))
cx.annotate("Maximum rise: 3.27 ppmv/yr",(100,np.interp(100,t,rate)),xytext=(145,3.5),arrowprops={"arrowstyle":"->"},fontsize=9,color="#9a3e20")
bx.legend(loc="upper left",frameon=False,fontsize=9);cx.legend(loc="lower right",frameon=False,fontsize=9)
for axis in (ax,bx):axis.grid(alpha=.15)

fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
            dpi=120, facecolor="white", transparent=False,
            metadata={"Software": "Matplotlib"})
plt.close(fig)