源文件:notebooks/02_hamiltonian_phase_space.ipynb
哈密顿相空间与辛积分 notebook
这是手机端静态预览:保留说明和代码;图表请在电脑端 Jupyter 中运行 notebook 查看。
??????????
Audience:
- ????????????????????????????????
Prerequisites:
- ??????
p = partial L / partial qdot? - ???????????
H = p^2/(2m) + kq^2/2? - ???? Python?NumPy ? Matplotlib ???
Learning goals:
- ?????????????????
- ?????? Euler?RK4?symplectic Euler?Velocity Verlet ??????
- ???????????????????????
Outline
- ?????????????
- ?????????????
- ??????????????
- ?????????????
- ???????????????????
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable
import matplotlib.pyplot as plt
import numpy as np
plt.rcParams["figure.figsize"] = (9, 4.8)
plt.rcParams["axes.grid"] = True
plt.rcParams["grid.alpha"] = 0.25
1. ?????
??????????
\[H(q,p) = \frac{p^2}{2m} + \frac{1}{2}kq^2\]
??????
\[\dot q = \frac{\partial H}{\partial p} = \frac{p}{m}\]
\[\dot p = -\frac{\partial H}{\partial q} = -kq\]
???????????????????? (qdot, pdot)?
@dataclass(frozen=True)
class Oscillator:
mass: float = 1.0
k: float = 1.0
def hamiltonian(q: np.ndarray, p: np.ndarray, system: Oscillator) -> np.ndarray:
return p**2 / (2 * system.mass) + 0.5 * system.k * q**2
def vector_field(y: np.ndarray, system: Oscillator) -> np.ndarray:
q, p = y
q_dot = p / system.mass
p_dot = -system.k * q
return np.array([q_dot, p_dot], dtype=float)
system = Oscillator(mass=1.0, k=1.0)
q0, p0 = 1.0, 0.0
hamiltonian(np.array(q0), np.array(p0), system).item()
2. ??????????
????????????????????????????????????
q_grid, p_grid = np.meshgrid(np.linspace(-2.0, 2.0, 25), np.linspace(-2.0, 2.0, 25))
q_dot = p_grid / system.mass
p_dot = -system.k * q_grid
energy_grid = hamiltonian(q_grid, p_grid, system)
fig, ax = plt.subplots(figsize=(6.5, 6.0))
ax.contour(q_grid, p_grid, energy_grid, levels=10, colors="#66706d", alpha=0.55)
ax.streamplot(q_grid, p_grid, q_dot, p_dot, color="#087c75", density=1.1, linewidth=1.1, arrowsize=1.0)
ax.scatter([q0], [p0], color="#c35a2b", s=60, zorder=3, label="initial state")
ax.set_title("Hamiltonian phase flow")
ax.set_xlabel("q")
ax.set_ylabel("p")
ax.set_aspect("equal", adjustable="box")
ax.legend()
plt.show()
3. ??????
?????????
- explicit Euler?????????????????
- RK4???????????????????
- symplectic Euler?????????????
- Velocity Verlet?????????
?????????????????????????????????
StepFn = Callable[[np.ndarray, float, Oscillator], np.ndarray]
def explicit_euler_step(y: np.ndarray, dt: float, system: Oscillator) -> np.ndarray:
return y + dt * vector_field(y, system)
def rk4_step(y: np.ndarray, dt: float, system: Oscillator) -> np.ndarray:
k1 = vector_field(y, system)
k2 = vector_field(y + 0.5 * dt * k1, system)
k3 = vector_field(y + 0.5 * dt * k2, system)
k4 = vector_field(y + dt * k3, system)
return y + (dt / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4)
def symplectic_euler_step(y: np.ndarray, dt: float, system: Oscillator) -> np.ndarray:
q, p = y
p_next = p - dt * system.k * q
q_next = q + dt * p_next / system.mass
return np.array([q_next, p_next], dtype=float)
def velocity_verlet_step(y: np.ndarray, dt: float, system: Oscillator) -> np.ndarray:
q, p = y
p_half = p - 0.5 * dt * system.k * q
q_next = q + dt * p_half / system.mass
p_next = p_half - 0.5 * dt * system.k * q_next
return np.array([q_next, p_next], dtype=float)
def simulate(step_fn: StepFn, y0: np.ndarray, dt: float, steps: int, system: Oscillator) -> dict[str, np.ndarray]:
y = np.zeros((steps + 1, 2), dtype=float)
y[0] = y0
for i in range(steps):
y[i + 1] = step_fn(y[i], dt, system)
t = np.arange(steps + 1) * dt
q = y[:, 0]
p = y[:, 1]
return {"t": t, "q": q, "p": p, "H": hamiltonian(q, p, system)}
methods: dict[str, StepFn] = {
"explicit Euler": explicit_euler_step,
"RK4": rk4_step,
"symplectic Euler": symplectic_euler_step,
"Velocity Verlet": velocity_verlet_step,
}
runs = {name: simulate(fn, np.array([1.0, 0.0]), dt=0.08, steps=2200, system=system) for name, fn in methods.items()}
{name: float(run["H"][-1] - run["H"][0]) for name, run in runs.items()}
4. ?????
?????????????????????????????????????????????????????????
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
for ax, (name, run) in zip(axes.flat, runs.items()):
ax.plot(run["q"], run["p"], lw=1.5, color="#087c75")
ax.scatter([run["q"][0]], [run["p"][0]], color="#c35a2b", s=30)
ax.set_title(name)
ax.set_xlabel("q")
ax.set_ylabel("p")
ax.set_aspect("equal", adjustable="box")
fig.tight_layout()
plt.show()
5. ??????
????????????????????????????????????????????
fig, ax = plt.subplots(figsize=(10, 5))
for name, run in runs.items():
relative_error = (run["H"] - run["H"][0]) / run["H"][0]
ax.plot(run["t"], relative_error, label=name, lw=1.8)
ax.set_title("Relative energy error")
ax.set_xlabel("t")
ax.set_ylabel("(H(t)-H(0))/H(0)")
ax.legend()
plt.show()
max_errors = {
name: float(np.max(np.abs((run["H"] - run["H"][0]) / run["H"][0])))
for name, run in runs.items()
}
max_errors
6. ???????
???????????????????????????????????????? Euler ? symplectic Euler ???????
def polygon_area(points: np.ndarray) -> float:
x = points[:, 0]
y = points[:, 1]
return 0.5 * abs(np.dot(x, np.roll(y, -1)) - np.dot(y, np.roll(x, -1)))
def evolve_cloud(step_fn: StepFn, cloud: np.ndarray, dt: float, steps: int, system: Oscillator) -> np.ndarray:
evolved = cloud.copy()
for _ in range(steps):
evolved = np.array([step_fn(point, dt, system) for point in evolved])
return evolved
angles = np.linspace(0, 2 * np.pi, 80, endpoint=False)
cloud = np.column_stack([1.0 + 0.12 * np.cos(angles), 0.0 + 0.12 * np.sin(angles)])
initial_area = polygon_area(cloud)
euler_cloud = evolve_cloud(explicit_euler_step, cloud, dt=0.08, steps=300, system=system)
symp_cloud = evolve_cloud(symplectic_euler_step, cloud, dt=0.08, steps=300, system=system)
areas = {
"initial": initial_area,
"explicit Euler": polygon_area(euler_cloud),
"symplectic Euler": polygon_area(symp_cloud),
}
areas
fig, axes = plt.subplots(1, 2, figsize=(10, 4.6))
for ax, title, evolved in [
(axes[0], "explicit Euler", euler_cloud),
(axes[1], "symplectic Euler", symp_cloud),
]:
ax.plot(cloud[:, 0], cloud[:, 1], color="#66706d", lw=1.5, label="initial")
ax.plot(evolved[:, 0], evolved[:, 1], color="#c35a2b", lw=1.8, label="after evolution")
ax.set_title(title)
ax.set_xlabel("q")
ax.set_ylabel("p")
ax.set_aspect("equal", adjustable="box")
ax.legend()
fig.tight_layout()
plt.show()
??
- ?
dt?0.08??0.2???????????????? - ??????
(q0,p0)=(0,1.5)???????????????? - ?
mass=2?k=4????????????? - ???????????RK4 ?????RK4 ???????????
# Exercise scaffold: change dt and compare final relative energy error.
def compare_dt(dt: float) -> dict[str, float]:
local_runs = {
name: simulate(fn, np.array([1.0, 0.0]), dt=dt, steps=int(120 / dt), system=system)
for name, fn in methods.items()
}
return {
name: float((run["H"][-1] - run["H"][0]) / run["H"][0])
for name, run in local_runs.items()
}
compare_dt(0.2)
??
????????????H ?????????????????????????????????????
- ???
qdot = partial H / partial p?pdot = -partial H / partial q? - ??????????????????
- ?????????????????