add NumericalMethods.py

This commit is contained in:
2026-05-23 14:59:36 +08:00
parent 28fcbe22dc
commit 0f04630fc0
+331
View File
@@ -0,0 +1,331 @@
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 简谐振子系统参数
k = 1.0 # 劲度系数
m = 1.0 # 质量
omega = np.sqrt(k/m) # 角频率
def potential_energy(x):
"""势能: (1/2)*k*x^2"""
return 0.5 * k * x**2
def kinetic_energy(v):
"""动能: (1/2)*m*v^2"""
return 0.5 * m * v**2
def total_energy(x, v):
"""总能量"""
return potential_energy(x) + kinetic_energy(v)
def acceleration(x):
"""加速度: a = -(k/m)*x"""
return -omega**2 * x
# ============ 四种数值方法 ============
def euler_explicit(x0, v0, dt, n_steps):
"""欧拉显式 (前向欧拉)"""
x = np.zeros(n_steps)
v = np.zeros(n_steps)
t = np.zeros(n_steps)
x[0] = x0
v[0] = v0
t[0] = 0
for i in range(n_steps - 1):
x[i+1] = x[i] + v[i] * dt
v[i+1] = v[i] + acceleration(x[i]) * dt
t[i+1] = t[i] + dt
return t, x, v
def euler_implicit(x0, v0, dt, n_steps):
"""欧拉隐式 (后向欧拉)"""
x = np.zeros(n_steps)
v = np.zeros(n_steps)
t = np.zeros(n_steps)
x[0] = x0
v[0] = v0
t[0] = 0
for i in range(n_steps - 1):
# 解析解:对简谐振子联立方程可直接得出
x[i+1] = (x[i] + v[i] * dt) / (1 + omega**2 * dt**2)
v[i+1] = v[i] + acceleration(x[i+1]) * dt
t[i+1] = t[i] + dt
return t, x, v
def midpoint_method(x0, v0, dt, n_steps):
"""中点差分法 (二阶 Runge-Kutta / 改进欧拉)"""
x = np.zeros(n_steps)
v = np.zeros(n_steps)
t = np.zeros(n_steps)
x[0] = x0
v[0] = v0
t[0] = 0
for i in range(n_steps - 1):
x_mid = x[i] + v[i] * dt / 2
v_mid = v[i] + acceleration(x[i]) * dt / 2
x[i+1] = x[i] + v_mid * dt
v[i+1] = v[i] + acceleration(x_mid) * dt
t[i+1] = t[i] + dt
return t, x, v
def leapfrog_method(x0, v0, dt, n_steps):
"""蛙跳法 (Leapfrog / Störmer-Verlet)
核心思想:位置和速度交错半步更新
v_{n+1/2} = v_{n-1/2} + a(x_n) * dt <- 速度在半整数时刻
x_{n+1} = x_n + v_{n+1/2} * dt <- 位置在整数时刻
启动:用欧拉半步得到 v_{1/2},之后进入交错循环。
输出时将速度还原到整数时刻:v_n = (v_{n-1/2} + v_{n+1/2}) / 2
"""
x = np.zeros(n_steps)
v = np.zeros(n_steps) # 整数时刻速度(仅用于输出/能量计算)
t = np.zeros(n_steps)
x[0] = x0
v[0] = v0
t[0] = 0
# 用欧拉半步初始化 v_{1/2}
v_half = v0 + acceleration(x0) * dt / 2
for i in range(n_steps - 1):
# 更新位置(整数时刻)
x[i+1] = x[i] + v_half * dt
t[i+1] = t[i] + dt
# 更新半步速度(用新位置的加速度)
v_half_new = v_half + acceleration(x[i+1]) * dt
# 整数时刻速度(用于能量计算和输出):取两个半步的平均
v[i+1] = (v_half + v_half_new) / 2
v_half = v_half_new
return t, x, v
def exact_solution(x0, v0, t):
"""精确解"""
A = np.sqrt(x0**2 + (v0 / omega)**2)
phi = np.arctan2(-v0 / omega, x0)
x_exact = A * np.cos(omega * t + phi)
v_exact = -A * omega * np.sin(omega * t + phi)
return x_exact, v_exact
# ============ 参数设置 ============
dt = 0.05 # 时间步长
T_total = 10.0 # 总时间
n_steps = int(T_total / dt) + 1
# 三个初始条件 (x0, v0)
initial_conditions = [
(1.0, 0.0), # 从最大位移释放
(0.0, 1.0), # 从平衡位置给初速度
(0.8, 0.6) # 混合初始条件
]
# 四种方法:(名称, 函数, 线型颜色, alpha)
methods = [
('欧拉显式', euler_explicit, 'r-', 0.8),
('欧拉隐式', euler_implicit, 'b-', 0.8),
('中点差分法', midpoint_method, 'g-', 0.8),
('蛙跳法', leapfrog_method, 'm-', 0.8),
]
colors_ic = ['#e63946', '#457b9d', '#2d6a4f'] # 三种初始条件的颜色
labels_ic = ['从最大位移释放', '平衡位置初速度', '混合条件']
# 四种方法对应的绘图颜色(用于位移/速度时序图)
method_colors = ['r', 'b', 'g', 'm']
print(f"时间步长 dt = {dt}s, 步数 = {n_steps}")
# ============ 计算结果 ============
results = {}
for method_name, method_func, color_style, alpha in methods:
results[method_name] = {}
for idx, (x0, v0) in enumerate(initial_conditions):
t_arr, x_arr, v_arr = method_func(x0, v0, dt, n_steps)
E = total_energy(x_arr, v_arr)
results[method_name][idx] = {
't': t_arr, 'x': x_arr, 'v': v_arr, 'E': E,
'E_initial': E[0], 'E_final': E[-1],
'E_drift': (E[-1] - E[0]) / E[0] * 100
}
# 精确解(高密度时间轴)
exact_t = np.linspace(0, T_total, 1000)
# ============ 图形布局 ============
fig = plt.figure(figsize=(18, 12))
fig.suptitle('简谐振子数值方法四方比较', fontsize=16, fontweight='bold')
gs = fig.add_gridspec(3, 4, hspace=0.40, wspace=0.30)
ax_phase = fig.add_subplot(gs[0, :]) # 第1行全宽:相空间
ax_energy = fig.add_subplot(gs[1, :]) # 第2行全宽:能量演化
ax_traj = fig.add_subplot(gs[2, 0]) # 第3行:位置-时间
ax_velocity = fig.add_subplot(gs[2, 1]) # 第3行:速度-时间
ax_err_bar = fig.add_subplot(gs[2, 2]) # 第3行:能量误差柱状图
ax_animation= fig.add_subplot(gs[2, 3]) # 第3行:相空间动画
# ============ 1. 相空间轨迹 ============
ax_phase.set_title('相空间轨迹比较 (x vs v)', fontsize=13)
ax_phase.set_xlabel('位置 x', fontsize=11)
ax_phase.set_ylabel('速度 v', fontsize=11)
ax_phase.grid(True, alpha=0.3)
ax_phase.axhline(y=0, color='k', linestyle='-', alpha=0.3)
ax_phase.axvline(x=0, color='k', linestyle='-', alpha=0.3)
line_styles_ic = ['-', '--', ':']
method_line_colors = ['r', 'b', 'g', 'm']
for midx, (x0, v0) in enumerate(initial_conditions):
x_exact, v_exact = exact_solution(x0, v0, exact_t)
ax_phase.plot(x_exact, v_exact, 'k-', alpha=0.25, linewidth=1,
label='精确解' if midx == 0 else "")
for mi, (method_name, _, _, _) in enumerate(methods):
res = results[method_name][midx]
ax_phase.plot(res['x'], res['v'],
linestyle=['-', '--', ':', '-.'][midx],
color=method_line_colors[mi], alpha=0.75, linewidth=1.4,
label=method_name if midx == 0 else "")
handles, labels_leg = ax_phase.get_legend_handles_labels()
by_label = dict(zip(labels_leg, handles))
ax_phase.legend(by_label.values(), by_label.keys(),
loc='upper right', fontsize=8, ncol=2)
# ============ 2. 能量演化(归一化) ============
ax_energy.set_title('归一化能量随时间演化 E(t)/E(0)', fontsize=13)
ax_energy.set_xlabel('时间 t (s)', fontsize=11)
ax_energy.set_ylabel('E / E0', fontsize=11)
ax_energy.grid(True, alpha=0.3)
energy_linestyles = ['-', '--', ':', '-.']
for mi, (method_name, _, _, _) in enumerate(methods):
res = results[method_name][0] # 只用第一个初始条件展示能量
E_norm = res['E'] / res['E_initial']
ax_energy.plot(res['t'], E_norm,
linestyle=energy_linestyles[mi],
color=method_line_colors[mi],
linewidth=1.8, alpha=0.9, label=method_name)
ax_energy.axhline(y=1.0, color='k', linestyle='-', alpha=0.5, linewidth=0.8)
ax_energy.set_ylim(0.85, 1.70)
ax_energy.legend(loc='upper left', fontsize=9)
# ============ 3. 位置-时间 (第一初始条件) ============
idx_show = 0
ax_traj.set_title(f'位置-时间 ({labels_ic[idx_show]})', fontsize=11)
ax_traj.set_xlabel('时间 t (s)', fontsize=10)
ax_traj.set_ylabel('位置 x', fontsize=10)
ax_traj.grid(True, alpha=0.3)
x_ex, v_ex = exact_solution(*initial_conditions[idx_show], exact_t)
ax_traj.plot(exact_t, x_ex, 'k-', alpha=0.5, linewidth=1.2, label='精确解')
for mi, (method_name, _, _, _) in enumerate(methods):
res = results[method_name][idx_show]
ax_traj.plot(res['t'], res['x'],
color=method_line_colors[mi], linewidth=1.4,
alpha=0.85, label=method_name)
ax_traj.legend(loc='upper right', fontsize=7)
# ============ 4. 速度-时间 (第一初始条件) ============
ax_velocity.set_title(f'速度-时间 ({labels_ic[idx_show]})', fontsize=11)
ax_velocity.set_xlabel('时间 t (s)', fontsize=10)
ax_velocity.set_ylabel('速度 v', fontsize=10)
ax_velocity.grid(True, alpha=0.3)
ax_velocity.plot(exact_t, v_ex, 'k-', alpha=0.5, linewidth=1.2, label='精确解')
for mi, (method_name, _, _, _) in enumerate(methods):
res = results[method_name][idx_show]
ax_velocity.plot(res['t'], res['v'],
color=method_line_colors[mi], linewidth=1.4,
alpha=0.85, label=method_name)
ax_velocity.legend(loc='upper right', fontsize=7)
# ============ 5. 能量误差柱状图 ============
ax_err_bar.set_title('能量漂移百分比 |dE/E0|*100%', fontsize=11)
ax_err_bar.set_ylabel('漂移 (%)', fontsize=10)
ax_err_bar.grid(True, alpha=0.3, axis='y')
method_names_short = ['显式', '隐式', '中点', '蛙跳']
bar_colors = method_line_colors
drifts = [abs(results[mn][0]['E_drift']) for mn, *_ in methods]
bars = ax_err_bar.bar(method_names_short, drifts, color=bar_colors, alpha=0.8, width=0.5)
for bar, val in zip(bars, drifts):
ax_err_bar.text(bar.get_x() + bar.get_width() / 2,
bar.get_height() + 0.3,
f'{val:.2f}%', ha='center', va='bottom', fontsize=9)
ax_err_bar.set_ylim(0, max(drifts) * 1.2 + 2)
# ============ 6. 动画:四种方法相空间对比 (同一初始条件) ============
ax_animation.set_xlim(-1.5, 1.5)
ax_animation.set_ylim(-1.5, 1.5)
ax_animation.set_title('四种方法同步释放 (相空间)', fontsize=11)
ax_animation.set_xlabel('位置 x', fontsize=10)
ax_animation.set_ylabel('速度 v', fontsize=10)
ax_animation.grid(True, alpha=0.3)
ax_animation.axhline(y=0, color='k', linestyle='-', alpha=0.3)
ax_animation.axvline(x=0, color='k', linestyle='-', alpha=0.3)
anim_points = []
anim_trails = []
anim_idx = 0 # 选第一组初始条件,比较四种方法的同步演化
for mi, (method_name, _, _, _) in enumerate(methods):
pt, = ax_animation.plot([], [], 'o', color=method_line_colors[mi],
markersize=7, label=method_name)
tr, = ax_animation.plot([], [], '-', color=method_line_colors[mi], alpha=0.35, linewidth=1)
anim_points.append(pt)
anim_trails.append(tr)
x_anim_exact, v_anim_exact = exact_solution(*initial_conditions[anim_idx], exact_t)
ax_animation.plot(x_anim_exact, v_anim_exact, 'k-', alpha=0.2, linewidth=0.8, label='精确解')
ax_animation.legend(loc='upper right', fontsize=8)
def animate(frame):
for mi, (method_name, _, _, _) in enumerate(methods):
res = results[method_name][anim_idx]
if frame < len(res['x']):
anim_points[mi].set_data([res['x'][frame]], [res['v'][frame]])
start = max(0, frame - 50)
anim_trails[mi].set_data(res['x'][start:frame+1], res['v'][start:frame+1])
return anim_points + anim_trails
anim = FuncAnimation(fig, animate, frames=n_steps, interval=20, blit=True)
# ============ 打印能量误差统计 ============
print("\n" + "=" * 65)
print("能量守恒误差分析 (相对漂移百分比,初始条件:从最大位移释放)")
print("=" * 65)
for method_name, _, _, _ in methods:
res0 = results[method_name][0]
print(f" {method_name:10s}{res0['E_drift']:+.4f}%")
print("\n" + "=" * 65)
print("所有初始条件下的能量漂移")
print("=" * 65)
for method_name, _, _, _ in methods:
print(f"\n{method_name}:")
for idx, (x0, v0) in enumerate(initial_conditions):
res = results[method_name][idx]
print(f" {labels_ic[idx]}: {res['E_drift']:+.4f}%")
# 显示图形
plt.tight_layout()
plt.show()
# 可选:保存动画
# anim.save('harmonic_oscillator_4methods.gif', writer='pillow', fps=30)