Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

第 2 章 · 交互式演示(chapter2.ipynb)

本笔记本承接正文 chapter2.md 中的五段代码:光电效应、玻尔氢原子、类氢轨道可视化、多电子能级示意、电子构型预测。建议在 JupyterLab / VS Code 中运行,并确保已安装:numpy、matplotlib、ipywidgets、plotly、scipy(用于球谐函数与可选平滑)。

§1 光电效应(Photoelectric effect)

下面做两件事:一是按光子模型和爱因斯坦方程,用金属钠、入射波长 400 nm 把光子能量、是否发生光电效应、光电子最大动能算一遍;二是画一张示意图,看光子能量和最大动能随频率怎么变。演示里用的功函数是查表近似值,不是精密实验复现。按表中数据,钠的逸出功 Φ=2.36 eV\Phi = 2.36\ \text{eV}(约 3.781×10−19 J3.781\times10^{-19}\ \text{J})。400 nm 的紫光对应频率约 7.495×1014 Hz7.495\times10^{14}\ \text{Hz},光子能量约 3.100 eV3.100\ \text{eV}(约 4.966×10−19 J4.966\times10^{-19}\ \text{J})。因为光子能量大于逸出功,会发生光电效应,光电子最大动能约 0.740 eV0.740\ \text{eV}(约 1.185×10−19 J1.185\times10^{-19}\ \text{J})。

可用下方控件切换金属与波长;结果与图像会即时更新。

import numpy as np
import matplotlib.pyplot as plt
import ipywidgets as widgets
from IPython.display import display

def run_photoelectric_demo():
    """
    运行光电效应交互演示。
    封装在函数中以避免全局变量污染,并采用现代化的 UI 和绘图风格。
    """
    h = 6.626e-34       # 普朗克常数 (J·s)
    c = 2.998e8         # 光速 (m/s)
    eV_to_J = 1.602e-19 # eV 到 J 的转换系数

    WORK_FUNCTIONS_EV = {"Sodium": 2.36, "Potassium": 2.29, "Calcium": 2.87}

    style = dict(description_width="8em")
    
    metal_dd = widgets.Dropdown(
        options=list(WORK_FUNCTIONS_EV.keys()),
        value="Sodium",
        description="Metal",
        **style,
    )
    wl_slider = widgets.FloatSlider(
        value=400.0,
        min=200.0,
        max=800.0,
        step=5.0,
        description="λ (nm)",
        continuous_update=False,
        **style,
    )
    
    out = widgets.Output()
    
    def update(metal, wavelength_nm):
        with out:
            out.clear_output(wait=True)
            phi_eV = WORK_FUNCTIONS_EV[metal]
            phi_J = phi_eV * eV_to_J
            wavelength_m = wavelength_nm * 1e-9
            frequency = c / wavelength_m
            E_photon_J = h * frequency
            E_photon_eV = E_photon_J / eV_to_J
            
            occurs = E_photon_eV >= phi_eV
            KE_max_eV = E_photon_eV - phi_eV if occurs else 0.0
            
            nu_threshold = phi_J / h
            nu_array = np.linspace(0.5 * nu_threshold, 2 * nu_threshold, 200)
            E_photon_array_eV = (h * nu_array) / eV_to_J
            KE_array_eV = np.where(E_photon_array_eV > phi_eV, E_photon_array_eV - phi_eV, 0.0)
            
            # 现代化绘图风格设置
            plt.rcParams.update({
                "font.family": "sans-serif",
                "axes.edgecolor": "#cbd5e1",
                "axes.linewidth": 1.5,
                "axes.labelcolor": "#334155",
                "xtick.color": "#475569",
                "ytick.color": "#475569",
            })
            
            fig, ax1 = plt.subplots(figsize=(9.5, 5.5), dpi=100)
            fig.patch.set_facecolor('#f8fafc')
            ax1.set_facecolor('#ffffff')
            
            ax2 = ax1.twinx()
            
            # 绘制能量曲线
            (l1,) = ax1.plot(nu_array, E_photon_array_eV, color="#3b82f6", lw=2.5, label=r"Photon energy $E=h\nu$")
            (l2,) = ax2.plot(nu_array, KE_array_eV, color="#ef4444", lw=2.5, label=r"Max photoelectron KE $KE_{\max}$")
            
            # 绘制参考线
            ax1.axhline(phi_eV, color="#3b82f6", ls="--", lw=1.5, alpha=0.6, label=r"Work function $\Phi$")
            ax1.axvline(nu_threshold, color="#94a3b8", ls=":", lw=2.5, label=r"Threshold $\nu_0$")
            
            # 标记当前频率点
            ax1.plot([frequency], [E_photon_eV], 'o', color="#1e40af", markersize=9, markeredgecolor='white', markeredgewidth=1.5, zorder=5)
            if occurs:
                ax2.plot([frequency], [KE_max_eV], 'o', color="#991b1b", markersize=9, markeredgecolor='white', markeredgewidth=1.5, zorder=5)
            
            # 坐标轴与标签
            ax1.set_xlabel(r"Frequency $\nu$ (Hz)", fontsize=12, fontweight='bold')
            ax1.set_ylabel("Photon energy (eV)", color="#1d4ed8", fontsize=12, fontweight='bold')
            ax2.set_ylabel("Max photoelectron KE (eV)", color="#b91c1c", fontsize=12, fontweight='bold')
            
            ax1.tick_params(axis="y", labelcolor="#1d4ed8", labelsize=10)
            ax2.tick_params(axis="y", labelcolor="#b91c1c", labelsize=10)
            ax1.tick_params(axis="x", labelsize=10)
            
            ax1.set_title(f"Photoelectric effect — {metal}", fontsize=15, fontweight='bold', color="#0f172a", pad=15)
            ax1.grid(True, color="#f1f5f9", linestyle="-", linewidth=1.5)
            
            # 合并图例
            lines = [l1, l2, ax1.lines[0], ax1.lines[1]]
            labels = [l.get_label() for l in lines]
            ax1.legend(lines, labels, loc="upper left", frameon=True, facecolor='white', edgecolor='#e2e8f0', fontsize=11, framealpha=0.9)
            
            fig.tight_layout()
            
            # 结果摘要面板
            status_color = '#15803d' if occurs else '#dc2626'
            status_bg = '#f0fdf4' if occurs else '#fef2f2'
            status_border = '#22c55e' if occurs else '#ef4444'
            status_text = "Yes (emission)" if occurs else "No"
            
            summary = widgets.HTML(
                value=(
                    f"""<div style="font-family:system-ui,Segoe UI,sans-serif; background-color:{status_bg}; border-left:5px solid {status_border}; padding:16px 20px; border-radius:6px; color:#0f172a; margin-bottom:16px; box-shadow:0 2px 4px rgba(0,0,0,0.05);">
                    <div style="font-size:1.15em; font-weight:700; margin-bottom:10px; color:#334155;">Results (Einstein equation)</div>
                    <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px; font-size:1.05em;">
                        <div><span style="color:#64748b;">Metal:</span> <b>{metal}</b></div>
                        <div><span style="color:#64748b;">λ:</span> <b>{wavelength_nm:.1f} nm</b></div>
                        <div><span style="color:#64748b;">Photon energy E:</span> <b style="color:#1d4ed8;">{E_photon_eV:.3f} eV</b></div>
                        <div><span style="color:#64748b;">Work function Φ:</span> <b>{phi_eV:.2f} eV</b></div>
                        <div style="grid-column:span 2; margin-top:8px; padding-top:12px; border-top:1px solid #cbd5e1; font-size:1.1em;">
                            <span style="color:#64748b;">Photoelectric effect:</span> <span style="color:{status_color}; font-weight:800;">{status_text}</span>
                            &nbsp;|&nbsp; <span style="color:#64748b;">KE<sub>max</sub>:</span> <b style="color:#b91c1c;">{KE_max_eV:.3f} eV</b>
                        </div>
                    </div></div>"""
                )
            )
            display(summary)
            plt.show()
            
    def on_change(change):
        update(metal_dd.value, wl_slider.value)
        
    metal_dd.observe(on_change, names='value')
    wl_slider.observe(on_change, names='value')
    
    ui = widgets.VBox([
        widgets.HTML("<h2 style='margin-bottom:16px; color:#1e293b; font-family:system-ui;'>§1 Photoelectric effect</h2>"),
        widgets.HBox([metal_dd, wl_slider], layout=widgets.Layout(margin='0 0 16px 0')),
        out
    ])
    display(ui)
    update(metal_dd.value, wl_slider.value)

