codex/past-exam-of-the-quantum-condensed-matter-course-of-the-university-of-cambridge/2006-donor-carrier-concentration.py

past-exam-of-the-quantum-condensed-matter-course-of-the-university-of-cambridge/2006-donor-carrier-concentration.py
"""Charge-neutral donor model with the specified constant band parameters.

Outputs to cwd. Pure NumPy; no SciPy dependency. Densities are in cm^-3.
"""
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt

KB = 1.380649e-23
HBAR = 1.054571817e-34
M_E = 9.1093837139e-31
Q_E = 1.602176634e-19
N_D = 1e15
E_B = 13.605693122994 * 0.3 / 12**2 * Q_E
E_G = 1.12 * Q_E

def densities(temperature):
    t = np.asarray(temperature, dtype=float)
    nc = 2 * (0.3 * M_E * KB * t / (2 * np.pi * HBAR**2))**1.5 / 1e6
    nv = 2 * (0.49 * M_E * KB * t / (2 * np.pi * HBAR**2))**1.5 / 1e6
    intrinsic = np.sqrt(nc * nv) * np.exp(-E_G / (2 * KB * t))
    donor_coefficient = 2 * np.exp(E_B / (KB * t)) / nc
    low = np.maximum(intrinsic, 1e-100)
    high = N_D + intrinsic
    for _ in range(130):
        middle = np.sqrt(low * high)
        residual = middle - intrinsic**2 / middle - N_D / (1 + donor_coefficient * middle)
        low, high = np.where(residual < 0, middle, low), np.where(residual >= 0, middle, high)
    electron = np.sqrt(low * high)
    return electron, intrinsic, nc, nv

def plateau_boundary(ratio, low, high):
    for _ in range(70):
        middle = (low + high) / 2
        if densities(middle)[0] < ratio * N_D:
            low = middle
        else:
            high = middle
    return (low + high) / 2

def main():
    temperature = np.geomspace(15, 1000, 1800)
    electron, intrinsic, nc, _ = densities(temperature)
    x = 1000 / temperature
    freeze = np.sqrt(N_D * nc / 2) * np.exp(-E_B / (2 * KB * temperature))
    lower = plateau_boundary(0.9, 50, 150)
    upper = plateau_boundary(1.1, 550, 700)
    fig, ax = plt.subplots(figsize=(8.0, 4.7), layout="constrained")
    ax.axvspan(1000 / upper, 1000 / lower, alpha=0.12, color="#39956b", label="Within 10% of donor density")
    ax.plot(x, np.log10(electron), color="#216ca4", lw=2.3, label="Charge-neutral electron density")
    hot, cold = temperature >= 600, temperature <= 65
    ax.plot(x[hot], np.log10(intrinsic[hot]), ":", color="#c65029", lw=2, label="Intrinsic limit")
    ax.plot(x[cold], np.log10(freeze[cold]), ":", color="#8552a3", lw=2, label="Freeze-out limit")
    ax.axhline(15, color="#555555", lw=1, ls="--")
    ax.text(35, 15.15, r"$N_D=10^{15}$ cm$^{-3}$", fontsize=10)
    ax.text(1.3, 17.15, "Intrinsic", fontsize=10)
    ax.text(6.8, 15.35, "Extrinsic plateau", ha="center", fontsize=10)
    ax.text(43, 12.7, "Freeze-out", fontsize=10)
    ax.set_xlim(0, 1000 / 15)
    ax.set_ylim(10, 18)
    ax.set_xlabel(r"Inverse temperature $1000\,\mathrm{K}/T$")
    ax.set_ylabel(r"$\log_{10}(n/\mathrm{cm}^{-3})$")
    ax.set_title("Donor ionization and intrinsic excitation")
    ax.grid(alpha=0.2)
    ax.legend(loc="lower left", fontsize=9)
    top = ax.secondary_xaxis("top", functions=(lambda x: 1000 / np.maximum(x, 1e-9), lambda t: 1000 / np.maximum(t, 1e-9)))
    top.set_xticks([300, 100, 50, 25, 15], labels=["300", "100", "50", "25", "15"])
    top.set_xlabel("Temperature (K)")
    fig.savefig(Path(__file__).with_suffix(".png").name, dpi=120, facecolor="white")
    plt.close(fig)

if __name__ == "__main__":
    main()