源文件:notebooks/01_single_pendulum_phase_space.ipynb
单摆相空间与能量 notebook
这是手机端静态预览:保留说明和代码;图表请在电脑端 Jupyter 中运行 notebook 查看。
????????????
Audience:
- ?????????????????????????-???????????
Prerequisites:
- ???????????
- ?????????
L = T - V? - ?????????
theta????omega?
Learning goals:
- ????????????
- ??? Runge-Kutta ?????????
- ?????????????
- ????????????????
Outline
- ???????????
- ??????????
- ??????????
- ????????????
- ?????????????????????
from __future__ import annotations
from dataclasses import dataclass
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. ??
?????????
\[L = rac{1}{2}ml^2\dot heta^2 - mgl(1-\cos heta)\]
??-????????
\[\ddot heta + rac{g}{l}\sin heta = 0\]
???????????????theta_dot = omega?omega_dot = -(g/l)sin(theta)?
@dataclass
class PendulumParams:
length: float = 1.2
gravity: float = 9.8
mass: float = 1.0
damping: float = 0.0
def derivatives(y: np.ndarray, params: PendulumParams) -> np.ndarray:
theta, omega = y
theta_dot = omega
omega_dot = -(params.gravity / params.length) * np.sin(theta) - params.damping * omega
return np.array([theta_dot, omega_dot], dtype=float)
def rk4_step(y: np.ndarray, dt: float, params: PendulumParams) -> np.ndarray:
k1 = derivatives(y, params)
k2 = derivatives(y + 0.5 * dt * k1, params)
k3 = derivatives(y + 0.5 * dt * k2, params)
k4 = derivatives(y + dt * k3, params)
return y + (dt / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4)
def energy(theta: np.ndarray, omega: np.ndarray, params: PendulumParams) -> np.ndarray:
kinetic = 0.5 * params.mass * params.length**2 * omega**2
potential = params.mass * params.gravity * params.length * (1 - np.cos(theta))
return kinetic + potential
def lagrangian(theta: np.ndarray, omega: np.ndarray, params: PendulumParams) -> np.ndarray:
kinetic = 0.5 * params.mass * params.length**2 * omega**2
potential = params.mass * params.gravity * params.length * (1 - np.cos(theta))
return kinetic - potential
def simulate(theta0: float, omega0: float, params: PendulumParams, t_max: float = 12.0, dt: float = 0.002) -> dict[str, np.ndarray]:
steps = int(t_max / dt) + 1
t = np.linspace(0.0, t_max, steps)
y = np.zeros((steps, 2), dtype=float)
y[0] = [theta0, omega0]
for i in range(1, steps):
y[i] = rk4_step(y[i - 1], dt, params)
theta = y[:, 0]
omega = y[:, 1]
return {
"t": t,
"theta": theta,
"omega": omega,
"energy": energy(theta, omega, params),
"lagrangian": lagrangian(theta, omega, params),
"action": np.cumsum(lagrangian(theta, omega, params)) * dt,
}
2. ?????????
???????????????????? 55???????? sin(theta)?
params = PendulumParams(length=1.2, gravity=9.8, damping=0.0)
result = simulate(theta0=np.deg2rad(55), omega0=0.0, params=params, t_max=12.0, dt=0.002)
summary = {
"theta_min_rad": float(result["theta"].min()),
"theta_max_rad": float(result["theta"].max()),
"omega_max_abs_rad_s": float(np.abs(result["omega"]).max()),
"energy_initial_J": float(result["energy"][0]),
"energy_final_J": float(result["energy"][-1]),
"action_final": float(result["action"][-1]),
}
summary
3. ???????
????????????????????????????????????????
fig, axes = plt.subplots(1, 2, figsize=(11, 4.4))
axes[0].plot(result["t"], result["theta"], color="#087c75", lw=2)
axes[0].set_title("Angle over time")
axes[0].set_xlabel("t (s)")
axes[0].set_ylabel("theta (rad)")
axes[1].plot(result["theta"], result["omega"], color="#c35a2b", lw=2)
axes[1].set_title("Phase portrait")
axes[1].set_xlabel("theta (rad)")
axes[1].set_ylabel("omega (rad/s)")
fig.tight_layout()
plt.show()
4. ??????
???????????????????????????????????????????????????
energy_drift = result["energy"] - result["energy"][0]
relative_drift = energy_drift / result["energy"][0]
fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(result["t"], relative_drift, color="#7458b6", lw=2)
ax.set_title("Relative energy drift")
ax.set_xlabel("t (s)")
ax.set_ylabel("(E(t) - E(0)) / E(0)")
plt.show()
float(np.max(np.abs(relative_drift)))
5. ????
????????????????????? beta * omega ???????????????????????
damped_params = PendulumParams(length=1.2, gravity=9.8, damping=0.18)
damped = simulate(theta0=np.deg2rad(55), omega0=0.0, params=damped_params, t_max=16.0, dt=0.002)
fig, axes = plt.subplots(1, 2, figsize=(11, 4.4))
axes[0].plot(damped["t"], damped["energy"], color="#087c75", lw=2)
axes[0].set_title("Damped energy")
axes[0].set_xlabel("t (s)")
axes[0].set_ylabel("E (J)")
axes[1].plot(damped["theta"], damped["omega"], color="#c35a2b", lw=2)
axes[1].set_title("Damped phase portrait")
axes[1].set_xlabel("theta (rad)")
axes[1].set_ylabel("omega (rad/s)")
fig.tight_layout()
plt.show()
??
- ??????
5???????????????2*pi*sqrt(l/g)? - ?
dt?0.002??0.02???????????? - ?
damping??0.5?????????????????
def estimate_period_from_zero_crossings(t: np.ndarray, theta: np.ndarray) -> float:
crossings = []
for i in range(1, len(theta)):
if theta[i - 1] > 0 and theta[i] <= 0:
crossings.append(t[i])
if len(crossings) < 2:
return float("nan")
return 2 * float(np.mean(np.diff(crossings)))
small_angle = simulate(
theta0=np.deg2rad(5),
omega0=0.0,
params=PendulumParams(length=1.2, gravity=9.8, damping=0.0),
t_max=12.0,
dt=0.002,
)
numerical_period = estimate_period_from_zero_crossings(small_angle["t"], small_angle["theta"])
small_angle_period = 2 * np.pi * np.sqrt(params.length / params.gravity)
{
"numerical_period_s": numerical_period,
"small_angle_period_s": float(small_angle_period),
"absolute_error_s": abs(numerical_period - small_angle_period),
}
????
- ????????
sin?Python ?????????????? - ????
dt?????????????????????????????????????? - ?????
S???????????????????????????????????