run_photoelectric_demo()
Loading...

说明(原 code-1):先按所选金属与波长把数算清;图中蓝线为光子能量 E=hνE=h\nu,红线为最大动能,水平虚线为逸出功,竖直灰线为截止频率 ν0\nu_0。截止频率以下打不出电子、以上动能随频率上升,与爱因斯坦方程一致。

§2 玻尔模型:氢原子能级与单次跃迁

按玻尔模型把氢原子 n=1∼5n=1\sim5 的定态能量算出来,并取跃迁 ni→nfn_i \to n_f,用里德伯公式和能量差两种办法算发射光子的波长、频率和能量,最后画能级示意图。这里用文献常用的里德伯常数近似,略去精细结构。

程序里用的里德伯能量常数下,前五个能级(eV)约为:n=1:−13.607n=1:-13.607,n=2:−3.402n=2:-3.402,…;对 4→24\to2 的发射,ΔE≈2.551 eV\Delta E \approx 2.551\ \text{eV},λ≈486.0 nm\lambda \approx 486.0\ \text{nm}(巴尔末系)。可用下拉框改跃迁。

import numpy as np
import matplotlib.pyplot as plt
import ipywidgets as widgets
from IPython.display import display

def run_bohr_demo():
    """
    运行玻尔模型氢原子能级与跃迁演示。
    """
    h = 6.626e-34
    c = 2.998e8
    R_inf = 1.097373e7
    R_H_J = 2.179872e-18
    eV_to_J = 1.602e-19

    n_max = 6
    n_values = np.arange(1, n_max + 1)
    E_n_eV = -R_H_J / (n_values ** 2) / eV_to_J

    # 生成常见的跃迁选项 (Lyman, Balmer, Paschen)
    TRANSITIONS = [(n_i, n_f) for n_f in range(1, 4) for n_i in range(n_f+1, n_max+1)]
    series_names = {1: 'Lyman (UV)', 2: 'Balmer (visible)', 3: 'Paschen (IR)'}
    TRANSITIONS_LABELS = [f"n={i} → n={f}  [{series_names[f]}]" for i, f in TRANSITIONS]

    dd = widgets.Dropdown(
        options=list(zip(TRANSITIONS_LABELS, TRANSITIONS)),
        value=(4, 2),
        description="Transition",
        style={"description_width": "5em"},
        layout=widgets.Layout(width='380px')
    )

    out = widgets.Output()

    def update(transition):
        with out:
            out.clear_output(wait=True)
            n_initial, n_final = transition
            
            wavenumber = R_inf * (1 / n_final ** 2 - 1 / n_initial ** 2)
            wavelength_nm = (1 / wavenumber) * 1e9
            delta_E_J = R_H_J * (1 / n_final ** 2 - 1 / n_initial ** 2)
            delta_E_eV = delta_E_J / eV_to_J

            # 现代化绘图风格
            plt.rcParams.update({
                "font.family": "sans-serif",
                "axes.edgecolor": "#cbd5e1",
                "axes.linewidth": 1.5,
            })

            fig, ax = plt.subplots(figsize=(9, 7.5), dpi=100)
            fig.patch.set_facecolor('#f8fafc')
            ax.set_facecolor('#ffffff')
            
            E_ground = E_n_eV[0]
            
            # 绘制能级水平线
            for n in n_values:
                E = E_n_eV[n - 1]
                ax.hlines(E, 0.15, 0.85, color="#475569", lw=2.5, alpha=0.8)
                ax.text(0.87, E, f"n = {n}", va="center", fontsize=12, fontweight='bold', color="#334155")
                dy = 0.18 if n == 1 else 0.08
                ax.text(0.13, E + dy, f"{E:.2f} eV", ha="right", va="bottom", fontsize=11, color="#2563eb", fontweight='600')

            # 绘制跃迁箭头
            E_i = E_n_eV[n_initial - 1]
            E_f = E_n_eV[n_final - 1]
            
            # 根据线系设定箭头颜色
            arrow_color = "#9333ea" if n_final == 1 else ("#10b981" if n_final == 2 else "#ef4444")
            
            ax.annotate(
                "",
                xy=(0.5, E_f),
                xytext=(0.5, E_i),
                arrowprops=dict(arrowstyle="-|>", color=arrow_color, lw=3.5, mutation_scale=22),
            )
            
            # 跃迁信息浮窗
            mid = (E_i + E_f) / 2
            info_text = f"Transition: n={n_initial} → n={n_final}\nλ = {wavelength_nm:.1f} nm\nΔE = {delta_E_eV:.2f} eV"
            ax.text(
                0.54, mid, info_text,
                va="center", fontsize=12, color=arrow_color, fontweight='bold',
                bbox=dict(boxstyle="round,pad=0.6", facecolor="#ffffff", edgecolor=arrow_color, alpha=0.95, lw=1.5)
            )

            ax.set_ylim(E_ground - 1.0, 0.5)
            ax.set_xlim(0, 1)
            ax.set_ylabel("Orbital energy (eV)", fontsize=13, fontweight='bold', color="#334155")
            ax.set_title("Bohr model: H energy levels and one transition", fontsize=16, fontweight='bold', color="#0f172a", pad=20)
            ax.set_xticks([])
            ax.grid(True, axis="y", color="#f1f5f9", linestyle="--", linewidth=1.5)
            
            # 隐藏边框
            ax.spines['top'].set_visible(False)
            ax.spines['right'].set_visible(False)
            ax.spines['bottom'].set_visible(False)
            
            fig.tight_layout()
            plt.show()

    def on_change(change):
        update(change['new'])

    dd.observe(on_change, names='value')
    
    ui = widgets.VBox([
        widgets.HTML("<h2 style='margin-bottom:16px; color:#1e293b; font-family:system-ui;'>§2 Bohr model (H)</h2>"),
        dd, 
        out
    ])
    display(ui)
    update(dd.value)

