量子的トンネル効果のシュミレーション

シュレディンガー理論に於ける「量子的トンネル効果」をシミュレーションするpythonのアニメーションプログラムをAI (主にClaude) に依頼して作ったので示しておく.

まず、プログラム作成の元になった B. Thaller, Visual Quantum Mechanics の § 9.1 〜 § 9.4 の和訳を示す:

この文章をAIに入力してプログラムの作成を依頼した. 出力されたpythonプログラムが次である:

# This program is made by AI(claude) at 2026. 10. 28.
#  Step_Scatter_fft_momentum.py に入射波の運動量k0値を入力できるようにしたもの.

"""
階段ポテンシャルによる波束散乱 (PDF §9.4, 式 9.42) のシミュレーション
  上段: 位置の確率密度 |psi(x,t)|^2
  下段: 運動量の確率密度 |psi_hat(k,t)|^2

平均運動量 k0 (E0 = k0^2/2) と階段の高さ V0 を変えて、
  E0 > V0 (古典的には全透過) と E0 < V0 (古典的には全反射)
での反射の様子の違いを比較できる。

(使い方)
  $ conda activate myenv
  $ python Step_Scatter_k0_input.py                # 既定値 (k0=6, V0=12.5)
  $ python Step_Scatter_k0_input.py --k0 3 --v0 12.5
    実行中は k0 スライダー / k0 入力欄 / V0 スライダー で変更可能 (変更すると t=0 から再開)
"""

import argparse
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.widgets import Slider, TextBox, Button

# =========================================================
# コマンドライン引数
# =========================================================
parser = argparse.ArgumentParser()
parser.add_argument('--k0', type=float, default=6.0, help='平均運動量 k0 (E0=k0^2/2)')
parser.add_argument('--v0', type=float, default=12.5, help='階段の高さ V0')
parser.add_argument('--x0', type=float, default=-4.0, help='初期位置')
args, _ = parser.parse_known_args()

x0 = args.x0

# =========================================================
# 数値パラメータ
# =========================================================
emin = 0.01          # エネルギー積分の下限 (0付近の不安定性を避ける)
n_energy = 600       # エネルギー積分の分割数
k_spread = 5.0       # 積分範囲: k0 + k_spread まで (ガウス幅 ~1 に対して十分大)

x_left, x_right, dx = -12.0, 12.0, 0.05
x_grid = np.arange(x_left, x_right + dx, dx)
X = x_grid[:, np.newaxis]

k_plot_min, k_plot_max = -10.0, 10.0
n_fft = 2**14

dt, t_max = 0.01, 4.0   # アニメーションの時間刻みと周期 dt=0.02, t_max=6.0

# =========================================================
# 物理関数
# =========================================================
def k(e):
    return np.sqrt(2 * e)

def k_trans(e, v0):
    # E < V0 のとき純虚数 (指数減衰)
    return np.sqrt(2 * (e - v0 + 0j))

def reflection_coeff(e, v0):
    k1, k2 = k(e), k_trans(e, v0)
    return (k1 - k2) / (k1 + k2)

def transmission_coeff(e, v0):
    k1, k2 = k(e), k_trans(e, v0)
    return 2 * np.sqrt(k1 * k2) / (k1 + k2)

def g_energy(e, k0, x0):
    kv = k(e)
    return (1.0 / np.sqrt(kv)) * (1.0 / np.pi)**0.25 \
        * np.exp(-(kv - k0)**2 / 2.0) * np.exp(-1j * x0 * kv)

# =========================================================
# k0, V0 が決まったときの基底関数・係数をまとめて作る
# =========================================================
def build(k0, v0):
    emax = (k0 + k_spread)**2 / 2
    e_grid = np.linspace(emin, emax, n_energy)
    de = e_grid[1] - e_grid[0]
    E = e_grid[np.newaxis, :]

    K1 = k(E)
    K2 = k_trans(E, v0)
    G = g_energy(E, k0, x0)

    in_wave = (1.0 / np.sqrt(2 * np.pi * K1)) * np.exp(1j * K1 * X)
    re_wave = reflection_coeff(E, v0) * (1.0 / np.sqrt(2 * np.pi * K1)) * np.exp(-1j * K1 * X)
    tr_wave = transmission_coeff(E, v0) * (1.0 / np.sqrt(2 * np.pi * K2)) * np.exp(1j * K2 * X)
    phi_basis = np.where(X < 0, (in_wave + re_wave) * G, tr_wave * G)

    # 反射確率 P_R = ∫|R(E)|^2 |g(E)|^2 dE,  透過確率 P_T = 1 - P_R  (E0 に限った積分なので、全体の規格化 ∫|g|^2 dE で割って比をとる)
    g2 = np.abs(G[0])**2
    norm = np.sum(g2) * de
    R2 = np.abs(reflection_coeff(e_grid, v0))**2
    p_r = np.sum(R2 * g2) * de / norm
    return dict(e_grid=e_grid, de=de, phi=phi_basis, p_r=p_r, p_t=1 - p_r, k0=k0, v0=v0)

# =========================================================
# 運動量表示 (ゼロパディング FFT)
#   psi_hat(k) = 1/sqrt(2π) ∫ psi(x) e^{-ikx} dx
# =========================================================
k_fft = np.fft.fftshift(2 * np.pi * np.fft.fftfreq(n_fft, d=dx))
k_mask = (k_fft >= k_plot_min) & (k_fft <= k_plot_max)
k_axis = k_fft[k_mask]

def momentum_density(psi):
    psi_hat = np.fft.fftshift(np.fft.fft(psi, n=n_fft)) * dx / np.sqrt(2 * np.pi)
    return np.abs(psi_hat[k_mask])**2

