第 2 章 · 交互式演示(chapter2.ipynb)
本笔记本承接正文 chapter2.md 中的五段代码:光电效应、玻尔氢原子、类氢轨道可视化、多电子能级示意、电子构型预测。建议在 JupyterLab / VS Code 中运行,并确保已安装:numpy、matplotlib、ipywidgets、plotly、scipy(用于球谐函数与可选平滑)。
§1 光电效应(Photoelectric effect)¶
下面做两件事:一是按光子模型和爱因斯坦方程,用金属钠、入射波长 400 nm 把光子能量、是否发生光电效应、光电子最大动能算一遍;二是画一张示意图,看光子能量和最大动能随频率怎么变。演示里用的功函数是查表近似值,不是精密实验复现。按表中数据,钠的逸出功 (约 )。400 nm 的紫光对应频率约 ,光子能量约 (约 )。因为光子能量大于逸出功,会发生光电效应,光电子最大动能约 (约 )。
可用下方控件切换金属与波长;结果与图像会即时更新。
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>
| <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()
说明(原 code-1):先按所选金属与波长把数算清;图中蓝线为光子能量 ,红线为最大动能,水平虚线为逸出功,竖直灰线为截止频率 。截止频率以下打不出电子、以上动能随频率上升,与爱因斯坦方程一致。
§2 玻尔模型:氢原子能级与单次跃迁¶
按玻尔模型把氢原子 的定态能量算出来,并取跃迁 ,用里德伯公式和能量差两种办法算发射光子的波长、频率和能量,最后画能级示意图。这里用文献常用的里德伯常数近似,略去精细结构。
程序里用的里德伯能量常数下,前五个能级(eV)约为:,,…;对 的发射,,(巴尔末系)。可用下拉框改跃迁。
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()
说明(原 code-2):几条横线表示能级,红箭头表示所选跃迁,旁标波长与 ; 对应巴尔末系中熟悉的青绿线。
§3 类氢原子轨道与 可视化¶
在原子单位()下,单电子本征函数分解为径向与角向部分;概率密度为 。下方为教学可视化:左为 3D 等值面,右为 平面上的 (Viridis)。
用下拉框切换轨道,用滑块调节视野半宽(10–)。
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()
说明(原 code-3):左为 等值面,右为 切面;轨道标签与正文公式对应。
§4 多电子原子:亚层分裂与能级交错(示意)¶
用简化的 定性展示同 下 以及可能的 。 为演示取值。
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()
说明(原 code-4):拖动有效核电荷滑块可定性观察亚层分裂与 / 相对高低如何依赖 ;数值为教学演示,非实验拟合。
§5 基态电子构型与周期表位置(Z ≤ 86)¶
根据原子序数 按填充顺序得到基态构型,并对 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} 周期 | {blk} 区 | {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()
说明(原 code-5):默认 (Se)对应 等;可拖动 查看 Cr、Cu 等反常构型。