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)