codex/electron-hole-band-occupation.py

electron-hole-band-occupation.py
"""Original occupation schematic; tested with Python 3.14, matplotlib 3.10.7, numpy 2.3.5.

Run from _media; write the matching PNG to the current working directory.
Circles represent spin-resolved states in the upper panels and localized
occupancies in the lower cartoons, not an empirical band dispersion.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


BLUE = "#246bb1"
ORANGE = "#c45e16"
GRAY = "#d1dae2"
X = np.linspace(0.20, 0.92, 7)


def row(ax, y, occupied):
    ax.plot([0.15, 0.98], [y, y], color=GRAY, lw=2, zorder=1)
    for x, filled in zip(X, occupied, strict=True):
        ax.scatter(x, y, s=165, facecolor=BLUE if filled else "white",
                   edgecolor=BLUE if filled else ORANGE, linewidth=1.8, zorder=3)


def arrow(ax, start, end, color=ORANGE):
    ax.annotate("", xy=end, xytext=start,
                arrowprops={"arrowstyle": "->", "color": color, "lw": 2})


def main():
    full = np.ones(7, dtype=bool)
    empty = np.zeros(7, dtype=bool)
    hole = full.copy()
    hole[3] = False
    promoted = empty.copy()
    promoted[3] = True
    shifted = full.copy()
    shifted[2] = False
    assert full.sum() == hole.sum() + promoted.sum() == 7
    assert (~hole).sum() == (~shifted).sum() == 1
    assert shifted[3] and hole[2]

    plt.rcParams.update({"font.size": 11, "font.family": "DejaVu Sans"})
    fig, axs = plt.subplots(2, 2, figsize=(10, 6.4), facecolor="white")
    fig.subplots_adjust(left=0.06, right=0.98, bottom=0.13, top=0.85,
                        wspace=0.22, hspace=0.58)
    for ax in axs.flat:
        ax.set(xlim=(0, 1.08), ylim=(0, 1.05))
        ax.axis("off")

    ax = axs[0, 0]
    ax.set_title("Unexcited band occupation", loc="left", fontweight="bold")
    row(ax, 0.72, empty)
    row(ax, 0.24, full)
    ax.text(0.16, 0.85, "Conduction band: empty")
    ax.text(0.16, 0.36, "Valence band: full")
    arrow(ax, (0.055, 0.16), (0.055, 0.86), color="#555555")
    ax.text(0.02, 0.52, "Energy", rotation=90, va="center", ha="center")
    ax.text(0.16, 0.02, "7 electrons; no electron hole")

    ax = axs[0, 1]
    ax.set_title("One electron promoted", loc="left", fontweight="bold")
    row(ax, 0.72, promoted)
    row(ax, 0.24, hole)
    ax.text(0.16, 0.85, "Conduction electron: −e")
    ax.text(0.16, 0.36, "Missing valence electron: +e")
    arrow(ax, (X[3], 0.42), (X[3], 0.66))
    ax.text(0.16, 0.02, "6 + 1 = 7 electrons; net pair charge = 0")

    ax = axs[1, 0]
    ax.set_title("Local vacancy moves when an electron fills it", loc="left",
                 fontweight="bold", fontsize=10)
    row(ax, 0.76, hole)
    row(ax, 0.24, shifted)
    arrow(ax, (X[2], 0.91), (X[3], 0.91), color=BLUE)
    ax.text(0.08, 0.94, "electron →", color=BLUE, fontsize=10)
    arrow(ax, (X[3], 0.49), (X[2], 0.49))
    ax.text(0.65, 0.48, "← hole", color=ORANGE, fontsize=10)
    ax.text(0.16, 0.02, "Before and after: 6 electrons, 1 hole")

    ax = axs[1, 1]
    ax.set_title("Recombination fills the hole", loc="left", fontweight="bold")
    row(ax, 0.76, promoted)
    row(ax, 0.24, hole)
    arrow(ax, (X[3], 0.69), (X[3], 0.31), color=BLUE)
    ax.text(0.63, 0.51, "Energy leaves", fontsize=10)
    ax.text(0.16, 0.02, "After: full valence band, empty conduction band")

    fig.suptitle("Electron holes: missing occupation, positive charge, mobile vacancy",
                 fontsize=14, y=0.96)
    fig.text(0.5, 0.055, "Filled blue = occupied   |   Open orange = empty\n"
             "Qualitative schematic; lower panels are local cartoons, not Bloch-state trajectories.",
             ha="center", fontsize=10)
    fig.savefig(Path(__file__).stem + ".png", dpi=120, facecolor="white",
                transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()