Show code cell content
import mmf_setup;mmf_setup.nbinit()
from pathlib import Path
import os
FIG_DIR = Path(mmf_setup.ROOT) / '../Docs/_build/figures/'
os.makedirs(FIG_DIR, exist_ok=True)
import logging;logging.getLogger('matplotlib').setLevel(logging.CRITICAL)
%matplotlib inline
import numpy as np, matplotlib.pyplot as plt
try: from myst_nb import glue
except: glue = None
This cell adds /home/docs/checkouts/readthedocs.org/user_builds/physics-521-classical-mechanics-i/checkouts/latest/src to your path, and contains some definitions for equations and some CSS for styling the notebook. If things look a bit strange, please try the following:
- Choose "Trust Notebook" from the "File" menu.
- Re-execute this cell.
- Reload the notebook.
Kapitza’s Pendulum#
Here we consider the high-frequency limit of parametric oscillations of a pendulum:
This problem was considered by Pyotr Kaptitza, who showed that one can use such a mechanism to stabilize the motion about unstable points.
Simplified Analysis#
We start with a simplified analysis, getting at the essence of the problem without placing too much attention on rigor. This analysis is based on the following assumptions:
Separation of scales: \(\omega \gg \omega_0\). We decompose the motion into a fast part \(f(\omega t)\) and a slow part \(s(t)\):
\[\begin{gather*} \theta(t) = s(t) + f(\omega t), \qquad \braket{f}(\omega t) = 0,\qquad \braket{A} = \frac{1}{T}\int_{t}^{t+T}\!\!\!\!\!\!\!\!A(\tau)\d{\tau}. \end{gather*}\]To recover the slow components, we average over one fast period \(T = 2\pi / \omega\). We remove any slow component from \(f(\omega t)\), placing them in \(s(t)\) so that \(\braket{f}(t) = 0\).
Linearization: we assume that the amplitude of the fast part \(\abs{f} \ll 1\) is small. Note: we do not consider the amplitude of the drive \(a\) to be small. The final effects will be \(O(a^2)\), so we must at least keep this to quadratic order.
The idea will be to consider separately the fast motion \(f(\omega t)\) by treating the slow variables as constants, and then the slow motion \(s(t)\) by averaging over fast variables. This will yield an effective potential for the slow motion that permits stabilization.
Expanding the original equation, we have
Fast Motion#
We first consider the fast motion \(f(\phi)\) where \(\phi = \omega t\). For this analysis, we treat \(s(t)\) as constant, absorbing the constant shift \(\omega_0^2 \sin s + 2\lambda \dot{s} + \ddot{s}\) into \(s\) so that \(\braket{f} = 0\) as discussed above. Finally, we assume that \(\abs{\omega} \gg \lambda\) so we can neglect the damping. The resulting equation is
Note that \(f\) is periodic, so all averages \(\braket{f} = \braket{f'} = \braket{f''} = 0\).
Slow Motion#
We now averaging the dynamical equation:
Thus, the slow degree of freedom experiences damped motion in an effective potential of the form
Do It! Demonstrate numerically that this is correct.
Solution
To numerically check the results, we simulate both the full theory, and the effective theory. The results are plotted below.
Show code cell source
# Here is a numerical check
from scipy.integrate import solve_ivp
class Kapitza:
w0 = 1 # Natural frequency
w = 10.0 # Drive frequency
a = 20.0 # Drive amplitude
lam = 0.01 # Damping
phi = 0.0 # Angle of drive
corrected = False
def __init__(self, **kw):
for key in kw:
if not hasattr(self, key):
raise ValueError(f"Unknown {key=}")
setattr(self, key, kw[key])
self.init()
def init(self):
pass
def pack(self, q, dq, qe, dqe):
y = np.ravel([q, dq, qe, dqe])
return y
def unpack(self, y):
q, dq, qe, dqe = y
return q, dq, qe, dqe
def compute_dy_dt(self, t, y):
"""Return dy_dt."""
q, dq, qe, dqe = self.unpack(y)
w0, w, a, phi, lam = map(self.__getattribute__, ['w0', 'w', 'a', 'phi', 'lam'])
th = w * t
Fq = - w0**2 * np.sin(q) - a * w0**2 * np.sin(q - phi) * np.sin(th)
ddq = -2*lam*dq + Fq
ddqe = -2*lam*dq - self.get_Ueff(qe, d=1)
return self.pack(dq, ddq, dqe, ddqe)
def get_Ueff(self, q, c2=None, d=0):
"""Return the dth derivative of Veff(q)"""
w, w0, a, phi = self.w, self.w0, self.a, self.phi
c1 = -w0**2
if c2 is None:
c2 = - a**2 * w0**4 / 8 / w**2
q_phi = q - phi
if self.corrected and d == 1:
_tmp = (1 - (w0/w)**2*np.cos(q_phi))
_cor = _tmp/(_tmp**2 + 4*(self.lam/w)**2)
return - (c1 * np.sin(q) + 2 * c2 * np.sin(2*q_phi) * _cor)
return (c1 * (1j)**d * np.exp(1j*q) + c2 * (2j)**d * np.exp(2j*(q_phi))).real
def solve(self, q0=None, dq0=0.1, periods=6, points=10, **kw):
"""Return a dense solution.
Arguments
---------
periods : int
Number of slow periods to solve.
points : int
Number of points per fast cycle.
"""
if q0 is None:
q0 = np.pi + self.phi
y0 = self.pack(q0, dq0, q0, dq0)
Ts = 2*np.pi / self.w0 # Slow periods
Tf = 2*np.pi / self.w # Fast periods
dt = Tf / points
T = periods * Ts
Nf = int(np.ceil(T/Tf)) # Number of fast periods
t_eval = np.arange(Nf*points + 1)*dt
kwargs = dict(
y0=y0, t_span=(0, T), t_eval=t_eval, method="BDF", dense_output=True)
kwargs.update(kw)
self.sol = solve_ivp(self.compute_dy_dt, **kwargs)
assert self.sol.success
# Compute some useful quantities by averaging over periods
q, dq, qe, dqe = self.unpack(self.sol.y)
t_ = self.sol.t[:-1].reshape((Nf, points)).mean(axis=1)
q_ = q[:-1].reshape((Nf, points)).mean(axis=1)
self.sol.t_ = t_
self.sol.q_ = q_
self.sol.q, self.sol.qe = q, qe
return self.sol
for (kw, dq0) in [(dict(phi=0.05*np.pi, lam=0.01), 0.4),
(dict(phi=0, lam=0.01), 0.6)]:
s = Kapitza(**kw)
sc = Kapitza(corrected=True, **kw)
sol = s.solve(dq0=dq0)
solc = sc.solve(dq0=dq0)
t_unit = 2*np.pi / s.w0
q_unit = np.pi
fig, axs = plt.subplots(
1, 2,
figsize=(8*1.2, 2*1.2),
sharey=True, width_ratios=(1, 0.2),
gridspec_kw=dict(wspace=0.02))
for ax in axs:
ax.grid(True)
# These are wrong... only approximate
#[ax.axhline(phi_min/q_unit, lw=1, ls=":", c="grey")
# for phi_min in [s.phi, np.pi + 2*s.phi]]
ax = axs[0]
ax.plot(sol.t/t_unit, sol.q / q_unit, 'C0-', lw=1, label='Exact')
ax.plot(sol.t_/t_unit, sol.q_ / q_unit, 'C0-', label='Averaged')
ax.plot(sol.t/t_unit, sol.qe / q_unit, 'C1--', label='Effective')
ax.plot(solc.t/t_unit, solc.qe / q_unit, 'C1-', label='Corrected')
ax.set(xlabel=r"$\omega_0 t / 2\pi$", ylabel=r"$\theta / \pi$")
ax.legend()
ax = axs[1]
q0, q1 = sol.q.min(), sol.q.max()
dq = 0.1*(q1-q0)
q0 -= dq
q1 += dq
q0 = min([q0, s.phi, 0])
q1 = max(s.phi + np.pi, q1)
q = np.linspace(q0, q1)
ax.plot(s.get_Ueff(q, c2=0), q/q_unit, 'C0-', label=r"$U_{0}$")
ax.plot(s.get_Ueff(q), q/q_unit, 'C1-', label=r"$U_{\mathrm{eff}}$")
ax.set(xlim=(-2.1, 1.1), xlabel=r"$U(q)$");
#ax.legend(loc='lower right', bbox_to_anchor=(0, 0));
ax.legend(loc='upper left');
title = [
rf"$\omega/\omega_0={s.w/s.w0:.2g}$",
rf"$a={s.a:.2g}$",
rf"$\lambda/\omega_0={s.lam/s.w0:.2g}$",
rf"$\phi={s.phi/np.pi:.2g}\pi$",
]
plt.suptitle(", ".join(title));
We see that our approach does indeed correctly stabilize the inverted pendulum, and that the effective potential is roughly correct, however, there are quantitative disagreement. If you play with these equations, you will find that these disagreements are mostly an issue with larger amplitude excitations.
Do It! Improve the effective potential.
Hint
One approximation that is not so difficult to improve is to keep the linear pieces in the fast response equation.
This correction is shown in the plot, and seems to help, but not perfectly.
Solution
Following the hint, we have
Another approach would be to numerically simulate the full system, then try to match the effective theory.
Do It!
Show that driving at a different angle \(\phi\) gives
as shown in the video. Analyze the stability as a function of \(\phi\).
Solution
Here is a start. First we find the equilibrium positions. In units where \(\omega_0 = 1\):
This gives a quartic equation for \(\sin s\).
For a related approach, please see sec:FloquetTheory.
Exact Solution#
We start by considering the