🎯 学習目標
- モンテカルロ法の原理と重点サンプリングを理解する
- Metropolisアルゴリズムを実装できる
- 2次元Isingモデルのシミュレーションを実行する
- エルゴード性と平衡化の概念を理解する
- 物理量の統計誤差を評価する
- 分子動力学法(Verlet法)の基礎を学ぶ
- Lennard-Jonesポテンシャルと動径分布関数を理解する
- 材料科学への応用(吸着、磁性)を実践する
📖 モンテカルロ法の基礎
モンテカルロ法
確率分布 \(P(x)\) に従うサンプル \(\{x_i\}\) を生成し、期待値を計算:
\[
\langle f \rangle = \int f(x) P(x) dx \approx \frac{1}{N} \sum_{i=1}^N f(x_i)
\]
Importance Sampling(重点サンプリング):
カノニカル分布 \(P(E) = e^{-\beta E} / Z\) からサンプリングして熱力学量を計算。
Markov chain Monte Carlo(MCMC):
- 状態 \(i\) から状態 \(j\) への遷移確率 \(W_{i \to j}\) を定義
- 詳細釣り合い条件: \(P_i W_{i \to j} = P_j W_{j \to i}\)
- 平衡分布に収束することが保証される
Metropolisアルゴリズム
カノニカル集団のサンプリング手法:
- 現在の状態 \(i\) からランダムに新状態 \(j\) を提案
- エネルギー差 \(\Delta E = E_j - E_i\) を計算
- 遷移確率 \(W_{i \to j} = \min(1, e^{-\beta \Delta E})\)
- 確率 \(W\) で状態 \(j\) を受理、\(1 - W\) で棄却
特徴:
- \(\Delta E < 0\): 必ず受理(エネルギー低下)
- \(\Delta E > 0\): \(e^{-\beta \Delta E}\) の確率で受理
- 詳細釣り合いを満たす
💻 例題5.1: 2次元Isingモデルのモンテカルロシミュレーション
2次元Isingモデル
\(L \times L\) 格子上のスピン系:
\[
H = -J \sum_{\langle i,j \rangle} s_i s_j
\]
周期境界条件を適用。Metropolisアルゴリズムでカノニカル分布をサンプリング。
測定物理量:
- 磁化: \(m = \frac{1}{N}\sum_i s_i\)
- エネルギー: \(E = -J \sum_{\langle i,j \rangle} s_i s_j\)
- 比熱: \(C = \beta^2 (\langle E^2 \rangle - \langle E \rangle^2)\)
- 磁化率: \(\chi = \beta N (\langle m^2 \rangle - \langle m \rangle^2)\)
Python実装: 2次元Isingモデル
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
class Ising2D:
def __init__(self, L, T, J=1.0):
"""
2次元Isingモデル
L: 格子サイズ (L x L)
T: 温度
J: 相互作用強度
"""
self.L = L
self.N = L * L
self.T = T
self.J = J
self.beta = 1.0 / T if T > 0 else np.inf
# ランダム初期配置
self.spins = np.random.choice([-1, 1], size=(L, L))
# 統計量
self.energy_history = []
self.magnetization_history = []
def energy(self):
"""系全体のエネルギー"""
E = 0
for i in range(self.L):
for j in range(self.L):
# 最隣接との相互作用(周期境界条件)
S = self.spins[i, j]
neighbors = (
self.spins[(i+1) % self.L, j] +
self.spins[i, (j+1) % self.L]
)
E += -self.J * S * neighbors
return E
def delta_energy(self, i, j):
"""スピン (i,j) を反転させたときのエネルギー変化"""
S = self.spins[i, j]
neighbors = (
self.spins[(i+1) % self.L, j] +
self.spins[(i-1) % self.L, j] +
self.spins[i, (j+1) % self.L] +
self.spins[i, (j-1) % self.L]
)
return 2 * self.J * S * neighbors
def magnetization(self):
"""磁化"""
return np.abs(np.sum(self.spins)) / self.N
def metropolis_step(self):
"""Metropolis 1ステップ(全スピンを1回ずつ試行)"""
for _ in range(self.N):
# ランダムにスピンを選択
i, j = np.random.randint(0, self.L, size=2)
# エネルギー変化
dE = self.delta_energy(i, j)
# Metropolis判定
if dE < 0 or np.random.random() < np.exp(-self.beta * dE):
self.spins[i, j] *= -1 # スピン反転
def simulate(self, steps, equilibration=1000):
"""シミュレーション実行"""
# 平衡化
for _ in range(equilibration):
self.metropolis_step()
# 測定
for step in range(steps):
self.metropolis_step()
if step % 10 == 0: # 10ステップごとに記録
self.energy_history.append(self.energy())
self.magnetization_history.append(self.magnetization())
def statistics(self):
"""統計量の計算"""
E_mean = np.mean(self.energy_history)
E_std = np.std(self.energy_history)
M_mean = np.mean(self.magnetization_history)
M_std = np.std(self.magnetization_history)
# 比熱と磁化率
E_array = np.array(self.energy_history)
M_array = np.array(self.magnetization_history)
C = self.beta**2 * (np.mean(E_array**2) - np.mean(E_array)**2) / self.N
chi = self.beta * self.N * (np.mean(M_array**2) - np.mean(M_array)**2)
return {
'E_mean': E_mean / self.N,
'E_std': E_std / self.N,
'M_mean': M_mean,
'M_std': M_std,
'C': C,
'chi': chi
}
# 異なる温度でシミュレーション
L = 20
T_range = np.linspace(1.0, 4.0, 16)
J = 1.0
T_c_onsager = 2 * J / np.log(1 + np.sqrt(2)) # Onsager解析解
results = []
for T in T_range:
print(f"Temperature T = {T:.2f}...")
ising = Ising2D(L, T, J)
ising.simulate(steps=5000, equilibration=1000)
stats = ising.statistics()
stats['T'] = T
results.append(stats)
# 可視化
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# エネルギー
ax1 = axes[0, 0]
T_vals = [r['T'] for r in results]
E_vals = [r['E_mean'] for r in results]
E_err = [r['E_std'] for r in results]
ax1.errorbar(T_vals, E_vals, yerr=E_err, fmt='o-', linewidth=2, markersize=6)
ax1.axvline(T_c_onsager, color='r', linestyle='--', linewidth=2, label=f'T_c = {T_c_onsager:.2f}')
ax1.set_xlabel('Temperature T')
ax1.set_ylabel('Energy per spin ⟨E⟩/N')
ax1.set_title('エネルギーの温度依存性')
ax1.legend()
ax1.grid(True, alpha=0.3)
# 磁化
ax2 = axes[0, 1]
M_vals = [r['M_mean'] for r in results]
M_err = [r['M_std'] for r in results]
ax2.errorbar(T_vals, M_vals, yerr=M_err, fmt='o-', linewidth=2, markersize=6, color='blue')
ax2.axvline(T_c_onsager, color='r', linestyle='--', linewidth=2, label=f'T_c = {T_c_onsager:.2f}')
ax2.set_xlabel('Temperature T')
ax2.set_ylabel('Magnetization ⟨|m|⟩')
ax2.set_title('磁化の温度依存性')
ax2.legend()
ax2.grid(True, alpha=0.3)
# 比熱
ax3 = axes[1, 0]
C_vals = [r['C'] for r in results]
ax3.plot(T_vals, C_vals, 'o-', linewidth=2, markersize=6, color='green')
ax3.axvline(T_c_onsager, color='r', linestyle='--', linewidth=2, label=f'T_c = {T_c_onsager:.2f}')
ax3.set_xlabel('Temperature T')
ax3.set_ylabel('Specific heat C')
ax3.set_title('比熱(T_c でピーク)')
ax3.legend()
ax3.grid(True, alpha=0.3)
# 磁化率
ax4 = axes[1, 1]
chi_vals = [r['chi'] for r in results]
ax4.plot(T_vals, chi_vals, 'o-', linewidth=2, markersize=6, color='purple')
ax4.axvline(T_c_onsager, color='r', linestyle='--', linewidth=2, label=f'T_c = {T_c_onsager:.2f}')
ax4.set_xlabel('Temperature T')
ax4.set_ylabel('Susceptibility χ')
ax4.set_title('磁化率(T_c でピーク)')
ax4.legend()
ax4.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('stat_mech_ising_mc_simulation.png', dpi=300, bbox_inches='tight')
plt.show()
# 数値結果
print("\n=== 2次元Isingモデル モンテカルロシミュレーション ===\n")
print(f"格子サイズ: {L} × {L}")
print(f"Onsager臨界温度: T_c = {T_c_onsager:.4f}\n")
print("温度依存性:")
for r in results:
print(f"T = {r['T']:.2f}: E/N = {r['E_mean']:.3f}, M = {r['M_mean']:.3f}, C = {r['C']:.3f}, χ = {r['chi']:.2f}")
💻 例題5.2: 自己相関とエルゴード性
自己相関関数
物理量 \(A(t)\) の自己相関:
\[
C(t) = \langle A(t_0) A(t_0 + t) \rangle - \langle A \rangle^2
\]
相関時間 \(\tau\): \(C(t) \sim e^{-t/\tau}\) の減衰定数。
統計誤差:
\[
\sigma_{\bar{A}} = \frac{\sigma_A}{\sqrt{N_{\text{eff}}}}, \quad N_{\text{eff}} = \frac{N}{2\tau + 1}
\]
相関のあるデータでは有効サンプル数が減少します。
Python実装: 自己相関と統計誤差
import numpy as np
import matplotlib.pyplot as plt
def autocorrelation(data):
"""自己相関関数の計算"""
n = len(data)
mean = np.mean(data)
variance = np.var(data)
# ゼロ平均化
data_centered = data - mean
# 自己相関
autocorr = np.correlate(data_centered, data_centered, mode='full')
autocorr = autocorr[n-1:] / (variance * np.arange(n, 0, -1))
return autocorr
def correlation_time(autocorr):
"""相関時間の推定(指数減衰フィット)"""
# 最初のゼロ交差または十分小さくなる点を探す
for i, val in enumerate(autocorr):
if val < np.exp(-1) or val < 0:
return i
return len(autocorr) // 2
# 2次元Isingモデルでの自己相関
L = 20
temperatures = [1.5, T_c_onsager, 3.0]
colors = ['blue', 'red', 'green']
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 異なる温度での自己相関
ax1 = axes[0, 0]
for T, color in zip(temperatures, colors):
ising = Ising2D(L, T, J=1.0)
ising.simulate(steps=10000, equilibration=1000)
# 磁化の自己相関
autocorr = autocorrelation(np.array(ising.magnetization_history))
tau = correlation_time(autocorr)
ax1.plot(autocorr[:100], color=color, linewidth=2,
label=f'T = {T:.2f}, τ ≈ {tau}')
ax1.set_xlabel('Time lag (MC steps)')
ax1.set_ylabel('Autocorrelation C(t)')
ax1.set_title('磁化の自己相関')
ax1.legend()
ax1.grid(True, alpha=0.3)
ax1.set_yscale('log')
# エルゴード性の検証(時間平均 vs アンサンブル平均)
ax2 = axes[0, 1]
T_test = 2.5
num_runs = 20
time_averages = []
for run in range(num_runs):
ising = Ising2D(L, T_test, J=1.0)
ising.simulate(steps=5000, equilibration=1000)
time_avg = np.mean(ising.magnetization_history)
time_averages.append(time_avg)
ax2.hist(time_averages, bins=15, color='skyblue', edgecolor='black', alpha=0.7)
ax2.axvline(np.mean(time_averages), color='r', linestyle='--', linewidth=2,
label=f'Mean = {np.mean(time_averages):.3f}')
ax2.set_xlabel('Time-averaged magnetization')
ax2.set_ylabel('Frequency')
ax2.set_title(f'エルゴード性検証(T = {T_test}, {num_runs} runs)')
ax2.legend()
ax2.grid(True, alpha=0.3)
# 統計誤差の評価
ax3 = axes[1, 0]
ising_err = Ising2D(L, T_c_onsager, J=1.0)
ising_err.simulate(steps=10000, equilibration=1000)
M_data = np.array(ising_err.magnetization_history)
autocorr_err = autocorrelation(M_data)
tau_err = correlation_time(autocorr_err)
# ブロック平均法
block_sizes = np.logspace(0, 3, 20, dtype=int)
block_errors = []
for block_size in block_sizes:
if block_size > len(M_data) // 2:
break
n_blocks = len(M_data) // block_size
blocks = M_data[:n_blocks * block_size].reshape(n_blocks, block_size)
block_means = np.mean(blocks, axis=1)
block_error = np.std(block_means) / np.sqrt(n_blocks)
block_errors.append(block_error)
ax3.semilogx(block_sizes[:len(block_errors)], block_errors, 'o-', linewidth=2, markersize=6)
ax3.axhline(block_errors[-1], color='r', linestyle='--', linewidth=2,
label=f'Converged error ≈ {block_errors[-1]:.4f}')
ax3.set_xlabel('Block size')
ax3.set_ylabel('Standard error')
ax3.set_title('ブロック平均法による統計誤差評価')
ax3.legend()
ax3.grid(True, alpha=0.3)
# 平衡化プロセス
ax4 = axes[1, 1]
ising_eq = Ising2D(L, 2.0, J=1.0)
# 完全に秩序状態から開始
ising_eq.spins = np.ones((L, L))
equilibration_M = []
for step in range(2000):
ising_eq.metropolis_step()
if step % 10 == 0:
equilibration_M.append(ising_eq.magnetization())
ax4.plot(np.arange(len(equilibration_M)) * 10, equilibration_M, 'b-', linewidth=2)
ax4.axhline(np.mean(equilibration_M[-50:]), color='r', linestyle='--', linewidth=2,
label='Equilibrium value')
ax4.set_xlabel('MC steps')
ax4.set_ylabel('Magnetization')
ax4.set_title('平衡化プロセス(秩序→無秩序)')
ax4.legend()
ax4.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('stat_mech_mc_autocorrelation.png', dpi=300, bbox_inches='tight')
plt.show()
print("\n=== 自己相関と統計誤差 ===\n")
print(f"相関時間 τ ≈ {tau_err} MC steps")
print(f"有効サンプル数 N_eff = N / (2τ + 1) = {len(M_data)} / {2*tau_err + 1} ≈ {len(M_data) / (2*tau_err + 1):.0f}")
print(f"\n統計誤差(ブロック平均法): σ ≈ {block_errors[-1]:.4f}")
💻 例題5.3: 分子動力学法の基礎
分子動力学法(Molecular Dynamics)
Newton運動方程式を数値積分:
\[
m \frac{d^2 \mathbf{r}_i}{dt^2} = \mathbf{F}_i = -\nabla_i U(\{\mathbf{r}_j\})
\]
Velocity Verlet法:
- \(\mathbf{r}(t + \Delta t) = \mathbf{r}(t) + \mathbf{v}(t) \Delta t + \frac{1}{2}\mathbf{a}(t) \Delta t^2\)
- \(\mathbf{a}(t + \Delta t) = \mathbf{F}(\mathbf{r}(t + \Delta t)) / m\)
- \(\mathbf{v}(t + \Delta t) = \mathbf{v}(t) + \frac{1}{2}[\mathbf{a}(t) + \mathbf{a}(t + \Delta t)] \Delta t\)
Lennard-Jonesポテンシャル:
\[
U(r) = 4\varepsilon \left[\left(\frac{\sigma}{r}\right)^{12} - \left(\frac{\sigma}{r}\right)^6\right]
\]
- \(\varepsilon\): ポテンシャル井戸の深さ
- \(\sigma\): ゼロ交差距離
Python実装: Lennard-Jones分子動力学
import numpy as np
import matplotlib.pyplot as plt
class LennardJonesMD:
def __init__(self, N, L, T, dt=0.001, epsilon=1.0, sigma=1.0):
"""
Lennard-Jones分子動力学
N: 粒子数
L: ボックスサイズ
T: 温度
dt: 時間ステップ
"""
self.N = N
self.L = L
self.T = T
self.dt = dt
self.epsilon = epsilon
self.sigma = sigma
# 初期配置(FCC格子)
n = int(np.ceil(N**(1/3)))
positions = []
for i in range(n):
for j in range(n):
for k in range(n):
if len(positions) < N:
positions.append([i, j, k])
self.positions = np.array(positions, dtype=float) * (L / n)
# 初期速度(Maxwell分布)
self.velocities = np.random.randn(N, 3) * np.sqrt(T)
# 重心運動量をゼロに
self.velocities -= np.mean(self.velocities, axis=0)
# 履歴
self.energy_history = []
self.temperature_history = []
def apply_pbc(self, r):
"""周期境界条件"""
return r - self.L * np.floor(r / self.L)
def lennard_jones(self, r):
"""Lennard-Jonesポテンシャルと力"""
r_norm = np.linalg.norm(r)
if r_norm < 0.1 * self.sigma:
r_norm = 0.1 * self.sigma # 発散回避
sr6 = (self.sigma / r_norm)**6
potential = 4 * self.epsilon * (sr6**2 - sr6)
force_magnitude = 24 * self.epsilon * (2 * sr6**2 - sr6) / r_norm
force = force_magnitude * r / r_norm
return potential, force
def compute_forces(self):
"""全粒子間の力とポテンシャルエネルギー"""
forces = np.zeros((self.N, 3))
potential = 0
for i in range(self.N):
for j in range(i+1, self.N):
r_ij = self.positions[i] - self.positions[j]
r_ij = self.apply_pbc(r_ij)
U_ij, F_ij = self.lennard_jones(r_ij)
forces[i] += F_ij
forces[j] -= F_ij
potential += U_ij
return forces, potential
def velocity_verlet_step(self):
"""Velocity Verletアルゴリズム"""
# 現在の力
forces, potential = self.compute_forces()
# 位置更新
self.positions += self.velocities * self.dt + 0.5 * forces * self.dt**2
self.positions = self.apply_pbc(self.positions)
# 新しい力
new_forces, new_potential = self.compute_forces()
# 速度更新
self.velocities += 0.5 * (forces + new_forces) * self.dt
# エネルギーと温度
kinetic = 0.5 * np.sum(self.velocities**2)
total_energy = kinetic + new_potential
temperature = 2 * kinetic / (3 * self.N)
return total_energy, temperature
def simulate(self, steps):
"""シミュレーション実行"""
for step in range(steps):
energy, temp = self.velocity_verlet_step()
if step % 10 == 0:
self.energy_history.append(energy)
self.temperature_history.append(temp)
def radial_distribution(self, bins=100):
"""動径分布関数 g(r)"""
r_max = self.L / 2
hist, bin_edges = np.histogram([], bins=bins, range=(0, r_max))
for i in range(self.N):
for j in range(i+1, self.N):
r_ij = self.positions[i] - self.positions[j]
r_ij = self.apply_pbc(r_ij)
r = np.linalg.norm(r_ij)
if r < r_max:
bin_idx = int(r / r_max * bins)
if bin_idx < bins:
hist[bin_idx] += 1
# 規格化
r_bins = (bin_edges[:-1] + bin_edges[1:]) / 2
dr = r_bins[1] - r_bins[0]
shell_volume = 4 * np.pi * r_bins**2 * dr
density = self.N / self.L**3
g_r = hist / (shell_volume * density * self.N)
return r_bins, g_r
# Lennard-Jones MDシミュレーション
N = 64
L = 5.0
T = 1.0
dt = 0.001
steps = 5000
md = LennardJonesMD(N, L, T, dt)
md.simulate(steps)
# 動径分布関数
r_bins, g_r = md.radial_distribution(bins=50)
# 可視化
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# エネルギー保存
ax1 = axes[0, 0]
time = np.arange(len(md.energy_history)) * 10 * dt
ax1.plot(time, md.energy_history, 'b-', linewidth=1)
ax1.set_xlabel('Time')
ax1.set_ylabel('Total Energy')
ax1.set_title('エネルギー保存(Verlet法)')
ax1.grid(True, alpha=0.3)
# 温度揺らぎ
ax2 = axes[0, 1]
ax2.plot(time, md.temperature_history, 'r-', linewidth=1)
ax2.axhline(T, color='k', linestyle='--', linewidth=2, label=f'T_target = {T}')
ax2.set_xlabel('Time')
ax2.set_ylabel('Temperature')
ax2.set_title('温度の時間発展')
ax2.legend()
ax2.grid(True, alpha=0.3)
# 動径分布関数
ax3 = axes[1, 0]
ax3.plot(r_bins, g_r, 'g-', linewidth=2)
ax3.set_xlabel('r / σ')
ax3.set_ylabel('g(r)')
ax3.set_title('動径分布関数')
ax3.grid(True, alpha=0.3)
# 粒子配置(2Dプロジェクション)
ax4 = axes[1, 1]
ax4.scatter(md.positions[:, 0], md.positions[:, 1], s=100, alpha=0.6, c='blue', edgecolors='black')
ax4.set_xlim([0, L])
ax4.set_ylim([0, L])
ax4.set_xlabel('x')
ax4.set_ylabel('y')
ax4.set_title('粒子配置(XY平面)')
ax4.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('stat_mech_lennard_jones_md.png', dpi=300, bbox_inches='tight')
plt.show()
print("\n=== Lennard-Jones 分子動力学 ===\n")
print(f"粒子数: {N}")
print(f"ボックスサイズ: {L:.2f}")
print(f"時間ステップ: {dt}")
print(f"総ステップ数: {steps}")
print(f"\n平均エネルギー: {np.mean(md.energy_history):.4f}")
print(f"平均温度: {np.mean(md.temperature_history):.4f}")
print(f"温度標準偏差: {np.std(md.temperature_history):.4f}")
💻 例題5.4: 材料科学応用 - 吸着シミュレーション
Grand Canonical Monte Carlo(GCMC)
グランドカノニカル集団でのモンテカルロ法:
- 粒子挿入: 確率 \(\min(1, \frac{V}{N+1} e^{\beta\mu} e^{-\beta \Delta E})\) で受理
- 粒子削除: 確率 \(\min(1, \frac{N}{V} e^{-\beta\mu} e^{-\beta \Delta E})\) で受理
- 粒子移動: Metropolis判定
材料への気体吸着をシミュレート可能。
Python実装: GCMC吸着シミュレーション
import numpy as np
import matplotlib.pyplot as plt
class GCMCAdsorption:
def __init__(self, L, mu, T, epsilon_wall=-1.0, sigma=1.0):
"""
Grand Canonical Monte Carlo 吸着シミュレーション
L: ボックスサイズ
mu: 化学ポテンシャル
T: 温度
epsilon_wall: 壁との相互作用
"""
self.L = L
self.mu = mu
self.T = T
self.beta = 1.0 / T
self.epsilon_wall = epsilon_wall
self.sigma = sigma
self.particles = [] # 粒子位置のリスト
self.N_history = []
def wall_energy(self, z):
"""壁との相互作用エネルギー(z方向)"""
# 簡易的なLennard-Jones型壁ポテンシャル
if z < self.sigma:
z = self.sigma
E = self.epsilon_wall * ((self.sigma / z)**12 - (self.sigma / z)**6)
return E
def particle_energy(self, pos):
"""粒子のエネルギー(壁との相互作用のみ)"""
z = pos[2]
return self.wall_energy(z)
def insert_particle(self):
"""粒子挿入試行"""
# ランダム位置
new_pos = np.random.rand(3) * self.L
# エネルギー計算
E_new = self.particle_energy(new_pos)
# 受理確率
N = len(self.particles)
acceptance = min(1, (self.L**3 / (N + 1)) * np.exp(self.beta * (self.mu - E_new)))
if np.random.random() < acceptance:
self.particles.append(new_pos)
return True
return False
def delete_particle(self):
"""粒子削除試行"""
if len(self.particles) == 0:
return False
# ランダムに粒子選択
idx = np.random.randint(len(self.particles))
pos = self.particles[idx]
# エネルギー計算
E_old = self.particle_energy(pos)
# 受理確率
N = len(self.particles)
acceptance = min(1, (N / self.L**3) * np.exp(-self.beta * (self.mu - E_old)))
if np.random.random() < acceptance:
del self.particles[idx]
return True
return False
def move_particle(self):
"""粒子移動試行"""
if len(self.particles) == 0:
return False
idx = np.random.randint(len(self.particles))
old_pos = self.particles[idx].copy()
# ランダム移動
displacement = (np.random.rand(3) - 0.5) * 0.5
new_pos = old_pos + displacement
# 周期境界条件
new_pos = new_pos % self.L
# エネルギー差
dE = self.particle_energy(new_pos) - self.particle_energy(old_pos)
# Metropolis判定
if dE < 0 or np.random.random() < np.exp(-self.beta * dE):
self.particles[idx] = new_pos
return True
return False
def gcmc_step(self):
"""GCMC 1ステップ"""
# ランダムに操作を選択
operation = np.random.choice(['insert', 'delete', 'move'], p=[0.33, 0.33, 0.34])
if operation == 'insert':
self.insert_particle()
elif operation == 'delete':
self.delete_particle()
else:
self.move_particle()
def simulate(self, steps, record_interval=10):
"""シミュレーション実行"""
for step in range(steps):
self.gcmc_step()
if step % record_interval == 0:
self.N_history.append(len(self.particles))
def density_profile(self, bins=50):
"""密度プロファイル(z方向)"""
if len(self.particles) == 0:
return np.linspace(0, self.L, bins), np.zeros(bins)
z_coords = [p[2] for p in self.particles]
hist, bin_edges = np.histogram(z_coords, bins=bins, range=(0, self.L))
z_bins = (bin_edges[:-1] + bin_edges[1:]) / 2
# 密度(粒子数/体積)
bin_volume = self.L**2 * (self.L / bins)
density = hist / bin_volume
return z_bins, density
# 異なる化学ポテンシャルでGCMC
L = 10.0
T = 1.0
epsilon_wall = -2.0
mu_values = [-5.0, -4.0, -3.0, -2.0]
results_gcmc = []
for mu in mu_values:
print(f"Chemical potential μ = {mu:.2f}...")
gcmc = GCMCAdsorption(L, mu, T, epsilon_wall)
gcmc.simulate(steps=10000, record_interval=10)
z_bins, density = gcmc.density_profile(bins=50)
results_gcmc.append({
'mu': mu,
'N_avg': np.mean(gcmc.N_history[-100:]),
'N_history': gcmc.N_history,
'z_bins': z_bins,
'density': density
})
# 可視化
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 粒子数の時間発展
ax1 = axes[0, 0]
colors = ['blue', 'green', 'orange', 'red']
for r, color in zip(results_gcmc, colors):
ax1.plot(r['N_history'], color=color, linewidth=1, alpha=0.7,
label=f"μ = {r['mu']:.1f}")
ax1.set_xlabel('MC steps')
ax1.set_ylabel('Number of particles N')
ax1.set_title('粒子数の時間発展')
ax1.legend()
ax1.grid(True, alpha=0.3)
# 吸着等温線
ax2 = axes[0, 1]
mu_vals = [r['mu'] for r in results_gcmc]
N_avg_vals = [r['N_avg'] for r in results_gcmc]
ax2.plot(mu_vals, N_avg_vals, 'o-', linewidth=2, markersize=8, color='purple')
ax2.set_xlabel('Chemical potential μ')
ax2.set_ylabel('Average number of particles ⟨N⟩')
ax2.set_title('吸着等温線(GCMC)')
ax2.grid(True, alpha=0.3)
# 密度プロファイル
ax3 = axes[1, 0]
for r, color in zip(results_gcmc, colors):
ax3.plot(r['z_bins'], r['density'], color=color, linewidth=2,
label=f"μ = {r['mu']:.1f}")
ax3.set_xlabel('z (distance from wall)')
ax3.set_ylabel('Density ρ(z)')
ax3.set_title('密度プロファイル(壁近傍で吸着)')
ax3.legend()
ax3.grid(True, alpha=0.3)
# 粒子数分布
ax4 = axes[1, 1]
for r, color in zip(results_gcmc, colors):
ax4.hist(r['N_history'][-500:], bins=20, alpha=0.5, color=color,
label=f"μ = {r['mu']:.1f}, ⟨N⟩ = {r['N_avg']:.1f}")
ax4.set_xlabel('Number of particles N')
ax4.set_ylabel('Frequency')
ax4.set_title('粒子数分布(揺らぎ)')
ax4.legend()
ax4.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('stat_mech_gcmc_adsorption.png', dpi=300, bbox_inches='tight')
plt.show()
print("\n=== GCMC 吸着シミュレーション ===\n")
print("結果:")
for r in results_gcmc:
print(f"μ = {r['mu']:.2f}: ⟨N⟩ = {r['N_avg']:.2f}")
📚 まとめ
- モンテカルロ法は確率分布からのサンプリングで平衡物性を計算する手法
- Metropolisアルゴリズムは詳細釣り合い条件を満たし、カノニカル分布をサンプリング
- 2次元IsingモデルのMCシミュレーションで相転移と臨界現象を観察可能
- 自己相関と相関時間は統計誤差評価に重要で、独立サンプル数を決定
- エルゴード性により時間平均とアンサンブル平均が一致
- 分子動力学法はNewton方程式を数値積分し、動的性質を計算
- Velocity Verlet法はエネルギー保存性に優れた時間積分法
- Lennard-Jonesポテンシャルは実在気体・液体の標準モデル
- GCMCは粒子数が変化する吸着・化学反応系のシミュレーションに有効
- 統計力学シミュレーションは材料科学の構造・物性予測に不可欠なツール
💡 演習問題
- Wolffアルゴリズム: 臨界減速を回避するクラスターアルゴリズムを実装し、臨界点近傍での効率を比較せよ。
- 3次元Ising: 3次元Isingモデルをシミュレートし、臨界温度と臨界指数を求めよ。
- Andersen熱浴: Lennard-Jones MDにAndersen熱浴を実装し、温度制御の効果を検証せよ。
- 動径分布関数と構造因子: g(r)から静的構造因子 S(q) を計算し、液体構造を解析せよ。
- 多成分吸着: 2種類の気体の競争吸着をGCMCでシミュレートし、選択吸着を調べよ。
- 磁性材料: Heisenbergスピンモデル(古典的3成分スピン)をシミュレートし、磁化曲線を計算せよ。
🎓 シリーズ完了
おめでとうございます!古典統計力学入門シリーズを完了しました。
ここまでで、統計力学の3つの統計集団(ミクロカノニカル、カノニカル、グランドカノニカル)、
分配関数からの熱力学量導出、相転移理論、そして計算統計力学の基礎を学びました。
次のステップ:
- 量子統計力学: Fermi-Dirac統計、Bose-Einstein統計の詳細、超伝導理論(BCS理論)
- 非平衡統計力学: Boltzmann方程式、線形応答理論、揺動散逸定理
- 計算統計力学の発展: 経路積分モンテカルロ、量子モンテカルロ、第一原理分子動力学
- 材料科学応用: 相図計算、第一原理計算との組み合わせ、機械学習ポテンシャル
統計力学は物質科学の根幹をなす理論体系です。ここで学んだ概念とシミュレーション手法を
実際の研究に活かしてください!