Hide code cell content

import numpy as np, matplotlib.pyplot as plt
import mmf_setup;mmf_setup.nbinit()
import logging;logging.getLogger('matplotlib').setLevel(logging.CRITICAL)

This cell adds /home/docs/checkouts/readthedocs.org/user_builds/gpe/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.

Coordinate Transformations#

Here we consider solutions to time-dependent GPE-like theories of the form:

\[\begin{gather*} \I \hbar \dot{Ψ}(\vect{X}, t) = \left( \frac{-\hbar^2\nabla^2}{2m} + \mathcal{E}'\Bigl(\abs{Ψ(\vect{X}, t)}^2\Bigr) + \tilde{V}(\vect{X}, t) \right)Ψ(\vect{X}, t). \end{gather*}\]

If the cloud under consideration is moving, rotating, or expanding, then one can often gain significant computational advantage by transforming the coordinates from the lab frame \(\vect{X}\) to a co-moving frame \(\vect{x}\). In some cases, it also helps to scale the wavefunction by a potentially time- and space-dependent factor \(f\):

\[\begin{gather*} Ψ(\vect{X}, t) = f(\vect{X}, t)Φ(\vect{x}, t), \qquad \vect{x} = \vect{x}(\vect{X}, t). \end{gather*}\]

Our goal here will be to find transformations that facilitate the numerical problem of finding \(Φ(\vect{x}, t)\).

General Transformations#

Although we will not consider changing time, we can simplify our calculations of the general transformations by adopting Einstein’s notation, letting \(X^{\mu} = (t, \vect{X})\) etc. and denoting partials \(\partial Φ/\partial t \equiv Φ_{,t}\). We then have

\[\begin{gather*} Ψ = fΦ, \qquad Ψ_{,α} = f_{,α}Φ + fΦ_{,μ}x^{μ}_{,α},\\ Ψ_{,αβ} = f_{,αβ}Φ + f_{,α}Φ_{,μ}x^{μ}_{,β} + f_{,β}Φ_{,μ}x^{μ}_{,α} + fΦ_{,μν}x^{μ}_{,α}x^{ν}_{,β} + fΦ_{,μ}x^{μ}_{,αβ}. \end{gather*}\]

The transformed GPE is

\[\begin{gather*} \I\hbar (f_{,t}Φ + fΦ_{,μ}x^{μ}_{,t}) = \frac{-\hbar^2}{2m} \Biggl( f_{,ii}Φ + 2f_{,i}Φ_{,μ}x^{μ}_{,i} + f(Φ_{,μν}x^{μ}_{,i}x^{ν}_{,i} + Φ_{,μ}x^{μ}_{,ii}) \Biggr) + \left(\mathcal{E}'(n) + \tilde{V}\right)fΦ. \end{gather*}\]

If we simplify by considering only transformation of space \(x^{t}_{,t} = t_{,t} = 1\), \(x^{t}_{,i} = t_{,i} = 0\), we can re-express this as a modified GPE for \(Φ\):

\[\begin{multline*} \I\hbar Φ_{,t} = \frac{-\hbar^2}{2m}\overbrace{x^{j}_{,i}x^{k}_{,i}Φ_{,jk}}^{g^{jk}\nabla_{j}\nabla_{k}\;Φ} %+ \\ + \Biggl(\overbrace{ x^{j}_{,t} + \frac{\hbar}{2m\I} \Bigl( 2\frac{f_{,i}}{f}x^{j}_{,i} + x^{j}_{,ii} \Bigr)}^{\vect{v}} \Biggr) \overbrace{(-\I\hbar Φ_{,j})}^{\op{\vect{p}}\;Φ} + \\ + \Bigl( \underbrace{ \mathcal{E}'(n) + \tilde{V} - \frac{\hbar^2}{2m}\frac{f_{,ii}}{f} - \I\hbar \frac{f_{,t}}{f} }_{\mathcal{E}'(n) + V} \Bigr)Φ,\qquad n = \abs{fΦ}^2. \end{multline*}\]

The first term is a modified kinetic energy with metric \(g^{jk} = x^{j}_{,i}x^{k}_{,i}\), while the second term contains the momentum operator \(\op{p} \equiv -\I\hbar \vect{\nabla}_{x}\) multiplied by a generalized velocity \(\vect{v}\), and the last term is a corrected potential.

Alternatively, we can write \(f(X^μ) = e^{\I Θ(X^{μ})}\) (but note that \(Θ\) need not be real):

\[\begin{gather*} Ψ = e^{\I Θ}Φ, \qquad e^{-\I Θ}Ψ_{,α} = \I Θ_{,α}Φ + Φ_{,μ}x^{μ}_{,α},\\ e^{-\I Θ}Ψ_{,αβ} = (\I Θ_{,αβ} - Θ_{,α}Θ_{,β})Φ + \I Θ_{,α}Φ_{,μ}x^{μ}_{,β} + \I Θ_{,β}Φ_{,μ}x^{μ}_{,α} + Φ_{,μν}x^{μ}_{,α}x^{ν}_{,β} + Φ_{,μ}x^{μ}_{,αβ}. \end{gather*}\]

The transformed GPE is

\[\begin{gather*} \I\hbar (\I Θ_{,t}Φ + Φ_{,μ}x^{μ}_{,t}) = \frac{-\hbar^2}{2m} \Biggl( (\I Θ_{,ii} - Θ_{,i}Θ_{,i})Φ + 2\I Θ_{,i}Φ_{,μ}x^{μ}_{,i} + Φ_{,μν}x^{μ}_{,i}x^{ν}_{,i} + Φ_{,μ}x^{μ}_{,ii} \Biggr) + \\ + \left(\mathcal{E}'(n) + \tilde{V}\right)Φ. \end{gather*}\]

Considering only transformation of space \(x^{t}_{,t} = t_{,t} = 1\), \(x^{t}_{,i} = t_{,i} = 0\), we have the modified GPE for \(Φ\):

\[\begin{multline*} \I\hbar Φ_{,t} = \frac{-\hbar^2}{2m}\overbrace{x^{j}_{,i}x^{k}_{,i}Φ_{,jk}}^{g^{jk}\nabla_{j}\nabla_{k}\;Φ} %+ \\ + \Biggl(\overbrace{ x^{j}_{,t} + \frac{\hbar}{m} Θ_{,i}x^{j}_{,i} + \frac{\hbar x^{j}_{,ii}}{2m\I} }^{\vect{v}} \Biggr) \overbrace{(-\I\hbar Φ_{,j})}^{\op{\vect{p}}\;Φ} + \\ + \Bigl( \underbrace{ \mathcal{E}'(n) + \tilde{V} - \frac{\hbar^2}{2m}(\I Θ_{,ii} - Θ_{,i}Θ_{,i}) + \hbar Θ_{,t} }_{\mathcal{E}'(n) + V} \Bigr)Φ,\qquad n = \abs{e^{\I Θ}Φ}^2. \end{multline*}\]

Examples

Translations

\[\begin{gather*} \vect{x} = \vect{X} - \vect{x}_0(t), \qquad f = 1, \\ x^{j}_{,i} = \delta^{j}_{i}, \qquad x^{j}_{,ii} = 0, \qquad x^{i}_{,t} = -v_0^{i} = -\dot{r}_0^{i},\\ g^{ij} = \delta^{ij}, \qquad \vect{v} = -\vect{v}_0, \\ \I\hbar \dot{Φ} = \Bigl( K(\op{p}) -\vect{v}_0\cdot\op{\vect{p}} + \mathcal{E}'(n) + \tilde{V} \Bigr)Φ. \end{gather*}\]

Note: Although we have only derived our transformations for quadratic dispersion \(K(\op{p}) = \op{p}^2/2m\), in this case, the kinetic energy does not transform, and this remains valid for arbitrary dispersion.

Rotations Consider a rotation about a fixed axis \(\vect{\theta}\):

\[\begin{gather*} \vect{x} = \mat{R}\vect{X}, \qquad f = 1, \qquad [\ln\mat{R}]_{ij} = [\vect{\theta}\times]_{ij} = \epsilon_{kji}[\vect{\theta}]_k,\\ x^{j}_{,i} = [\mat{R}]_{ji}, \qquad x^{j}_{,ii} = 0, \qquad x^{i}_{,t} = \epsilon_{kji}[\vect{\omega}]_{k}[\vect{x}]_{j} = [\vect{\omega}\times \vect{x}]_{i},\qquad g^{ij} = [\mat{R}^T\mat{R}]^{ij} = \delta^{ij},\\ \I\hbar Φ_{,t} = \Biggl( K(\op{p}) + \underbrace{(\vect{\omega}\times\vect{x})\cdot\vect{\op{p}}} _{\vect{\omega}\cdot(\vect{\op{x}}\times\vect{\op{p}}) = \vect{\omega}\cdot \vect{\op{L}}} + \mathcal{E}'(n) + \tilde{V} \Biggr)Φ. \end{gather*}\]

I.e., to work in a rotating frame, we include an angular momentum term.

Scaling

We start with a simple scaling of the coordinates – we will extend this discussion below.

\[\begin{gather*} x^{j} = \frac{X^{j}}{λ_{j}(t)}, \qquad x^{j}_{,t} = -x^{j}\frac{\dot{λ}_{j}}{λ_{j}}, \qquad x^{j}_{,i} = \frac{\delta^{j}_{i}}{λ_{i}}, \qquad x^{i}_{,ii} = 0, \qquad f = 1,\\ \I\hbar Φ_{,t} = \Biggl( K\Bigl(\frac{\op{\vect{p}}}{\vect{λ}}\Bigr) - \sum_{j} \frac{\dot{λ}_{j}}{λ_{j}}x^{j}\op{p}_j + \mathcal{E}'(n) + \tilde{V} \Biggr)Φ,\qquad n = \abs{Φ}^2. \end{gather*}\]

More typically, as we shall see below, we include a factor \(f\) so that the density scales properly, and to remove the term linear in \(\op{p}\), but the specialization there requires assuming that the dispersion is quadratic.

Translation#

The simplest transformation corresponds to an accelerating frame:

\[\begin{gather*} \vect{x} = \vect{X} - \vect{x}_0(t). \end{gather*}\]

The minimal transformation simply tracks this through the equations of motion:

\[\begin{gather*} Ψ(\vect{X}, t) = Φ(\vect{x}, t) = Φ\bigl(\vect{X} - \vect{x}_0(t), t\bigr). \end{gather*}\]

The only modifications to the equation of motion are:

  1. An addition contribution to the time dependence:

    \[\begin{gather*} \I\hbar \dot{Ψ} = \I\hbar \dot{Φ} - \I\hbar \underbrace{\dot{\vect{x}}_0(t)}_{\vect{v}_0(t)} \cdot \vect{\nabla}Φ = \I\hbar \dot{Φ} + \vect{v}_0(t) \cdot \op{\vect{p}}Φ. \end{gather*}\]
  2. The potential must be evaluated in the lab-frame: \(V(\vect{X}) = V\bigl(\vect{x} - \vect{x}_0(t)\bigr)\).

Thus, in the scaled coordinates, we have

\[\begin{gather*} \I\hbar \dot{Φ}(\vect{x}, t) = \Bigl( K(\op{p}) - \vect{v}\cdot\op{\vect{p}} + V(\vect{X}) + \mathcal{E}'(n) \Bigr)Φ(\vect{x}, t). \end{gather*}\]

This transformation can be accounted for by simply modifying the dispersion relationship.

Scaling (Expansion)#

If we need to scale the coordinates \(X_i = λ_i(t)x_i\) where \(\vect{X}\) is the lab frame and \(\vect{x}\) is the co-expanding frame, then we can introduce the following wavefunction:

\[\begin{gather*} Ψ(\vect{X}, t) = \frac{e^{\I\theta(\vect{X}, t)}Φ(\vect{x}, t)} {\sqrt{λ_x(t)λ_y(t)λ_z(t)}}, \qquad X_i = λ_i(t)x_i, \qquad \hbar\theta(\vect{X}, t) = \frac{m}{2} \sum_i \frac{X_i^2\dot{λ}_i(t)}{λ_i(t)}. \end{gather*}\]

With this transformation, the following theories are equivalent:

\[\begin{gather*} \I\hbar\dot{Ψ}(\vect{X}, t) = \left( \frac{-\hbar^2\nabla_{\vect{X}}^2}{2m} + \frac{m \sum_{i}\omega_i^2(t)X_i^2}{2} + \mathcal{E}'(n) + V(\vect{X}) \right)Ψ(\vect{X}, t), \\ \I\hbar\dot{Φ}(\vect{x}, t) = \left( \frac{-\hbar^2\nabla_{\vect{X}}^2}{2m} + \frac{m \sum_{i}\bigl( \omega_{i}^{2}(t)λ_{i}^{2} + λ_{i}\ddot{λ}_{i}\bigr)x_i^2}{2} + \mathcal{E}'(n) + V(\vect{X}) \right)Φ(\vect{x}, t). \end{gather*}\]

The utility of this transformation is that the classical behavior of an expanding gas can be modeled by setting:

\[\begin{gather*} \ddot{λ}_i(t) = \frac{\omega_i^2(0)}{λ_i(t)λ_x(t)λ_y(t)λ_z(t)} - \omega_i^2(t)λ_i(t), \end{gather*}\]

which gives:

\[\begin{gather*} \I\hbar\dot{Φ}(\vect{x}, t) = \left( \frac{-\hbar^2\nabla_{\vect{X}}^2}{2m} + \frac{1}{λ_x(t)λ_y(t)λ_z(t)}\frac{m}{2} \sum_{i} \omega_i^2(0) x_i^2 + \mathcal{E}'(n) + V(\vect{X}) \right)Φ(\vect{x}, t). \end{gather*}\]

With this transformation, \(Φ(\vect{x}, t)\) evolves minimally (see [Castin and Dum, 1996]). Note that in the new coordinates, the effective trapping potential is constant, up to the overall density scaling. I.e., the time-dependence of the harmonic potential is completely absorbed into the scaling factor. This is crucial for the dr-GPE since in this co-expanding frame, the effective trap remains, even if the trap is completely turned off: \(\omega_i(t>0) = 0\).

A similar expansion can be performed scaling only \(X\) and \(Y\):

\[\begin{gather*} \I\hbar\dot{Ψ} = \left\{\frac{-\hbar^2(\nabla_X^2+\nabla_Y^2)}{2m} + E(\op{p}_Z) + V(Z, t) + \frac{m}{2}\left(\omega_x^2(t)X^2 + \omega_y^2(t)Y^2\right) +\epsilon(\absΨ^2) \right\}Ψ, \\ Ψ(X, Y, Z; t) = e^{\I\theta}\frac{Φ(x, y, Z;t)}{\sqrt{λ_x(t)λ_y(t)}}, \qquad x = \frac{X}{λ_x(t)}, \qquad y = \frac{Y}{λ_y(t)}. \end{gather*}\]

The resulting equations of motion for \(Φ(x, y, Z;t)\) can be expressed as:

\[\begin{gather*} \I\hbar\dot{Φ} = \left\{ \frac{-\hbar^2(\nabla_X^2+\nabla_Y^2)}{2m} + E(\op{p}_Z) + V(Z, t) +\mathcal{E}'(n) +\tfrac{m}{2}\frac{\omega_x^2(0)x^2 + m\omega_y^2(0)y^2}{λ_x(t)λ_y(t)} \right\}Φ,\\ \ddot{λ}_j = - λ_j \omega_j^2(t) + \frac{\omega_j^2(0)}{λ_j(t)λ_x(t)λ_y(t)}, \qquad λ_i(0) = 1, \qquad \dot{λ}_j(0) = 0. \end{gather*}\]

This is similar to the form in [Castin and Dum, 1996], but does not include the dynamics along the \(z\) axis in the equation. A reason for using this is that the scaling transform depends on the dispersion being quadratic and we would like to explore modified dispersions (i.e. for spin-orbit coupled (SOC) BECs). In this expression, the dynamics along \(Z\) remain unscaled and explicitly managed.

A failed attempt.

One might try to express the equations simply by completely removing the potentials:

\[\begin{gather*} \I\hbar\dot{Φ} = \left\{ \frac{-\hbar^2(\nabla_X^2+\nabla_Y^2)}{2m} + E(\op{p}_Z) + V(Z, t) +\epsilon(\absΨ^2) \right\}Φ, \\ \ddot{λ}_j = - λ_j \omega_i^2(t), \qquad λ_i(0) = 1, \qquad \dot{λ}_j(0) = 0. \end{gather*}\]

This form, however, is of little use. Consider for example an expanding cloud with \(\omega_j(t) = 0\) for \(t>0\). The resulting theory simply has no scaling, \(λ_j(t) = 1\) and degenerates to the raw GPE. We have thrown out the baby with the bath water.

Derivation#

The general derivation is a bit messy, so we start with 1D. This will also allow us to consider simultaneous scaling and translation. Let \(Ψ(X, t)\) be the physical wavefunction satisfying the non-linear Schrödinger equation, and relate this to a computational wavefunction \(Φ(x, t)\) in the computational coordinates \(x(X, t)\) with:

\[\begin{gather*} Ψ(X, t) = e^{\IΘ(X,t)}Φ(x(X, t), t). \end{gather*}\]

Expanding the Schrödinger equation

\[\begin{gather*} \I\hbar \dot{Ψ} = \frac{-\hbar^2}{2m}\nabla_X^2Ψ, \end{gather*}\]

and demanding that first-order spatial derivatives \(\nabla_x Φ\) cancel (for reasons of computational efficiency), we obtain:

\[\begin{gather*} Θ'(X, t) = -\frac{m}{\hbar}\frac{\dot{x}}{x'} + \frac{\I}{2}\ln(x')',\\ Θ(X, t) = - \frac{m}{\hbar}\int\frac{\dot{x}}{x'}\d{X} + \frac{\I}{2}\ln(x') + \I C(t),\\ \dot{C}(t) = \frac{\dot{x}x''-x'\dot{x}'}{x'^2} = -\left(\frac{\dot{x}}{x'}\right)', \end{gather*}\]

where primes denote derivatives with respect to \(X\).

Note

\(Θ\) is not real here – the last two terms of \(Θ\) represents an overall scaling of the wavefunction, and the integration constant \(C(t)\) is chosen here so that the evolution remains unitary. (An additional real integration constant can be introduced as a chemical potential, but it will only affect the phase of the wavefunction so we drop this.) As a consistency condition, \(\dot{C}(t)\) must not depend on \(X\), so we are restricted to coordinate transformations that have the form \(\dot{x} = [a(t)+b(t)X]x'\). This will suffice for our purposes. More general transformation will require that we allow for linear derivatives.

We now specialize to a coordinate scaling and translation:

\[\begin{gather*} x(X, t) = \frac{X - ρ(t)}{λ(t)}, \qquad X = λ(t)x + ρ(t), \qquad Ψ(X, t) = \frac{e^{\I\theta(X, t)}}{\sqrt{λ(t)}}Φ(x(X, t), t), \\ \theta(X, t) = \frac{m}{2\hbar}\left( \frac{\dot{λ}}{λ}X^2 - 2λ\dot{ρ}X\right) = \frac{m}{2\hbar}\left( \frac{\dot{λ}}{λ}\left( X-\frac{λ^2\dot{ρ}}{\dot{λ}}\right)^2 - \frac{\dot{ρ}^2λ^3}{\dot{λ}} \right) \end{gather*}\]

The scaling factor \(1/\sqrt{λ}\) comes from the imaginary terms \(C(t)\) above in \(Θ\).

This leads to a effective potential of the following form:

\[\begin{gather*} V_{λ, ρ}(X, t) = \frac{m}{2}\left( \frac{\ddot{λ}}{λ}X^2 + 2\left(\ddot{ρ}-\frac{\ddot{λ}}{λ}ρ\right)X + \left( \dot{ρ} - ρ \frac{\dot{λ}}{λ} \right)^2 \right). \end{gather*}\]
\[\begin{gather*} V_{λ, ρ}(X, t) = \frac{m}{2}\left( λ \ddot{λ} x^2 + 2λ\ddot{ρ}x + c(t) \right),\\ c(t) = \dot{ρ}^2 + 2\ddot{ρ}ρ + + ρ^2\left(\frac{\dot{λ}}{λ}\right)^2 - 2\dot{ρ}ρ \frac{\dot{λ}}{λ} - ρ^2 \frac{\ddot{λ}}{λ} = \dot{ρ}^2 + 2\ddot{ρ}ρ - 2\dot{ρ}ρ \frac{\dot{λ}}{λ} - ρ^2(t)\diff[2]{\lnλ(t)}{t} \end{gather*}\]

One nice use of this is to take a problem that has a time-dependent external potential and replace it with a stationary potential. Thus, for example, suppose we have:

\[\begin{gather*} V_{\text{ext}}(X, t) = \frac{m\omega^2(t)}{2}[X-R(t)]^2 \end{gather*}\]

in the lab frame. The essence of Castin and Dumm’s argument is that a good strategy is to choose \(λ(t)\) and \(R(t)\) such that the effective potential for \(Φ\) is constant after scaling. I.e., for the GPE \(\mathcal{E}'(n) = gn\) and the equations of motion for \(Φ\) have the form:

(1)#\[\begin{align} \I\hbar \dot{Φ} &= \left( \frac{-\hbar^2\nabla^2_{\vect{X}}}{2m} + V_{λ,ρ}+ V_{\text{ext}} + g \abs{Ψ}^2 \right)Φ, \\ \I\hbar \dot{Φ} &= \left( \frac{-\hbar^2\nabla^2_{\vect{X}}}{2m} + \frac{ (λ_xλ_yλ_z) (V_{λ,ρ}+ V_{\text{ext}}) + g\abs{Φ}^2 }{λ_xλ_yλ_z} \right)Φ. \end{align}\]

The change in \(Φ\) is minimized if we choose

\[\begin{gather*} V_{λ,ρ}(t) = \frac{V_{\text{ext}}(0)}{λ_xλ_yλ_z} - V_{\text{ext}}(t). \end{gather*}\]

I.e., if we started in a stationary state at time \(t=0\) with external potential \(V_{\text{ext}}(0)\), then choosing such a transformation minimizes the effects of dynamics due to the trap expansion.

This gives the following differential equations for \(λ_i(t)\) and \(ρ_i(t)\):

\[\begin{gather*} \ddot{λ}_i(t) = \frac{\omega_i^2(t)}{λ_i λ_xλ_yλ_z} - \omega^2(t)λ_i, \qquad \ddot{ρ}_i(t) = \omega_i^2(t)[R_1(t) - ρ_i(t)]. \end{gather*}\]

As a simple check, consider a stationary solution in the frame where the coordinates scale to new values so that at some late time \(t\), \(λ \neq 1\) but \(\dot{λ} = \ddot{λ} = 0\). In the computational basis we should have:

\[\begin{gather*} Φ(x) = \sqrt{λ}Ψ(λ x)\\ \frac{-\hbar^2Ψ''(X)}{2mΨ(X)} + g \abs{Ψ}^2 + V(X) = E,\\ \frac{-\hbar^2Φ''(x)}{2mλ^2Φ(x)} + \frac{g}{λ} \abs{Φ}^2 + V(X) = E,\\ \end{gather*}\]

Radial Expansion#

A specific use-case is that of an axially symmetric trap in which we describe the expansion in the radial direction. This is implemented in gpe/axial.py. As before, the physical coordinate \(R\) is related to the scaled coordinate \(r\) as

\[\begin{gather*} x = X, \qquad r = \frac{R}{λ_\perp(t)} = \sqrt{x^2+y^2}, \\ Ψ(X, R, t) = e^{\I\theta} \frac{Φ(x, r, t)}{λ_\perp}, \qquad \theta = \frac{mR^2}{2\hbar}\diff{}{t}\lnλ_\perp(t). \end{gather*}\]

We discretize the equations for the scaled function \(Φ(x, r, t)\) on the scaled coordinates \(x\) and \(r\):

\[\begin{gather*} \I\hbar\dot{Φ}(x, r ,t) = \left\{ \frac{-\hbar^2}{2mλ_\perp^2}\left( \frac{1}{r}\pdiff{}{r}r\pdiff{}{r} + \frac{1}{r^2}\pdiff[2]{}{\phi} \right) + E(\op{p}_x) + V(X, R, t) -\frac{m\omega_\perp^2(t)}{2} R^2 +\mathcal{E}(n) + \tfrac{m}{2}\frac{\omega_\perp^2(0)r^2}{λ_\perp^2(t)} \right\}Φ(x, r ,t),\\ \ddot{λ}_\perp(t) = - λ_\perp(t) \omega_\perp^2(t) + \frac{\omega_\perp^2(0)}{λ_\perp^3(t)}, \qquad λ_\perp(0) = 1, \qquad \dot{λ}_\perp(0) = 0. \end{gather*}\]

Note that the scaling \(λ_\perp(t)\) depends only on the trapping frequency, so can be applied to multiple components as long as the external trapping potential behaves the same way for both.

Test Examples#

In the case of expansion \(\omega_\perp(t>0) = 0\) we have:

\[\begin{gather*} \ddot{λ}_\perp(t) = \frac{\omega_\perp^2(0)}{λ_\perp^3(t)}, \qquad λ_\perp(0) = 1, \qquad \dot{λ}_\perp(0) = 0,\\ λ_\perp = \sqrt{1+\tau^2}, \qquad \omega_\perp(0)t. \end{gather*}\]

Here we explicitly code this example so we have something to work with later.

from gpe.imports import *
from pytimeode import mixins, interfaces
from pytimeode.evolvers import EvolverABM
from mmfutils.math.bases import CylindricalBasis

@interfaces.implementer(interfaces.IStateForABMEvolvers)
class State0(mixins.ArrayStateMixin):
    def __init__(self, N=2**5, R=6):
        self.basis = CylindricalBasis(Nxr=(1, N), 
                                      Lxr=(1, R))
        R = self.R
        self.data = 5*np.exp(-R[None,:]**2/2) + 0j
        
    @property
    def R(self):
        return self.basis.xyz[1].ravel()
    
    @property
    def n(self):
        return abs(self[...].ravel())**2
    
    def compute_dy_dt(self, dy):
        y = self[...]
        dy[...] = self.basis.laplacian(y, factor=-1./2)
        dy[...] += abs(y)**2*y
        dy /= 1j
        return dy
    
    def plot(self):
        plt.plot(self.R, self.n)
        plt.title("t={:.4g}".format(self.t))


class State1(State0):
    @property
    def lam(self):
        return np.sqrt(1+self.t**2)
    
    @property
    def R(self):
        return self.basis.xyz[1].ravel() * self.lam
    
    @property
    def n(self):
        return abs(self[...].ravel())**2 / self.lam**2
    
    def compute_dy_dt(self, dy):
        x, r = self.basis.xyz
        y = self[...]
        dy[...] = self.basis.laplacian(y, factor=-1./2)
        dy[...] += (r**2/2.0 + abs(y)**2)*y
        dy /= self.lam**2
        dy /= 1j
        return dy
            
s0 = State0()
s1 = State1()

dt = 0.0001
steps = 200
e0 = EvolverABM(s0, dt=dt)
e1 = EvolverABM(s1, dt=dt)
#fig = plt.figure(figsize=(10,5))
with NoInterrupt() as interrupted:
    while not interrupted and e0.t < 1:
        e0.evolve(steps)
        e1.evolve(steps)

        plt.clf()
        e0.y.plot()
        e1.y.plot()
        display(plt.gcf())
        clear_output(wait=True)
../_images/62f0e972b22b70fa2a22e43428906dabc9ae4ef7719e5195836b61fbd80a85d0.png

Here we do the same thing with the axial.State2Axial class:

from gpe.imports import *
from gpe import bec2, axial;reload(bec2); reload(axial)
from gpe.axial import u
from gpe.minimize import MinimizeState

l_unit = 1.0
t_unit = 1.0
basis = axial.CylindricalBasis(Nxr=(64, 32), Lxr=(2*6*l_unit, 6*l_unit))


class State(axial.State2Axial):
    t_expand = .0*t_unit
    _ws = np.array([48.78*0, 48.78])*u.Hz
    _ws = np.array([0.0/u.Hz, 1.0/u.Hz])*u.Hz

    def get_ws(self, t=None):
        if t is None:
            t = self.t
        if t < self.t_expand:
            return self._ws
        else:
            #return 0*self._ws
            return np.concatenate([self._ws[:1], 0*self._ws[1:]])
        
    def get_ws_perp(self, t=None):
        return self.get_ws(t=t)[1:]        

    @property
    def ws(self):
        return self.get_ws()
        
    @ws.setter
    def ws(self, ws):
        pass

    def _get_Vext_(self):
        """Return (V_a, V_b, V_ab), the external potentials."""
        V_HO = (0.5*self.ms[self.bcast]
                * sum((_w*_x)**2 for _w, _x in zip(self.ws, self.xyz)))
        zero = np.zeros_like(V_HO[0])
        V_ab = self.Omega*np.exp(2j*self.k_r*self.xyz[0])
        mu_a = mu_b = 0.0
        if hasattr(self, 'mus') and self.mus:
            mu_a, mu_b = self.mus

        return (V_HO[0] - mu_a - self.delta/2.0,
                V_HO[1] - mu_b + self.delta/2.0,
                V_ab + zero)

class State_(State):
    def get_ws_perp(self, t=None):
        """Do not change these - no expansion in basis"""
        return self.get_ws(t=0)[1:]

args = dict(basis=basis, x_TF=4*u.micron, gs=[0.0007682083478971494, 0.0007682083478971494, 0])
args = dict(basis=basis, x_TF=4*u.micron, gs=[1.0, 1.0, 0.0], ms=[1.0, 1.0], Omega=0, delta=0)
s_ = State_(**args)
s = State(**args)
x, r = s.xyz
s[...] = 5/(l_unit)**1.5*np.exp(-(0*x**2+r**2)/2/l_unit**2)
s_[...] = s[...]
s.cooling_phase = s_.cooling_phase = 1.0
s.plot()
from gpe.plot_utils import MPLGrid

s0 = State0()

dt = 0.02*s.t_scale
steps = 100
e0 = EvolverABM(s0, dt=dt/t_unit)
e = EvolverABM(s, dt=dt)
e_ = EvolverABM(s_, dt=dt)

plt.figure(figsize=(15,5))
grid = MPLGrid(direction='right', space=0.2)
e.y.plot(grid=grid)
plt.plot(e0.y.R, 2*e0.y.n)
e_.y.plot(grid=grid)        
plt.plot(e0.y.R, 2*e0.y.n)

with NoInterrupt() as interrupted:
    while e.y.t < 0.3*t_unit and not interrupted:
        e0.evolve(steps)
        e.evolve(steps)
        e_.evolve(steps)
        plt.figure(figsize=(15,5))
        grid = MPLGrid(direction='right', space=0.2)
        e.y.plot(grid=grid)
        plt.plot(e0.y.R, 2*e0.y.n)
        e_.y.plot(grid=grid)        
        plt.plot(e0.y.R, 2*e0.y.n)
        display(plt.gcf())
        plt.close('all')
        clear_output(wait=True)

Here is a more realistic example of an expanding spherical cloud.

from gpe.imports import *
from gpe import bec2, axial;reload(bec2); reload(axial)
from gpe.axial import u
from gpe.minimize import MinimizeState

basis = axial.CylindricalBasis(Nxr=(2*64, 2*32), Lxr=(20.0*u.micron, 10*u.micron))

class State(axial.State2Axial):
    t_expand = .02*u.ms
    _ws = np.array([20*126.0, 20*126.0])*u.Hz

    def get_ws(self, t=None):
        if t is None:
            t = self.t
        if t <= self.t_expand:
            return self._ws
        else:
            return 0*self._ws
            return np.concatenate([self._ws[:1], 0*self._ws[1:]])
        
    def get_ws_perp(self, t=None):
        return self.get_ws(t=t)[1:]

    @property
    def ws(self):
        return self.get_ws()
        
    @ws.setter
    def ws(self, ws):
        pass

    def _get_Vext_(self):
        """Return (V_a, V_b, V_ab), the external potentials."""
        V_HO = (0.5*self.ms[self.bcast]
                * sum((_w*_x)**2 for _w, _x in zip(self.ws, self.xyz)))
        zero = np.zeros_like(V_HO[0])
        V_ab = self.Omega*np.exp(2j*self.k_r*self.xyz[0])
        mu_a = mu_b = 0.0
        if hasattr(self, 'mus') and self.mus:
            mu_a, mu_b = self.mus

        return (V_HO[0] - mu_a - self.delta/2.0,
                V_HO[1] - mu_b + self.delta/2.0,
                V_ab + zero)

class State_(State):
    def get_ws_perp(self, t=None):
        """Do not change these - no expansion in basis"""
        return self.get_ws(t=0)[1:]

args = dict(basis=basis, x_TF=4*u.micron)
s_ = State_(**args)
s = State(**args)
s.plot()
m = MinimizeState(s, fix_N=True)
assert m.check()
def callback(s, n=[0]):
    if n[0] % 200 == 0:
        plt.clf()
        s.plot()
        display(plt.gcf())
        plt.close('all')
        clear_output(wait=True)
    n[0] += 1
s = m.minimize(callback=callback, use_scipy=True)
s_[...] = s[...]
s.cooling_phase = s_.cooling_phase = 1.0
xi = np.sqrt(s.hbar**2/2/s.ms[0]/(s.gs[0]*s.get_density().max()))
from gpe.plot_utils import MPLGrid
dt = 0.1*s.t_scale
steps = 10
e = EvolverABM(s, dt=dt)
e_ = EvolverABM(s_, dt=dt)
with NoInterrupt() as interrupted:
    while e.y.t < 0.5*u.ms and not interrupted:
        e.evolve(steps)
        e_.evolve(steps)
        plt.figure(figsize=(15,5))
        grid = MPLGrid(direction='right', space=0.2)
        e.y.plot(grid=grid)
        e_.y.plot(grid=grid)        
        display(plt.gcf())
        plt.close('all')
        clear_output(wait=True)

Expansion#

If we consider pure expansion from a static trap with \(\omega_j(t>0) = 0\) and equal radial frequencies \(\omega_x = \omega_y = \omega_\perp\), then we have the following scaling parameters in the two cases:

\[\begin{gather*} λ_\perp(t) = \sqrt{1+\tau^2}, \qquad \tau = \omega_\perp(0)t. \end{gather*}\]
from scipy.integrate import odeint

from gpe import bec, minimize


class State(bec.State):
    """Here we simulate the state Phi(X, y, z) discussed above with
    explicit scaling dependence added (in y and z).
    
    Note: self.xyz == (X, y, z)
    """
    def init(self):
        lams = np.ones(self.dim)
        dlams = np.zeros(self.dim)
        self._lambda_cache = (0, np.ravel([lams, dlams]))
        super().init()        
        
    def _get_Vext_(self):
        return sum(self.m*(_w*_x)**2/2.0 
                   for _w, _x in zip(self.ws, self.xyz))
    
    def get_ws(self, t=None):
        if t is None:
            t = self.t
        if t <= 0:
            return self.ws
        else:
            return np.zeros(self.dim)
            
    def _rhs(self, q, t):
        """RHS for lambda(t) ODE."""
        lams, dlams =np.reshape(q, (2, self.dim))
        w0s = self.get_ws(t=0)
        ws = self.get_ws(t=t)
        ddlams = -lams * ws**2 + w0s**2/np.prod(lams)/lams
        return np.ravel([dlams, ddlams])
    
    def get_lambdas(self):
        if _t != self._lambda_cache[0]:
            t0, q0 = self._lambda_cache
            q1 = odeint(self._rhs, q0, [t0, self.t])[-1]
            self._lambda_cache = (t1, q1)
        return self._lambda_cache[1][:self.dim]

s = State()
s0 = minimize.MinimizeState(s).minimize()
s0.plot()

Summary#

Time-Dependent Harmonic Trap#

import sympy

sympy.init_printing()
t = sympy.var("t", real=True)
Xs = sympy.var("X, Y, Z", real=True)
xs_ = sympy.var("x, y, z", real=True)
ws = sympy.var("omega_x, omega_y, omega_z", real=True)
Ve = sympy.var("V_{ext}", cls=sympy.Function)
ls = [_l(t) for _l in sympy.var('lambda_x, lambda_y, lambda_z', cls=sympy.Function)]
xs = [_X / _l for _X, _l in zip(Xs, ls)]
m, h = sympy.var("m, hbar", positive=True)
theta = m / 2 / h * sum(_X**2 * _l.diff(t) / _l for _X, _l in zip(Xs, ls))
Phi = sympy.var("Phi", cls=sympy.Function)(*(xs + [t]))
s = sympy.sqrt(sympy.prod(ls))
Psi = sympy.exp(sympy.I * theta) * Phi / s
V = sum(m * _w**2 * _X**2 / 2 for _w, _X in zip(ws, Xs)) + Ve(*Xs)
L = (
    (
        sympy.I * h * Psi.diff(t)
        - (-(h**2) * (sum(Psi.diff(_X, _X) for _X in Xs)) / 2 / m + V * Psi)
    )
    / Psi
    * Phi
)
# display(sympy.simplify(sympy.I*h*Psi.diff(t).doit()))
subs = {_X: _x * _l for _X, _x, _l in zip(Xs, xs_, ls)}
L = L.subs(subs)
xs = xs_

# Here is the Lagrangian for Phi
Phi = Phi.subs(subs)
L0 = (
    sympy.I * h * Phi.diff(t)
    - (
        -(h**2) * (sum(Phi.diff(_x, _x) / _l**2 for _x, _l in zip(xs, ls))) / 2 / m
        + Ve(*Xs) * Phi
    )
).subs(subs)

sympy.simplify((L - L0).doit())
sympy.simplify((L).doit())

Demonstration#

from gpe import bec
from gpe.utils import GPUHelper
from gpe.contexts import FPS

reload(bec)
u = bec.u

class StateMixin(GPUHelper):
    ws = 2 * np.pi * np.array([np.sqrt(8) * 126.0, 126.0, 126.0]) * u.Hz
    def get_ws(self, t=None):
        if t is None:
            t = self.t
        if t <= 0:
            return self.ws
        else:
            return 1.5 * self.ws

    def get_Vext_GPU(self):
        ws = self.get_ws()
        xyz = self.get_xyz_GPU()
        V_trap = 0.5 * self.m * sum((_w * _x) ** 2 for _w, _x in zip(ws, xyz))

        # Arbitrary time-dependence to show that this still works.
        V_trap += 0.5 * np.sin(self.t) * np.exp(-xyz[0] ** 2 / 2)
        # V_trap += 0.5*np.exp(-self.xyz[0]**2/2)
        return V_trap


class State(StateMixin, bec.State):
    pass


class StateScale(StateMixin, bec.StateScaleHO):
    pass


args = dict(Nxyz=(32, 32, 32))
args = dict(Nxyz=(128,), x_TF=10.0)
s0 = State(**args)
s1 = StateScale(**args)


def plot(s):
    n = s.get_density()
    plt.subplot(221)
    x, y, z = list(map(np.ravel, s.xyz))
    imcontourf(y, z, n.max(axis=0), aspect=1)
    plt.subplot(222)
    imcontourf(x, z, n.max(axis=1), aspect=1)
    plt.subplot(223)
    imcontourf(x, y, n.max(axis=2), aspect=1)


dT = 0.1
dt = 0.1 * s0.t_scale
steps = int(np.ceil(dT/dt))
dt = dT/steps
e0 = EvolverABM(MinimizeState(s0).minimize(), dt=dt)
e1 = EvolverABM(MinimizeState(s1).minimize(), dt=dt)
# e0 = EvolverABM(s0, dt=0.4*s0.t_scale)
# e1 = EvolverABM(s1, dt=0.4*s1.t_scale)
clear_output()
e0.evolve(900)
e1.evolve(900)
e0.y.get_energy(), e1.y.get_energy()
T = 800
Nt = int(T/dT)
fig, ax = plt.subplots()
for frame in FPS(range(Nt), fig=fig, display=True):
    e0.evolve(steps)
    e1.evolve(steps)
    x0 = e0.y.xyz[0]
    x1 = e1.y.xyz[0]
    ax.cla()
    axs = e0.y.plot(axs=[ax])
    e1.y.plot(axs=axs)
    plt.suptitle(f"{e0.y.get_energy(), e1.y.get_energy()}")

Harmonic Traps#

\[\begin{gather*} -\I\hbar \nabla^X_i\Psi = (\hbar \nabla^X_i\theta)\Psi + \frac{e^{\I\theta}}{s}\lambda_i(-\I\hbar \nabla^x_i)\Phi(\vect{x}, t) = \frac{e^{\I\theta}}{s}\left( mX_i \frac{\dot{\lambda}_i}{\lambda_i} + \lambda_i(-\I\hbar \nabla^x_i)\right)\Phi(\vect{x}, t), \end{gather*}\]
\[\begin{gather*} \int \abs{\op{P}\Psi}^2 \d{X} = \frac{1}{s^2} \int \d{X} \left( \left(mX\frac{\dot{\lambda}}{\lambda}\Phi\right)^2 + mX\frac{\dot{\lambda}}{\lambda}(2\Phi^\dagger \op{p}\Phi) + \abs{\op{p}\Phi}^2 \right) \end{gather*}\]

Integrating the middle term by parts we get:

\[\begin{gather*} -\I\hbar\frac{\dot{\lambda}}{\lambda}m X\Phi^\dagger \Phi_X -\I\hbar\frac{\dot{\lambda}}{\lambda}m \Phi^\dagger (\Phi +X\Phi_X) \end{gather*}\]

Let \(\vect{X} = (X, Y, Z)\) be the physical coordinates. In a time-varying harmonic potential, [Castin and Dum, 1996] provide the following alternative description in terms of scaled coordinates:

\[\begin{gather*} x_i = \frac{X_i}{\lambda_i(t)} \end{gather*}\]

using the rescaled function \(\Phi(\vect{x}, t)\):

\[\begin{gather*} \Psi(\vect{X}, t) = e^{\I\theta} \frac{\Phi(\vect{x}, t)}{\sqrt{\lambda_x(t)\lambda_y(t)\lambda_z(t)}}, \qquad \theta = m\sum_{i}\frac{X_i^2}{2\hbar}\diff{}{t}\ln\lambda_i(t). \end{gather*}\]

Let \(s = \sqrt{\lambda_x(t)\lambda_y(t)\lambda_z(t)}\), then

\[\begin{gather*} \dot{s} = \frac{s}{2}\sum_i\diff{}{t}\ln\lambda_i(t), \qquad \partial_t \frac{1}{s} = -\frac{1}{2s}\sum_i\diff{}{t}\ln\lambda_i(t), \qquad \end{gather*}\]
\[\begin{gather*} \dot{\Psi}(\vect{X}, t) = e^{\I\theta}\frac{ \dot{\Phi}(\vect{x}, t) + \sum_i (x_i\partial_t\ln \lambda_i) \nabla_{i}\Phi }{\sqrt{\lambda_x(t)\lambda_y(t)\lambda_z(t)}} + \sum_{i}\left( \I m\frac{X_i^2}{2\hbar}\diff[2]{}{t}\ln\lambda_i(t) - \frac{1}{2}\diff{}{t}\ln\lambda_i(t) \right)\Psi\\ \nabla_{\vect{X}}\Psi(\vect{X}, t) = e^{\I\theta}\frac{\nabla_{\vect{X}}\Phi(\vect{x}, t)}{\sqrt{\lambda_x(t)\lambda_y(t)\lambda_z(t)}} + \I m\frac{X_i}{\hbar}\diff{}{t}\ln\lambda_i(t)\Psi. \end{gather*}\]

The energy is

\[\begin{gather*} E[\Phi] = \int\left( \frac{\I\hbar \Psi^\dagger\dot{\Psi} + \text{h.c.}}{2} + \frac{\hbar^2\abs{\nabla\Psi}^2}{2m} + V(\vect{X}, t)n + \mathcal{E}(n) \right)\d^3{X}, \qquad n = \frac{\abs{\Phi}^2}{\lambda_x(t)\lambda_y(t)\lambda_z(t)} \end{gather*}\]

Radial Expansion#

Consider the following GPE in an axially confining harmonic trap:

\[\begin{gather*} \I\hbar\dot{\Psi} = \left\{\frac{-\hbar^2(\nabla_X^2+\nabla_Y^2)}{2m} + E(p_Z) + V(Z, t) + \frac{m}{2}\left(\omega_x^2(t)X^2 + \omega_y^2(t)Y^2\right) +\epsilon(\abs\Psi^2) \right\}\Psi. \end{gather*}\]

Now consider the following transformation:

\[\begin{gather*} \Psi(X, Y, Z; t) = e^{\I\theta}\frac{\Phi(x, y, Z;t)}{\sqrt{\lambda_x(t)\lambda_y(t)}}, \qquad x = \frac{X}{\lambda_x(t)}, \qquad y = \frac{X}{\lambda_x(t)}. \end{gather*}\]

The resulting equations of motion for \(\Phi(x, y, Z;t)\) can be expressed in one of the following two forms:

\[\begin{gather*} \I\hbar\dot{\Phi} = \left\{ \frac{-\hbar^2(\nabla_X^2+\nabla_Y^2)}{2m} + E(\op{p}_Z) + V(Z, t) +\epsilon(\abs\Psi^2) \right\}\Phi, \qquad \ddot{\lambda}_j = - \lambda_j \omega_i^2(t), \qquad \lambda_i(0) = 1, \qquad \dot{\lambda}_j(0) = 0. \end{gather*}\]

This form, however, is not of particular use. Consider for example an expanding cloud with \(\omega_j(t) = 0\) for \(t>0\). The resulting theory simply has no scaling, \(\lambda_j(t) = 1\) and degenerates to the raw GPE.

Instead, we keep the \(t=0\) potentials, applying the scaling as follows:

\[\begin{gather*} \I\hbar\dot{\Phi} = \left\{ \frac{-\hbar^2(\nabla_X^2+\nabla_Y^2)}{2m} + E(\op{p}_Z) + V(Z, t) +\epsilon(\abs\Psi^2) + \frac{1}{\lambda_x(t)\lambda_y(t)} \frac{m}{2}\left(\omega_x^2(0)x^2 + m\omega_y^2(0)y^2\right) \right\}\Phi, \qquad \ddot{\lambda}_j = - \lambda_j \omega_i^2(t) + \frac{\omega_j^2(0)}{\lambda_j(t)\lambda_1(t)\lambda_2(t)}, \qquad \lambda_i(0) = 1, \qquad \dot{\lambda}_j(0) = 0. \end{gather*}\]

This is similar to the form in [Castin and Dum, 1996], but does not include the dynamics along the \(z\) axis in the equation. The reason is that the scaling transform depends on the dispersion being quadratic and we would like to explore modified dispersions. Thus, the dynamics along \(Z\) remain unscaled and explicitly managed, allowing for modified dispersion (SOC) for example.

Dynamics Traps#

As similar expansion can be performed scaling only \(X\) and \(Y\):

\[\begin{gather*} \I\hbar\dot{\Psi} = \left\{\frac{-\hbar^2(\nabla_X^2+\nabla_Y^2)}{2m} + E(\op{p}_Z) + V(Z, t) + \frac{m}{2}\left(\omega_x^2(t)X^2 + \omega_y^2(t)Y^2\right) +\epsilon(\abs\Psi^2) \right\}\Psi, \\ \Psi(X, Y, Z; t) = e^{\I\theta}\frac{\Phi(x, y, Z;t)}{\sqrt{\lambda_x(t)\lambda_y(t)}}, \qquad x = \frac{X}{\lambda_x(t)}, \qquad y = \frac{Y}{\lambda_y(t)}. \end{gather*}\]

The resulting equations of motion for \(\Phi(x, y, Z;t)\) can be expressed in one of the following two forms:

\[\begin{gather*} \I\hbar\dot{\Phi} = \left\{ \frac{-\hbar^2(\nabla_X^2+\nabla_Y^2)}{2m} + E(\op{p}_Z) + V(Z, t) +\epsilon(\abs\Psi^2) \right\}\Phi, \qquad \ddot{\lambda}_j = - \lambda_j \omega_i^2(t), \qquad \lambda_i(0) = 1, \qquad \dot{\lambda}_j(0) = 0. \end{gather*}\]

This form, however, is not of particular use. Consider for example an expanding cloud with \(\omega_j(t) = 0\) for \(t>0\). The resulting theory simply has no scaling, \(\lambda_j(t) = 1\) and degenerates to the raw GPE.

Instead, we keep the \(t=0\) potentials, applying the scaling as follows:

\[\begin{gather*} \I\hbar\dot{\Phi} = \left\{ \frac{-\hbar^2(\nabla_X^2+\nabla_Y^2)}{2m} + E(\op{p}_Z) + V(Z, t) +\epsilon(\abs\Psi^2) + \frac{1}{\lambda_x(t)\lambda_y(t)} \frac{m}{2}\left(\omega_x^2(0)x^2 + m\omega_y^2(0)y^2\right) \right\}\Phi, \qquad \ddot{\lambda}_j = - \lambda_j \omega_j^2(t) + \frac{\omega_j^2(0)}{\lambda_j(t)\lambda_x(t)\lambda_y(t)}, \qquad \lambda_i(0) = 1, \qquad \dot{\lambda}_j(0) = 0. \end{gather*}\]

This is similar to the form in [Castin and Dum, 1996], but does not include the dynamics along the \(z\) axis in the equation. The reason is that the scaling transform depends on the dispersion being quadratic and we would like to explore modified dispersions. Thus, the dynamics along \(Z\) remain unscaled and explicitly managed.

Radial Expansion#

A specific use-case is that of an axially symmetric trap in which we describe the expansion in the radial direction. This is implemented in gpe/axial.py. As before, the physical coordinate \(R\) is related to the scaled coordinate \(r\) as

\[\begin{gather*} x = X, \qquad r = \frac{R}{\lambda_\perp(t)} = \sqrt{x^2+y^2}, \\ \Psi(X, R, t) = e^{\I\theta} \frac{\Phi(x, r, t)}{\lambda_\perp}, \qquad \theta = \frac{mR^2}{2\hbar}\diff{}{t}\ln\lambda_\perp(t). \end{gather*}\]

We discretize the equations for the scaled function \(\Phi(x, r, t)\) on the scaled coordinates \(x\) and \(r\):

\[\begin{gather*} \I\hbar\dot{\Phi}(x, r ,t) = \left\{ \frac{-\hbar^2}{2m\lambda_\perp^2}\left( \frac{1}{r}\pdiff{}{r}r\pdiff{}{r} + \frac{1}{r^2}\pdiff[2]{}{\phi} \right) + E(\op{p}_x) + V(X, R, t) -\frac{m\omega_\perp^2(t)}{2} R^2 +\epsilon(\abs\Psi^2) +\frac{1}{\lambda_\perp^2(t)}\frac{m\omega_\perp^2(0)}{2}r^2 \right\}\Phi(x, r ,t),\\ \ddot{\lambda}_\perp(t) = - \lambda_\perp(t) \omega_\perp^2(t) + \frac{\omega_\perp^2(0)}{\lambda_\perp^3(t)}, \qquad \lambda_\perp(0) = 1, \qquad \dot{\lambda}_\perp(0) = 0. \end{gather*}\]

Note that the scaling \(\lambda_\perp(t)\) depends only on the trapping frequency, so can be applied to multiple components as long as the external trapping potential behaves the same way for both.

Test Example#

In the case of expansion \(\omega_\perp(t>0) = 0\) we have:

\[\begin{gather*} \ddot{\lambda}_\perp(t) = \frac{\omega_\perp^2(0)}{\lambda_\perp^3(t)}, \qquad \lambda_\perp(0) = 1, \qquad \dot{\lambda}_\perp(0) = 0,\\ \lambda_\perp = \sqrt{1+\tau^2}, \qquad \omega_\perp(0)t. \end{gather*}\]

Here we explicitly code this example so we have something to work with later.

%pylab inline --no-import-all

from gpe.imports import *
from pytimeode import mixins, interfaces
from pytimeode.evolvers import EvolverABM
from mmfutils.math.bases import CylindricalBasis
from zope.interface import implementer


@implementer(interfaces.IStateForABMEvolvers)
class State0(mixins.ArrayStateMixin):
    def __init__(self, N=2**5, R=6):
        self.basis = CylindricalBasis(Nxr=(1, N), Lxr=(1, R))
        R = self.R
        self.data = 5 * np.exp(-R[None, :] ** 2 / 2) + 0j

    @property
    def R(self):
        return self.basis.xyz[1].ravel()

    @property
    def n(self):
        return abs(self[...].ravel()) ** 2

    def compute_dy_dt(self, dy):
        y = self[...]
        dy[...] = self.basis.laplacian(y, factor=-1.0 / 2)
        dy[...] += abs(y) ** 2 * y
        dy /= 1j
        return dy

    def plot(self):
        plt.plot(self.R, self.n)
        plt.title("t={:.4g}".format(self.t))


@implementer(interfaces.IStateForABMEvolvers)
class State1(State0):
    @property
    def lam(self):
        return np.sqrt(1 + self.t**2)

    @property
    def R(self):
        return self.basis.xyz[1].ravel() * self.lam

    @property
    def n(self):
        return abs(self[...].ravel()) ** 2 / self.lam**2

    def compute_dy_dt(self, dy):
        x, r = self.basis.xyz
        y = self[...]
        dy[...] = self.basis.laplacian(y, factor=-1.0 / 2)
        dy[...] += (r**2 / 2.0 + abs(y) ** 2) * y
        dy /= self.lam**2
        dy /= 1j
        return dy


s0 = State0()
s1 = State1()

dt = 0.0001
steps = 200
e0 = EvolverABM(s0, dt=dt)
e1 = EvolverABM(s1, dt=dt)
# fig = plt.figure(figsize=(10,5))
with NoInterrupt() as interrupted:
    while not interrupted and e0.t < 1:
        e0.evolve(steps)
        e1.evolve(steps)

        plt.clf()
        e0.y.plot()
        e1.y.plot()
        display(plt.gcf())
        clear_output(wait=True)

Here we do the same thing with the axial.State2Axial class:

%pylab inline --no-import-all

from gpe.imports import *
from gpe import bec2, axial

reload(bec2)
reload(axial)
from gpe.axial import u
from gpe.minimize import MinimizeState

l_unit = 1.0
t_unit = 1.0
basis = axial.CylindricalBasis(Nxr=(64, 32), Lxr=(2 * 6 * l_unit, 6 * l_unit))


class State(axial.State2Axial):
    t_expand = 0.0 * t_unit
    _ws = np.array([48.78 * 0, 48.78]) * u.Hz
    _ws = np.array([0.0 / u.Hz, 1.0 / u.Hz]) * u.Hz

    def get_ws(self, t=None):
        if t is None:
            t = self.t
        if t < self.t_expand:
            return self._ws
        else:
            # return 0*self._ws
            return np.concatenate([self._ws[:1], 0 * self._ws[1:]])

    def get_ws_perp(self, t=None):
        return self.get_ws(t=t)[1:]

    @property
    def ws(self):
        return self.get_ws()

    @ws.setter
    def ws(self, ws):
        pass

    def get_Vext(self):
        """Return (V_a, V_b, V_ab), the external potentials."""
        V_HO = (
            0.5
            * self.ms[self.bcast]
            * sum((_w * _x) ** 2 for _w, _x in zip(self.ws, self.xyz))
        )
        V_ab = self.Omega * np.exp(2j * self.k_r * self.xyz[0])
        mu_a = mu_b = 0.0
        if hasattr(self, "mus") and self.mus:
            mu_a, mu_b = self.mus

        return (
            V_HO[0] - mu_a - self.delta / 2.0,
            V_HO[1] - mu_b + self.delta / 2.0,
            V_ab,
        )


class State_(State):
    def get_ws_perp(self, t=None):
        """Do not change these - no expansion in basis"""
        return self.get_ws(t=0)[1:]


args = dict(
    basis=basis, x_TF=4 * u.micron, gs=[0.0007682083478971494, 0.0007682083478971494, 0]
)
args = dict(
    basis=basis, x_TF=4 * u.micron, gs=[1.0, 1.0, 0.0], ms=[1.0, 1.0], Omega=0, delta=0
)
s_ = State_(**args)
s = State(**args)
x, r = s.xyz
s[...] = 5 / (l_unit) ** 1.5 * np.exp(-(0 * x**2 + r**2) / 2 / l_unit**2)
s_[...] = s[...]
s.cooling_phase = s_.cooling_phase = 1.0
s.plot()
from gpe.plot_utils import MPLGrid

s0 = State0()

dt = 0.02 * s.t_scale
steps = 100
e0 = EvolverABM(s0, dt=dt / t_unit)
e = EvolverABM(s, dt=dt)
e_ = EvolverABM(s_, dt=dt)

plt.figure(figsize=(15, 5))
grid = MPLGrid(direction="right", space=0.2)
e.y.plot(grid=grid)
plt.plot(e0.y.R, 2 * e0.y.n)
e_.y.plot(grid=grid)
plt.plot(e0.y.R, 2 * e0.y.n)

with NoInterrupt() as interrupted:
    while e.y.t < 0.3 * t_unit and not interrupted:
        e0.evolve(steps)
        e.evolve(steps)
        e_.evolve(steps)
        plt.figure(figsize=(15, 5))
        grid = MPLGrid(direction="right", space=0.2)
        e.y.plot(grid=grid)
        plt.plot(e0.y.R, 2 * e0.y.n)
        e_.y.plot(grid=grid)
        plt.plot(e0.y.R, 2 * e0.y.n)
        display(plt.gcf())
        plt.close("all")
        clear_output(wait=True)

Here is a more realistic example of an expanding spherical cloud.

%pylab inline --no-import-all

from gpe.imports import *
from gpe import bec2, axial

reload(bec2)
reload(axial)
from gpe.axial import u
from gpe.minimize import MinimizeState

basis = axial.CylindricalBasis(Nxr=(2 * 64, 2 * 32), Lxr=(20.0 * u.micron, 10 * u.micron))


class State(axial.State2Axial):
    t_expand = 0.02 * u.ms
    _ws = np.array([20 * 126.0, 20 * 126.0]) * u.Hz

    def get_ws(self, t=None):
        if t is None:
            t = self.t
        if t <= self.t_expand:
            return self._ws
        else:
            return 0 * self._ws
            return np.concatenate([self._ws[:1], 0 * self._ws[1:]])

    def get_ws_perp(self, t=None):
        return self.get_ws(t=t)[1:]

    @property
    def ws(self):
        return self.get_ws()

    @ws.setter
    def ws(self, ws):
        pass

    def get_Vext(self):
        """Return (V_a, V_b, V_ab), the external potentials."""
        V_HO = (
            0.5
            * self.ms[self.bcast]
            * sum((_w * _x) ** 2 for _w, _x in zip(self.ws, self.xyz))
        )
        V_ab = self.Omega * np.exp(2j * self.k_r * self.xyz[0])
        mu_a = mu_b = 0.0
        if hasattr(self, "mus") and self.mus:
            mu_a, mu_b = self.mus

        return (
            V_HO[0] - mu_a - self.delta / 2.0,
            V_HO[1] - mu_b + self.delta / 2.0,
            V_ab,
        )


class State_(State):
    def get_ws_perp(self, t=None):
        """Do not change these - no expansion in basis"""
        return self.get_ws(t=0)[1:]


args = dict(basis=basis, x_TF=4 * u.micron)
s_ = State_(**args)
s = State(**args)
s.plot()
m = MinimizeState(s, fix_N=True)
assert m.check()


def callback(s, n=[0]):
    if n[0] % 200 == 0:
        plt.clf()
        s.plot()
        display(plt.gcf())
        plt.close("all")
        clear_output(wait=True)
    n[0] += 1


s = m.minimize(callback=callback, use_scipy=True)
s_[...] = s[...]
s.cooling_phase = s_.cooling_phase = 1.0
xi = np.sqrt(s.hbar**2 / 2 / s.ms[0] / (s.gs[0] * s.get_density().max()))
from gpe.plot_utils import MPLGrid
dt = 0.1 * s.t_scale
steps = 10
e = EvolverABM(s, dt=dt)
e_ = EvolverABM(s_, dt=dt)
with NoInterrupt() as interrupted:
    while e.y.t < 0.5 * u.ms and not interrupted:
        e.evolve(steps)
        e_.evolve(steps)
        plt.figure(figsize=(15, 5))
        grid = MPLGrid(direction="right", space=0.2)
        e.y.plot(grid=grid)
        e_.y.plot(grid=grid)
        display(plt.gcf())
        plt.close("all")
        clear_output(wait=True)

Expansion#

If we consider pure expansion from a static trap with \(\omega_j(t>0) = 0\) and equal radial frequencies \(\omega_x = \omega_y = \omega_\perp\), then we have the following scaling parameters in the two cases:

\[\begin{gather*} \lambda_\perp(t) = \sqrt{1+\tau^2}, \qquad \tau = \omega_\perp(0)t. \end{gather*}\]
%pylab inline --no-import-all
from scipy.integrate import odeint

from gpe import bec, minimize


class State(bec.State):
    """Here we simulate the state Phi(X, y, z) discussed above with
    explicit scaling dependence added (in y and z).

    Note: self.xyz == (X, y, z)
    """

    def init(self):
        lams = np.ones(self.dim)
        dlams = np.zeros(self.dim)
        self._lambda_cache = (0, np.ravel([lams, dlams]))
        bec.State.init()

    def get_Vext(self):
        return sum(self.m * (_w * _x) ** 2 / 2.0 for _w, _x in zip(self.ws, self.xyz))

    def get_ws(self, t=None):
        if t is None:
            t = self.t
        if t <= 0:
            return self.ws
        else:
            return np.zeros(self.dim)

    def _rhs(self, q, t):
        """RHS for lambda(t) ODE."""
        lams, dlams = np.reshape(q, (2, self.dim))
        w0s = self.get_ws(t=0)
        ws = self.get_ws(t=t)
        ddlams = -lams * ws**2 + w0s**2 / np.prod(lams) / lams
        return np.ravel([dlams, ddlams])

    def get_lambdas(self):
        if _t != self._lambda_cache[0]:
            t0, q0 = self._lambda_cache
            q1 = odeint(self._rhs, q0, [t0, self.t])[-1]
            self._lambda_cache = (t1, q1)
        return self._lambda_cache[1][: self.dim]


s = State()
s0 = minimize.MinimizeState(s).minimize()
s0.plot()