run_bohr_demo()
Loading...

说明(原 code-2):几条横线表示能级,红箭头表示所选跃迁,旁标波长与 ΔE\Delta E;4→24\to2 对应巴尔末系中熟悉的青绿线。

§3 类氢原子轨道与 ∣ψ∣2|\psi|^2 可视化

在原子单位(a0=1a_0=1)下,单电子本征函数分解为径向与角向部分;概率密度为 ∣ψ∣2|\psi|^2。下方为教学可视化:左为 3D 等值面,右为 z=0z=0 平面上的 ∣ψ∣2|\psi|^2(Viridis)。

用下拉框切换轨道,用滑块调节视野半宽(10–30 a030\ a_0)。

import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import ipywidgets as widgets
from IPython.display import display

def run_orbital_demo():
    """
    运行类氢原子轨道 |ψ|² 交互式三维可视化。
    """
    try:
        from scipy.special import sph_harm_y
    except ImportError:
        from scipy.special import sph_harm as _sph_harm_legacy
        def sph_harm_y(l, m, theta, phi):
            return _sph_harm_legacy(m, l, phi, theta)

    try:
        from scipy.ndimage import gaussian_filter
    except ImportError:
        gaussian_filter = None

    GRID_R = 30.0
    POINTS = 45  # 稍微提高网格精度
    SMOOTH_SIGMA = 1.2
    ISO_SINGLE = 0.055
    ISO_MULTI = 0.042
    DEFAULT_HALF = GRID_R

    def build_cartesian_grid(half_extent=GRID_R, points=POINTS):
        lin = np.linspace(-half_extent, half_extent, points)
        x, y, z = np.meshgrid(lin, lin, lin, indexing="ij")
        r = np.sqrt(x * x + y * y + z * z)
        r = np.maximum(r, 1e-12)
        theta = np.arccos(np.clip(z / r, -1.0, 1.0))
        phi = np.arctan2(y, x)
        return x, y, z, r, theta, phi, lin

    def radial_nl(n, l, r):
        rn = r / max(n, 1e-9)
        return (rn**l) * np.exp(-rn)

    def norm_density(d):
        m = np.max(d)
        return d / m if m > 0 else d

    def psi_complex(n, l, m, x, y, z, r, theta, phi):
        R = radial_nl(n, l, r)
        Y = np.real(sph_harm_y(l, m, theta, phi))
        return R * Y

    def psi_p_cartesian(n, which, x, y, z, r, theta, phi):
        R = radial_nl(n, 1, r)
        if which == "pz": ang = np.cos(theta)
        elif which == "px": ang = np.sin(theta) * np.cos(phi)
        else: ang = np.sin(theta) * np.sin(phi)
        return R * ang

    def psi_d_cartesian(n, name, x, y, z, r, theta, phi):
        R = radial_nl(n, 2, r)
        r2 = x * x + y * y + z * z + 1e-12
        if name == "dxy": ang = x * y / r2
        elif name == "dyz": ang = y * z / r2
        elif name == "dxz": ang = x * z / r2
        elif name == "dx2-y2": ang = (x * x - y * y) / r2
        else: ang = (3 * z * z - r2) / r2
        return R * ang

    def psi_f_cartesian(n, k, x, y, z, r, theta, phi):
        R = radial_nl(n, 3, r)
        r2 = x * x + y * y + z * z + 1e-12
        forms = [
            z * (x * x - y * y), y * (3 * x * x - y * y), x * (3 * y * y - x * x),
            x * y * z, x * (5 * z * z - r2), y * (5 * z * z - r2), z * (5 * z * z - 3 * r2),
        ]
        return R * forms[k]

    def density_from_psi(psi):
        return norm_density(psi**2)

    def smooth_density(vol, sigma=SMOOTH_SIGMA):
        if gaussian_filter is None or sigma <= 0: return vol
        v = gaussian_filter(np.asarray(vol, dtype=float), sigma=sigma, mode="nearest")
        return norm_density(v)

    def solid_colorscale(rgb):
        rr, gg, bb = rgb
        return [[0, f"rgb({rr},{gg},{bb})"], [1, f"rgb({rr},{gg},{bb})"]]

    def make_isosurface(x, y, z, density, iso, opacity, colorscale, name=""):
        return go.Isosurface(
            x=x.flatten(), y=y.flatten(), z=z.flatten(),
            value=density.flatten(), isomin=iso, isomax=iso,
            surface_count=1, colorscale=colorscale,
            caps=dict(x_show=False, y_show=False, z_show=False),
            opacity=opacity, showscale=False, name=name,
            flatshading=False,
            lighting=dict(ambient=0.7, diffuse=0.6, specular=0.1, roughness=0.7, vertexnormalsepsilon=0.025),
            lightposition=dict(x=100, y=200, z=500),
        )

    def slice_z0(density_vol, lin):
        iz = int(np.argmin(np.abs(lin)))
        sl = np.asarray(density_vol[:, :, iz], dtype=float)
        m = np.max(sl)
        return sl / m if m > 0 else sl

    def slice_z0_sum(density_vols, lin):
        iz = int(np.argmin(np.abs(lin)))
        acc = np.zeros_like(density_vols[0][:, :, iz], dtype=float)
        for d in density_vols:
            acc += np.asarray(d[:, :, iz], dtype=float)
        m = np.max(acc)
        return acc / m if m > 0 else acc

    x, y, z, r, theta, phi, lin = build_cartesian_grid()
    scenarios = []

    def add_scenario(btn_label, menu_label, parts_dens_iso_opac, slice_volumes):
        scenarios.append((btn_label, menu_label, parts_dens_iso_opac, slice_volumes))

    for btn_label, menu_label, n, l, m in [
        ("1s", "1s   (n=1, ℓ=0, m=0)", 1, 0, 0),
        ("2s", "2s   (n=2, ℓ=0, m=0)", 2, 0, 0),
        ("2p−1", "2p   (n=2, ℓ=1, m=−1)", 2, 1, -1),
        ("2pz", "2pz (n=2, ℓ=1, m=0)", 2, 1, 0),
        ("2p+1", "2p   (n=2, ℓ=1, m=+1)", 2, 1, 1),
    ]:
        psi = psi_complex(n, l, m, x, y, z, r, theta, phi)
        d = density_from_psi(psi)
        add_scenario(btn_label, menu_label, [(d, solid_colorscale((139, 92, 246)), ISO_SINGLE, 0.65)], [d])

    add_scenario(
        "2p×3", "2p_x, 2p_y, 2p_z  (n=2, ℓ=1)",
        [
            (density_from_psi(psi_p_cartesian(2, "px", x, y, z, r, theta, phi)), solid_colorscale((239, 68, 68)), ISO_MULTI, 0.5),
            (density_from_psi(psi_p_cartesian(2, "py", x, y, z, r, theta, phi)), solid_colorscale((34, 197, 94)), ISO_MULTI, 0.5),
            (density_from_psi(psi_p_cartesian(2, "pz", x, y, z, r, theta, phi)), solid_colorscale((59, 130, 246)), ISO_MULTI, 0.5),
        ],
        [
            np.maximum.reduce([
                psi_p_cartesian(2, "px", x, y, z, r, theta, phi) ** 2,
                psi_p_cartesian(2, "py", x, y, z, r, theta, phi) ** 2,
                psi_p_cartesian(2, "pz", x, y, z, r, theta, phi) ** 2,
            ])
        ],
    )

    for m in range(-2, 3):
        psi = psi_complex(3, 2, m, x, y, z, r, theta, phi)
        d = density_from_psi(psi)
        add_scenario(f"3d{m:+d}", f"3d   (n=3, ℓ=2, m={m:+d})", [(d, solid_colorscale((245, 158, 11)), ISO_SINGLE, 0.65)], [d])

    _dnames = ["dxy", "dyz", "dxz", "dx2-y2", "dz2"]
    _dcolors = [(239, 68, 68), (34, 197, 94), (59, 130, 246), (245, 158, 11), (168, 85, 247)]
    add_scenario(
        "3d×5", "3d real set  (n=3, ℓ=2)",
        [(density_from_psi(psi_d_cartesian(3, _dnames[i], x, y, z, r, theta, phi)), solid_colorscale(_dcolors[i]), ISO_MULTI, 0.45) for i in range(5)],
        [psi_d_cartesian(3, nm, x, y, z, r, theta, phi) ** 2 for nm in _dnames],
    )

    for m in range(-3, 4):
        psi = psi_complex(4, 3, m, x, y, z, r, theta, phi)
        d = density_from_psi(psi)
        add_scenario(f"4f{m:+d}", f"4f   (n=4, ℓ=3, m={m:+d})", [(d, solid_colorscale((14, 165, 233)), ISO_SINGLE, 0.6)], [d])

    _fcols = [(239, 68, 68), (34, 197, 94), (59, 130, 246), (245, 158, 11), (168, 85, 247), (20, 184, 166), (236, 72, 153)]
    add_scenario(
        "4f×7", "4f real set  (n=4, ℓ=3)",
        [(density_from_psi(psi_f_cartesian(4, k, x, y, z, r, theta, phi)), solid_colorscale(_fcols[k]), ISO_MULTI, 0.4) for k in range(7)],
        [psi_f_cartesian(4, k, x, y, z, r, theta, phi) ** 2 for k in range(7)],
    )

    iso_traces, hm_traces, scenario_iso_idx = [], [], []

    for _btn, menu_label, parts, slice_vols in scenarios:
        i0 = len(iso_traces)
        for dens, cs, iso, opac in parts:
            dens_s = smooth_density(dens)
            iso_traces.append(make_isosurface(x, y, z, dens_s, iso, opac, cs, name=menu_label))
        scenario_iso_idx.append(list(range(i0, len(iso_traces))))
        
        slice_sm = [smooth_density(v) for v in slice_vols]
        z2d = slice_z0(slice_sm[0], lin) if len(slice_sm) == 1 else slice_z0_sum(slice_sm, lin)
        hm_traces.append(
            go.Heatmap(
                x=lin, y=lin, z=z2d, zsmooth="best", colorscale="Magma", showscale=True,
                colorbar=dict(title="|ψ|²", len=0.75, thickness=12, x=1.02, tickfont=dict(size=10)),
                hovertemplate="x=%{x:.2f}<br>y=%{y:.2f}<br>|ψ|²≈%{z:.3f}<extra></extra>",
            )
        )

    n_iso = len(iso_traces)
    n_s = len(scenarios)
    fig = make_subplots(
        rows=1, cols=2, specs=[[{"type": "scene"}, {"type": "xy"}]],
        column_widths=[0.6, 0.4], horizontal_spacing=0.08,
    )

    for tr in iso_traces: fig.add_trace(tr, row=1, col=1)
    for tr in hm_traces: fig.add_trace(tr, row=1, col=2)

    n_tot = len(fig.data)
    _default_rng = float(DEFAULT_HALF)

    for i, tr in enumerate(fig.data): tr.visible = False
    for ii in scenario_iso_idx[0]: fig.data[ii].visible = True
    fig.data[n_iso].visible = True

    fig.update_layout(
        title=dict(text="<b>Hydrogenic |ψ|²</b> — " + scenarios[0][1], x=0.5, xanchor="center", y=0.95, font=dict(size=16, color="#1e293b")),
        margin=dict(l=20, r=20, t=80, b=40),
        width=960, height=580,
        paper_bgcolor="#ffffff",
        plot_bgcolor="#ffffff",
        scene=dict(
            xaxis=dict(title="x (a₀)", range=[-_default_rng, _default_rng], showbackground=True, backgroundcolor="#f8fafc", gridcolor="#e2e8f0"),
            yaxis=dict(title="y (a₀)", range=[-_default_rng, _default_rng], showbackground=True, backgroundcolor="#f8fafc", gridcolor="#e2e8f0"),
            zaxis=dict(title="z (a₀)", range=[-_default_rng, _default_rng], showbackground=True, backgroundcolor="#f8fafc", gridcolor="#e2e8f0"),
            aspectmode="cube",
        ),
        annotations=[
            dict(text="<b>3D isosurface</b>", x=0.25, y=1.02, xref="paper", yref="paper", showarrow=False, font=dict(size=13, color="#475569")),
            dict(text="<b>|ψ|² at z = 0</b>", x=0.82, y=1.02, xref="paper", yref="paper", showarrow=False, font=dict(size=13, color="#475569")),
        ],
    )

    fig.update_xaxes(title_text="x (a₀)", range=[-_default_rng, _default_rng], row=1, col=2, gridcolor="#f1f5f9")
    fig.update_yaxes(title_text="y (a₀)", range=[-_default_rng, _default_rng], scaleanchor="x", scaleratio=1, row=1, col=2, gridcolor="#f1f5f9")

    fig_widget = go.FigureWidget(fig)

    scenario_dd = widgets.Dropdown(
        options=[(scenarios[i][0], i) for i in range(n_s)],
        value=0, description="Orbital",
        layout=widgets.Layout(width="250px"), style={"description_width": "5em"},
    )
    half_slider = widgets.FloatSlider(
        value=float(_default_rng), min=10.0, max=30.0, step=2.0,
        description="Half-width (a₀)", readout_format=".0f", continuous_update=False,
        layout=widgets.Layout(width="300px"), style={"description_width": "6em"},
    )

    scenario_index = 0

    def apply_view(h: float):
        fig_widget.update_layout(
            title=dict(text="<b>Hydrogenic |ψ|²</b> — " + scenarios[scenario_index][1]),
            scene=dict(xaxis=dict(range=[-h, h]), yaxis=dict(range=[-h, h]), zaxis=dict(range=[-h, h]))
        )
        fig_widget.update_xaxes(range=[-h, h], row=1, col=2)
        fig_widget.update_yaxes(range=[-h, h], row=1, col=2)

    def apply_scenario(idx: int):
        nonlocal scenario_index
        scenario_index = int(idx)
        vis = [False] * n_tot
        for ii in scenario_iso_idx[scenario_index]: vis[ii] = True
        vis[n_iso + scenario_index] = True
        with fig_widget.batch_update():
            for i, v in enumerate(vis): fig_widget.data[i].visible = v
        apply_view(float(half_slider.value))

    def _on_scenario(change): apply_scenario(change["new"])
    def _on_half(change): apply_view(float(change["new"]))

    scenario_dd.observe(_on_scenario, "value")
    half_slider.observe(_on_half, "value")

    ui = widgets.VBox([
        widgets.HTML("<h2 style='margin-bottom:12px; color:#1e293b; font-family:system-ui;'>§3 Hydrogenic orbitals</h2>"),
        widgets.HBox([scenario_dd, half_slider], layout=widgets.Layout(margin='0 0 16px 0', align_items='center')),
        fig_widget
    ])
    display(ui)
    apply_scenario(0)