# =========================================================
# 描画の準備
# =========================================================
state = dict(sim=build(args.k0, args.v0), t=0.0)

fig, (ax_x, ax_k) = plt.subplots(2, 1, figsize=(10, 8.5))
fig.subplots_adjust(left=0.1, right=0.95, top=0.92, bottom=0.2, hspace=0.35)

# --- 上段 ---
line_x, = ax_x.plot([], [], lw=2, color='blue', label=r'$|\psi(x,t)|^2$')
pot_line, = ax_x.plot([x_left, 0, 0, x_right], [0, 0, 0, 0], color='gray', lw=1.5,
                      label='Potential step (scaled)')
e0_line = ax_x.axhline(0, color='red', ls='--', lw=1, label='E0 (scaled)')
ax_x.set_xlim(x_left, x_right)
ax_x.set_ylim(-0.05, 1.3)
ax_x.set_xlabel('x')
ax_x.set_ylabel('Probability Density')
ax_x.legend(loc='upper left', fontsize=8)
info_text = ax_x.text(0.99, 0.97, '', transform=ax_x.transAxes, ha='right', va='top',
                      fontsize=9, family='monospace',
                      bbox=dict(boxstyle='round', fc='white', ec='gray', alpha=0.9))

# --- 下段 ---
line_k, = ax_k.plot([], [], lw=2, color='green', label=r'$|\hat\psi(k,t)|^2$')
vl_k0p = ax_k.axvline(0, color='red', ls=':', lw=1, label=r'$\pm k_0$')
vl_k0m = ax_k.axvline(0, color='red', ls=':', lw=1)
vl_v0 = ax_k.axvline(0, color='gray', ls='--', lw=1, label=r'$\sqrt{2V_0}$')
vl_kp = ax_k.axvline(0, color='purple', ls=':', lw=1, label=r"$k_0'=\sqrt{k_0^2-2V_0}$")
ax_k.set_xlim(k_plot_min, k_plot_max)
ax_k.set_ylim(-0.02, 1.0)
ax_k.set_xlabel('k')
ax_k.set_ylabel('Momentum Density')
ax_k.legend(loc='upper left', fontsize=8)

def refresh_static():
    """k0, V0 に依存する静的な要素(ポテンシャル・目印の線・説明文)を更新"""
    s = state['sim']
    k0, v0 = s['k0'], s['v0']
    E0 = k0**2 / 2
    scale = 0.8 / max(v0, E0)
    pot_line.set_ydata([0, 0, v0 * scale, v0 * scale])
    e0_line.set_ydata([E0 * scale, E0 * scale])

    vl_k0p.set_xdata([k0, k0])
    vl_k0m.set_xdata([-k0, -k0])
    vl_v0.set_xdata([np.sqrt(2 * v0)] * 2)
    if k0**2 - 2 * v0 > 0:
        kp = np.sqrt(k0**2 - 2 * v0)
        vl_kp.set_xdata([kp, kp])
        vl_kp.set_visible(True)
    else:
        vl_kp.set_visible(False)

    if E0 > v0:
        regime = 'E0 > V0 : classically full transmission'
    elif E0 < v0:
        regime = 'E0 < V0 : classically full reflection'
    else:
        regime = 'E0 = V0'
    info_text.set_text(f"k0={k0:.2f}  E0={E0:.2f}  V0={v0:.2f}\n{regime}\n"
                       f"Reflection prob. R = {s['p_r']:.3f}\n"
                       f"Transmission prob. T = {s['p_t']:.3f}")

refresh_static()

# =========================================================
# ウィジェット (k0 スライダー / k0 入力欄 / V0 スライダー / Restart)
# =========================================================
ax_sk = fig.add_axes([0.15, 0.10, 0.55, 0.03])
ax_sv = fig.add_axes([0.15, 0.05, 0.55, 0.03])
ax_tb = fig.add_axes([0.82, 0.095, 0.10, 0.04])
ax_bt = fig.add_axes([0.82, 0.04, 0.10, 0.04])

sl_k0 = Slider(ax_sk, 'k0', 1.5, 8.0, valinit=args.k0)
sl_v0 = Slider(ax_sv, 'V0', 0.5, 40.0, valinit=args.v0)
tb_k0 = TextBox(ax_tb, 'k0 input ', initial=f'{args.k0:g}')
btn = Button(ax_bt, 'Restart')

def on_param_change(_=None):
    state['sim'] = build(sl_k0.val, sl_v0.val)
    state['t'] = 0.0
    refresh_static()

def on_text_submit(text):
    try:
        val = float(text)
    except ValueError:
        return
    val = min(max(val, sl_k0.valmin), sl_k0.valmax)
    sl_k0.set_val(val)   # -> on_param_change が呼ばれる

def on_restart(_):
    state['t'] = 0.0

sl_k0.on_changed(on_param_change)
sl_v0.on_changed(on_param_change)
tb_k0.on_submit(on_text_submit)
btn.on_clicked(on_restart)

# =========================================================
# アニメーション
# =========================================================
def update(_frame):
    s = state['sim']
    t = state['t']
    # psi(x,t) = ∫ psi(E,x) g(E) e^{-iEt} dE
    psi = s['phi'] @ (np.exp(-1j * s['e_grid'] * t) * s['de'])

    line_x.set_data(x_grid, np.abs(psi)**2)
    line_k.set_data(k_axis, momentum_density(psi))
    fig.suptitle(f"Time t = {t:.2f}")

    state['t'] = t + dt
    if state['t'] > t_max:
        state['t'] = 0.0
    return line_x, line_k

ani = FuncAnimation(fig, update, frames=None, blit=False, interval=40,
                    cache_frame_data=False)

plt.show()

 

プログラムを実際に実行した時の映像は次のようである: