Semiclassical Wavepacket Propagation#

This tutorial builds up the Van Vleck and Herman-Kluk semiclassical propagators from a single classical trajectory to a full multi-trajectory wavepacket propagation, checking each step against an exactly solvable system (the harmonic oscillator) along the way.

A trajectory and its monodromy matrix#

physicskit.semiclassical.core.propagators.propagate_trajectory_monodromy_action() integrates a classical trajectory together with its monodromy matrix – the linearized map from small deviations in the initial position and momentum to deviations in the final position and momentum – and its classical action, all in one Numba-compiled pass:

import numpy as np
from numba import njit
from physicskit.semiclassical.core.propagators import propagate_trajectory_monodromy_action

m, omega = 1.0, 1.0
params = np.array([m * omega ** 2])
dVdx = njit(lambda q, params: params[0] * q)
d2Vdx2 = njit(lambda q, params: params[0])
V = njit(lambda q, params: 0.5 * params[0] * q ** 2)

t = 1.3
q_t, p_t, M, S, _ = propagate_trajectory_monodromy_action(
    q0=1.0, p0=0.3, dVdx=dVdx, d2Vdx2=d2Vdx2, V=V, m=m, dt=t / 4000, steps=4000, params=params
)
print(M)  # matches [[cos(t), sin(t)], [-sin(t), cos(t)]] to ~1e-6

Because the classical flow is Hamiltonian, \(\det M = 1\) exactly (Liouville’s theorem) – a good sanity check on any trajectory.

The Van Vleck propagator, checked against an exact result#

van_vleck_propagator_1d() turns that trajectory into a semiclassical propagator amplitude \(K(q_t,t;q_0,0)\). Because the harmonic oscillator’s classical action is exactly quadratic, the WKB expansion this formula comes from truncates exactly, and the result matches the harmonic oscillator’s exact quantum (Mehler) propagator to within \(10^{-5}\):

from physicskit.semiclassical.core.propagators import van_vleck_propagator_1d

q_t, K = van_vleck_propagator_1d(q0=1.0, p0=0.3, dVdx=dVdx, d2Vdx2=d2Vdx2, V=V, m=m, dt=t / 4000, steps=4000, params=params)
K_exact = (
    np.sqrt(m * omega / (2j * np.pi * np.sin(omega * t)))
    * np.exp(1j * m * omega / (2 * np.sin(omega * t)) * ((q_t ** 2 + 1.0 ** 2) * np.cos(omega * t) - 2 * q_t * 1.0))
)
print(abs(K - K_exact))  # ~1e-6

Herman-Kluk: summing many frozen Gaussians#

A single trajectory connects one fixed start point to one fixed end point. To propagate a whole wavepacket, herman_kluk_propagate_wavepacket() instead launches one classical trajectory – each carrying a fixed-width (“frozen”) Gaussian – from every point of a phase-space grid covering the initial state, and sums their contributions:

from physicskit.semiclassical.core.propagators import (
    frozen_gaussian_1d, herman_kluk_propagate_wavepacket,
)

x = np.linspace(-6, 6, 400)
psi_t = herman_kluk_propagate_wavepacket(
    qc0=1.0, pc0=0.0, gamma=1.0, dVdx=dVdx, d2Vdx2=d2Vdx2, V=V, m=m,
    dt=0.01, steps=100, x_eval=x, n_grid=61, n_sigma=7.0, params=params,
)

At very short times this reduces to the frozen-Gaussian basis’s resolution of the identity and reconstructs the unpropagated initial state almost exactly. Away from that limit, this particular quadrature-grid evaluation of the Herman-Kluk integral does not exactly conserve \(\int|\psi|^2\,dx\) the way the true continuous phase-space integral would for an at-most-quadratic potential – physicskit.semiclassical.visualizers.propagators.plot_classical_trajectory_on_wigner() is a good way to sanity-check a propagated state against the exact Wigner function of a comparison calculation, and convergence in n_grid, n_sigma, and gamma should always be checked for serious use.