run_orbital_demo()
Loading...

说明(原 code-3):左为 ∣ψ∣2|\psi|^2 等值面,右为 z=0z=0 切面;轨道标签与正文公式对应。

§4 多电子原子:亚层分裂与能级交错(示意)

用简化的 E∝−Zeff2/n2E \propto -Z_{\mathrm{eff}}^2/n^2 定性展示同 nn 下 ns<np<ndns<np<nd 以及可能的 E(4s)<E(3d)E(4s)<E(3d)。ZeffZ_{\mathrm{eff}} 为演示取值。

import numpy as np
import matplotlib.pyplot as plt
import ipywidgets as widgets
from IPython.display import display

def run_multielectron_demo():
    """
    运行多电子原子亚层分裂与能级交错演示。
    """
    def E_orb(n, zeff):
        return -(zeff ** 2) / (n ** 2)

    out = widgets.Output()

    def draw(Zs):
        orbitals = ["3s", "3p", "3d", "4s"]
        n_map = {"3s": 3, "3p": 3, "3d": 3, "4s": 4}
        energies = {o: E_orb(n_map[o], Zs[o]) for o in orbitals}
        order = sorted(orbitals, key=lambda k: energies[k])

        plt.rcParams.update({
            "font.family": "sans-serif",
            "axes.edgecolor": "#cbd5e1",
            "axes.linewidth": 1.5,
        })

        fig, ax = plt.subplots(figsize=(9, 6.5), dpi=100)
        fig.patch.set_facecolor('#f8fafc')
        ax.set_facecolor('#ffffff')
        
        yp = {o: energies[o] for o in orbitals}
        
        # 绘制能级
        for o in order:
            y = yp[o]
            ax.hlines(y, 0.15, 0.85, color="#334155", lw=3.5, alpha=0.9)
            
            # 轨道标签
            ax.text(0.12, y, f"{o}\nE ∝ {y:.3f}", ha="right", va="center", fontsize=12, fontweight='bold', color="#0f172a",
                    bbox=dict(boxstyle="round,pad=0.4", facecolor="#e0f2fe", edgecolor="#bae6fd", alpha=0.9))
            
            # Z_eff 标签
            ax.text(0.88, y, f"Z_eff = {Zs[o]:.2f}", ha="left", va="center", fontsize=11, style="italic", color="#64748b")

        # 注释箭头:亚层分裂
        ax.annotate(
            "Subshell splitting\nE(3s) < E(3p) < E(3d)",
            xy=(0.5, yp["3s"]),
            xytext=(0.5, min(energies.values()) - 0.18),
            arrowprops=dict(arrowstyle="->", color="#059669", lw=2),
            ha="center", fontsize=11, fontweight='bold', color="#065f46",
        )
        
        # 注释箭头:能级交错
        mid = (yp["4s"] + yp["3d"]) / 2
        ax.annotate(
            "Orbital crossover\n4s vs 3d relative energy",
            xy=(0.5, yp["4s"]),
            xytext=(0.5, mid + 0.1),
            arrowprops=dict(arrowstyle="->", color="#dc2626", lw=2),
            ha="center", fontsize=11, fontweight='bold', color="#991b1b",
        )

        ax.set_xlim(0, 1)
        vals = list(energies.values())
        ax.set_ylim(min(vals) - 0.3, max(vals) + 0.3)
        ax.set_title("Many-electron atom: subshell splitting & crossover (schematic)", fontsize=16, fontweight='bold', color="#0f172a", pad=20)
        ax.set_ylabel("Relative energy (scaled as −Z_eff²/n²)", fontsize=13, fontweight='bold', color="#334155")
        ax.set_xticks([])
        ax.grid(True, axis="y", color="#f1f5f9", linestyle="--", linewidth=1.5)
        ax.invert_yaxis()
        
        ax.spines['top'].set_visible(False)
        ax.spines['right'].set_visible(False)
        ax.spines['bottom'].set_visible(False)
        
        fig.tight_layout()
        plt.show()

        # 状态摘要
        crossover = energies['4s'] < energies['3d']
        status_color = "#15803d" if crossover else "#64748b"
        status_text = "4s below 3d (crossover)" if crossover else "no crossover"
        
        html = widgets.HTML(
            value=f"""
            <div style='font-family:system-ui; background-color:#f8fafc; border:1px solid #e2e8f0; padding:12px 16px; border-radius:6px; margin-top:12px;'>
                <div style='font-size:1.1em; color:#334155; margin-bottom:8px;'><b>Energy order (low to high):</b> {' < '.join(order)}</div>
                <div style='display:flex; gap:24px; font-size:1.05em;'>
                    <div><span style='color:#64748b;'>E(4s) =</span> <b>{energies['4s']:.4f}</b></div>
                    <div><span style='color:#64748b;'>E(3d) =</span> <b>{energies['3d']:.4f}</b></div>
                    <div><span style='color:#64748b;'>Status:</span> <b style='color:{status_color};'>{status_text}</b></div>
                </div>
            </div>
            """
        )
        return html

    base = {"3s": 2.2, "3p": 1.8, "3d": 1.0, "4s": 1.5}
    sliders = {
        k: widgets.FloatSlider(value=v, min=0.5, max=3.0, step=0.05, description=f"Z_eff ({k})", style={"description_width": "6em"}, layout=widgets.Layout(width='220px'))
        for k, v in base.items()
    }

    def refresh(_=None):
        with out:
            out.clear_output(wait=True)
            Zs = {k: sliders[k].value for k in base}
            h = draw(Zs)
            display(h)

    for s in sliders.values():
        s.observe(refresh, "value")

    ui = widgets.VBox([
        widgets.HTML("<h2 style='margin-bottom:16px; color:#1e293b; font-family:system-ui;'>§4 Many-electron levels</h2>"),
        widgets.HTML("<div style='color:#64748b; margin-bottom:12px;'>Drag sliders for each orbital's effective nuclear charge Z<sub>eff</sub> (qualitative model).</div>"),
        widgets.HBox(list(sliders.values()), layout=widgets.Layout(flex_wrap='wrap', gap='10px', margin='0 0 16px 0')),
        out
    ])
    display(ui)
    refresh()

run_multielectron_demo()
Loading...

说明(原 code-4):拖动有效核电荷滑块可定性观察亚层分裂与 4s4s/3d3d 相对高低如何依赖 ZeffZ_{\mathrm{eff}};数值为教学演示,非实验拟合。

§5 基态电子构型与周期表位置(Z ≤ 86)

根据原子序数 ZZ 按填充顺序得到基态构型,并对 Cr、Cu 等常见反常用实验事实覆盖;输出完整构型、稀有气体简写、周期与区块。

import ipywidgets as widgets
from IPython.display import display

def run_electron_config_demo():
    """
    基态电子构型预测(Z=1–86)与周期表位置。
    与 chapter2.md §2.7 对应:构造原理 + 常见「反常」用实验基态覆盖。
    """
    # Z=1..86 元素符号(IUPAC 顺序)
    ELEMENT_SYMBOLS = (
        "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne",
        "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar",
        "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge", "As", "Se", "Br", "Kr",
        "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe",
        "Cs", "Ba", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu",
        "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn",
    )
    ELEMENT_NAMES_CN = (
        "氢", "氦", "锂", "铍", "硼", "碳", "氮", "氧", "氟", "氖",
        "钠", "镁", "铝", "硅", "磷", "硫", "氯", "氩",
        "钾", "钙", "钪", "钛", "钒", "铬", "锰", "铁", "钴", "镍", "铜", "锌", "镓", "锗", "砷", "硒", "溴", "氪",
        "铷", "锶", "钇", "锆", "铌", "钼", "锝", "钌", "铑", "钯", "银", "镉", "铟", "锡", "锑", "碲", "碘", "氙",
        "铯", "钡", "镧", "铈", "镨", "钕", "钷", "钐", "铕", "钆", "铽", "镝", "钬", "铒", "铥", "镱", "镥",
        "铪", "钽", "钨", "铼", "锇", "铱", "铂", "金", "汞", "铊", "铅", "铋", "钋", "砹", "氡",
    )
    # 本演示中显式覆盖的「反常」基态及简要原因(交换稳定化 / d 全满或半满 等)
    ANOMALY_EXPLAIN = {
        24: "铬 Cr:3d⁵4s¹ — 半满 d 亚层因交换相互作用额外稳定,优于 3d⁴4s²。",
        29: "铜 Cu:3d¹⁰4s¹ — 全满 d¹⁰ 更稳定,4s 仅保留 1 电子。",
        41: "铌 Nb:4d⁴5s¹ — 与 Cr 类似,d/s 占据受能量竞争影响。",
        42: "钼 Mo:4d⁵5s¹ — 半满 d 稳定化。",
        44: "钌 Ru:4d⁷5s¹ — d/s 能量接近时的常见基态之一。",
        45: "铑 Rh:4d⁸5s¹ — 同上。",
        46: "钯 Pd:4d¹⁰5s⁰ — 全满 d¹⁰ 极稳定,5s 空出。",
        47: "银 Ag:4d¹⁰5s¹ — 全满 d + 单 5s。",
        79: "金 Au:5d¹⁰6s¹ — 相对论效应使 6s 收缩,与全满 5d 共同决定基态。",
    }

    def predict_electron_configuration(Z: int) -> dict:
        order = [
            (1, 0, 2), (2, 0, 2), (2, 1, 6), (3, 0, 2), (3, 1, 6), (4, 0, 2),
            (3, 2, 10), (4, 1, 6), (5, 0, 2), (4, 2, 10), (5, 1, 6), (6, 0, 2),
            (4, 3, 14), (5, 2, 10), (6, 1, 6), (7, 0, 2), (5, 3, 14), (6, 2, 10), (7, 1, 6),
        ]
        config = {}
        rem = Z
        for n, l, cap in order:
            if rem <= 0:
                break
            take = min(cap, rem)
            config[(n, l)] = take
            rem -= take

        special = {
            24: {(1, 0): 2, (2, 0): 2, (2, 1): 6, (3, 0): 2, (3, 1): 6, (4, 0): 1, (3, 2): 5},
            29: {(1, 0): 2, (2, 0): 2, (2, 1): 6, (3, 0): 2, (3, 1): 6, (4, 0): 1, (3, 2): 10},
            41: {(1, 0): 2, (2, 0): 2, (2, 1): 6, (3, 0): 2, (3, 1): 6, (4, 0): 2, (3, 2): 10, (4, 1): 6, (5, 0): 1, (4, 2): 4},
            42: {(1, 0): 2, (2, 0): 2, (2, 1): 6, (3, 0): 2, (3, 1): 6, (4, 0): 2, (3, 2): 10, (4, 1): 6, (5, 0): 1, (4, 2): 5},
            44: {(1, 0): 2, (2, 0): 2, (2, 1): 6, (3, 0): 2, (3, 1): 6, (4, 0): 2, (3, 2): 10, (4, 1): 6, (5, 0): 1, (4, 2): 7},
            45: {(1, 0): 2, (2, 0): 2, (2, 1): 6, (3, 0): 2, (3, 1): 6, (4, 0): 2, (3, 2): 10, (4, 1): 6, (5, 0): 1, (4, 2): 8},
            46: {(1, 0): 2, (2, 0): 2, (2, 1): 6, (3, 0): 2, (3, 1): 6, (4, 0): 2, (3, 2): 10, (4, 1): 6, (5, 0): 0, (4, 2): 10},
            47: {(1, 0): 2, (2, 0): 2, (2, 1): 6, (3, 0): 2, (3, 1): 6, (4, 0): 2, (3, 2): 10, (4, 1): 6, (5, 0): 1, (4, 2): 10},
            79: {(1, 0): 2, (2, 0): 2, (2, 1): 6, (3, 0): 2, (3, 1): 6, (4, 0): 2, (3, 2): 10, (4, 1): 6, (5, 0): 2, (4, 2): 10, (5, 1): 6, (6, 0): 2, (4, 3): 14, (5, 2): 10, (6, 1): 1},
        }
        return special.get(Z, config)

    def config_to_strings(config: dict, Z: int):
        letters = ["s", "p", "d", "f"]
        items = sorted(
            [(k, v) for k, v in config.items() if v > 0],
            key=lambda i: (i[0][0] + i[0][1], i[0][0], i[0][1]),
        )
        full = " ".join(f"{n}{letters[l]}<sup>{e}</sup>" for (n, l), e in items)

        noble = {2: "He", 10: "Ne", 18: "Ar", 36: "Kr", 54: "Xe", 86: "Rn"}
        core_Z = max((z for z in noble if z < Z), default=0)
        if core_Z <= 0:
            return full, full, None

        sym = noble[core_Z]
        acc = 0
        val_parts = []
        for (n, l), e in items:
            acc += e
            if acc > core_Z:
                val_parts.append(f"{n}{letters[l]}<sup>{e}</sup>")
        short = f"<b>[{sym}]</b> " + " ".join(val_parts)
        return full, short, sym

    def period_block(Z: int, config: dict):
        fill_order = [
            (1, 0), (2, 0), (2, 1), (3, 0), (3, 1), (4, 0), (3, 2), (4, 1), (5, 0), (4, 2), (5, 1),
            (6, 0), (4, 3), (5, 2), (6, 1), (7, 0), (5, 3), (6, 2), (7, 1),
        ]
        last = next((orb for orb in reversed(fill_order) if config.get(orb, 0) > 0), None)
        if not last:
            return None, "", None

        n, l = last
        if l == 0:
            blk = "s"
            g = config[last]
        elif l == 1:
            blk = "p"
            g = config.get((n, 0), 0) + config[last]
            g = 18 if g == 8 else (g + 10 if g > 3 else g)
        elif l == 2:
            blk = "d"
            g = min(12, max(3, config.get((n, 0), 0) + config[last]))
        else:
            blk = "f"
            g = None

        period = max(n for (n, _), e in config.items() if e > 0)
        return period, blk, g

    out = widgets.Output()
    z_slider = widgets.IntSlider(
        value=34, min=1, max=86, step=1, description="原子序数 Z",
        continuous_update=False, layout=widgets.Layout(width="400px"),
    )

    def show(Z: int):
        cfg = predict_electron_configuration(Z)
        full, short, _ = config_to_strings(cfg, Z)
        period, blk, group = period_block(Z, cfg)
        tot = sum(cfg.values())
        el_sym = ELEMENT_SYMBOLS[Z - 1]
        el_cn = ELEMENT_NAMES_CN[Z - 1]
        anomaly_txt = ANOMALY_EXPLAIN.get(Z, "")
        if anomaly_txt:
            anomaly_html = (
                '<div style="margin-top:14px;padding:12px 14px;background:#fffbeb;border-left:4px solid #f59e0b;'
                'border-radius:6px;color:#78350f;font-size:0.92em;line-height:1.55;">'
                f"<b>反常基态(本表显式覆盖)</b>:{anomaly_txt}</div>"
            )
        else:
            anomaly_html = (
                '<div style="margin-top:14px;padding:12px 14px;background:#f0fdf4;border-left:4px solid #22c55e;'
                'border-radius:6px;color:#14532d;font-size:0.92em;line-height:1.55;">'
                "<b>常规基态</b>:按构造原理填充;若存在其它 d/f 区细微反常,需查光谱学数据或更精细理论。</div>"
            )

        group_text = f"第 {group} 族" if group is not None else "f 区(镧系/锕系)"

        html = f"""
        <div style="font-family:system-ui,-apple-system,sans-serif; background-color:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; padding:20px; margin-top:16px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);">
            <div style="display:flex; flex-wrap:wrap; align-items:center; margin-bottom:16px; padding-bottom:12px; border-bottom:2px solid #e2e8f0; gap:12px;">
                <div style="font-size:2.2em; font-weight:800; color:#1e293b; min-width:72px; text-align:center;">Z={Z}</div>
                <div style="flex:1; min-width:200px;">
                    <div style="font-size:1.35em; font-weight:700; color:#0f172a;">{el_cn}({el_sym})</div>
                    <div style="font-size:1.05em; color:#475569; margin-top:6px;">
                        <b>周期表位置:</b> 第 {period} 周期 &nbsp;|&nbsp; {blk} 区 &nbsp;|&nbsp; {group_text}
                    </div>
                </div>
            </div>
            <div style="margin-bottom:12px;">
                <div style="color:#64748b; font-size:0.9em; font-weight:bold; text-transform:uppercase; letter-spacing:1px; margin-bottom:4px;">简化电子构型</div>
                <div style="font-size:1.45em; font-family:ui-monospace,Consolas,monospace; color:#0f172a; background-color:#ffffff; padding:10px 16px; border-radius:6px; border:1px solid #cbd5e1;">{short}</div>
            </div>
            <div style="margin-bottom:8px;">
                <div style="color:#64748b; font-size:0.9em; font-weight:bold; text-transform:uppercase; letter-spacing:1px; margin-bottom:4px;">完整电子构型</div>
                <div style="font-size:1.05em; font-family:ui-monospace,Consolas,monospace; color:#334155; background-color:#ffffff; padding:8px 16px; border-radius:6px; border:1px solid #cbd5e1;">{full}</div>
            </div>
            <div style="font-size:0.88em; color:#64748b;">电子数校验:{tot}(应与 Z={Z} 一致)</div>
            {anomaly_html}
        </div>
        """

        with out:
            out.clear_output(wait=True)
            display(widgets.HTML(html))

    def _on(change):
        show(change["new"])

    z_slider.observe(_on, names="value")

    ui = widgets.VBox([
        widgets.HTML(
            "<h2 style='margin-bottom:8px; color:#1e293b; font-family:system-ui;'>基态电子构型与周期表位置</h2>"
            "<p style='color:#64748b; margin:0 0 12px 0; font-size:0.95em;'>与 <code>chapter2.md</code> §1.7 配套:拖动 Z 查看中文名、符号、构型及是否属于本演示收录的「反常」基态。</p>"
        ),
        z_slider,
        out,
    ])
    display(ui)
    show(z_slider.value)


run_electron_config_demo()

Loading...

说明(原 code-5):默认 Z=34Z=34(Se)对应 [Ar] 4s23d104p4[\mathrm{Ar}]\,4s^23d^{10}4p^4 等;可拖动 ZZ 查看 Cr、Cu 等反常构型。