Hide code cell source

import mmf_setup; mmf_setup.nbinit()

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.

The Gross-Pitaevskii Equation (GPE)#

Here we demonstrate the solution of the Gross-Pitaevskii equation (GPE) (or non-linear Schrödinger equation (NLSEQ)) describing a weakly interacting Bose-Einsten condensate (BEC):

\[\begin{gather*} \I\hbar\pdiff{}{t}\psi(\vect{r}, t) = \left( \frac{-\hbar^2\vect{\nabla}^2}{2m} + V(\vect{r}, t) + gn(\vect{r}, t) \right)\psi(\vect{r}, t), \qquad n(\vect{r}, t) = \abs{\psi(\vect{r}, t)}^2. \end{gather*}\]

The idea here is that at very cold temperatures, a collection of bosons can all occupy the same quantum state. Without any interactions between the particles (\(g=0\)), the system of particles can be described by a single quantum wavefunction \(\psi(\vect{x}, t)\) which will evolve through the Schrödinger equation.

Once particle-particle interactions are included (\(g \neq 0\)) the system must formally be described by a manybody quantum wavefunction \(\Psi(\vect{x}_0, \vect{x}_1, \cdots, \vect{x}_{N-1}, t)\), but if the interactions are weak and temperature low, this can be well approximated again by a single wavefunction, suitably normalized so as to represent the local density \(n(\vect{x}, t)\). The interactions are codified in this theory as a non-linear term \(gn(\vect{x}, t)\) that acts as a density-dependent effective potential for the wavefunction.

This is the essense of the “mean field” approximation - the effects of the interactions are averaged over all of the particles an replaced by this non-linear term. For example, if the particles are repulsive \(g>0\), then this term \(gn\) acts as a repulsive potential, pushing other particles away from regions of high density. Likewise, if the interaction is attractive, then \(g<0\) and this term will act as a potential well, drawing more particles into regions of high density.

This mean-field description works well at \(T\approx 0\) when the interactions are weak in the sense that \(gn^3 \ll 1\). In this case, the coupling constant \(g\) is related to the two-body s-wave scattering length \(a_s\) as:

\[\begin{gather*} g = \frac{4\pi \hbar^2 a_s}{m}. \end{gather*}\]

The GPE follows from the following principle of minimal action:

\[\begin{gather*} S[\psi] = \int\d^{3}{\vect{x}}\d{t} L[\psi], \qquad \mathcal{L}[\psi] = \psi^\dagger \I\hbar \dot{\psi} - E[\psi],\\ \mathcal{E}[\psi] = \frac{\hbar^2\abs{\vect{\nabla}\psi}^2}{2m} + Vn + \frac{gn^2}{2}, \end{gather*}\]

with the GPE resulting from the Euler-Lagrange equations:

\[\begin{gather*} \pdiff{}{t}\frac{\delta \mathcal{L}[\psi]}{\delta\dot{\psi}^\dagger} = \frac{\delta L[\psi]}{\delta\psi^\dagger} = \frac{\delta \mathcal{E}[\psi]}{\delta\psi^\dagger}. \end{gather*}\]

Projections and Cooling#

The standard GPE can be expressed as

\[\begin{gather*} \I\hbar \dot{\ket{\psi}} = \op{H}\ket{\psi} \end{gather*}\]

where the Hamiltonian operator depends on the state \(\ket{\psi}\) through the density (hence, the non-linear Schr”odinger equation), and effects the operation

\[\begin{gather*} \op{H}\ket{\psi} = \pdiff{E}{\bra{\psi}}. \end{gather*}\]

Several modifications to this standard evolution are used in the code:

Imaginary-Time Cooling and Quantum Friction#

The first is complex-time evolution, or cooling with imaginary-time evolution. This is typically denoted through a phase

\[\begin{gather*} \I e^{\I\eta} \hbar \dot{\ket{\psi}} = \op{H}\ket{\psi} \end{gather*}\]

where \(e^{\I \eta}\) is the StateBase.cooling_phase parameter. If cooling_phase = 1j, then we have what is referred to as imaginary-time cooling and the evolution implements a gradient descent:

\[\begin{gather*} \dot{\ket{\psi}} = -\hbar^{-1}\pdiff{E}{\bra{\psi}}. \end{gather*}\]

This is guaranteed to reduce the energy.

The dissipative GPE (dGPE)#

Constraints#

The previously mentioned cooling techniques will not only remove energy from the system, but may change the particle number. To fix this, one simply subtracts the chemical potential from the Hamiltonian

\[\begin{gather*} \op{H} - \mu \op{1}, \qquad \mu = \braket{\psi|\op{H}|\psi}. \end{gather*}\]

More generically, if we implement an evolution

\[\begin{gather*} \dot{\ket{\psi}} = \op{A}\ket{\psi} \end{gather*}\]

and want to constrain a quantity \(\op{C}= \op{C}^\dagger\):

\[\begin{gather*} c = \braket{\psi|\op{C}|\psi}, \qquad \op{C}\ket{\psi} = \pdiff{c}{\bra{\psi}}, \end{gather*}\]

then we want to make sure that the evolution is orthogonal to the gradient of the constraint. This can be done by projecting out the change along this direction:

\[\begin{gather*} \op{\Pi}_{\perp} = \frac{\op{C}\ket{\psi}\bra{\psi}\op{C}^\dagger} {\sqrt{\braket{\psi|\op{C}^\dagger\op{C}\psi}}},\qquad \dot{\ket{\psi}} = \Bigl(\op{1} - \op{\Pi}_{\perp}\Bigr)\op{A}\ket{\psi}. \end{gather*}\]

Ground State#

To obtain the ground state, we find the wavefunction that minimizes the energy \(E[\psi] = \int\d^3{\vect{x}} \mathcal{E}[\psi]\). However, this minimization must be constrained: \(\psi = 0\) in general minimizes the energy but with no particles. Thus, we obtain the constrained minimization problem:

\[\begin{gather*} \min_{\psi} E[\psi] \text{ where } N[\psi] = \int \d^3{\vect{x}} \abs{\psi}^2 = N_0. \end{gather*}\]

This can be implemented using Lagrange multipliers:

\[\begin{gather*} \frac{\delta}{\delta\psi^\dagger} \left(E[\psi] - \mu N[\psi]\right) = 0, \end{gather*}\]

which gives the time-independent non-linear Schrödinger equation:

\[\begin{gather*} \left( \frac{-\hbar^2\vect{\nabla}^2}{2m} + V + gn \right)\psi = \mu \psi. \end{gather*}\]

Solutions to this equation are “stationary” in the sense that their only time-evolution is a continual phase change (unobservable):

\[\begin{gather*} \psi(\vect{x}, t) = e^{-\I\mu t/\hbar}\psi(\vect{x}) = e^{-\I\omega t}\psi(\vect{x}), \end{gather*}\]

where the phase oscillates with frequency:

\[\begin{gather*} \omega = \frac{\mu}{\hbar}. \end{gather*}\]

Some Solutions#

Homogeneous States#

In the absence of an external potential, the system has translational invariance, hence momentum is a conserved and provides good quantum number \(\vect{k}\). These homogeneous states are solutions of the GPE with constant density \(n = n_0\):

\[\begin{gather*} \psi_{\vect{k}}(\vect{x}, t) = \sqrt{n_0}e^{\I(\vect{k}\cdot\vect{x} - \omega t)}, \qquad \mu = \hbar \omega = \frac{\hbar^2 k^2}{2m} + gn_0. \end{gather*}\]

If the potential varies slowly, then it can be a good approximation to treat each region of the gas locally as being homogeneous. This approximation is called the Thomas-Fermi approximation and amounts to solving the GPE in the limit where the kinetic energy can be neglected. These Thomas-Fermi states thus satisfy:

\[\begin{gather*} [gn(\vect{x}) + V(\vect{x}) - \mu]\psi_{TF}(\vect{x}) = 0, \qquad \psi_{TF}(\vect{x}) = \begin{cases} \sqrt{\frac{\mu - V(\vect{x})}{g}} & V(\vect{x}) < \mu\\ 0 & V(\vect{x}) > \mu \end{cases}. \end{gather*}\]

When there are lots of particles in a smoothly varying system, this approximation is good starting point, and often describes cold-atom experiments well.

Todo

Khalid: Discuss Pressure etc., speed of sound, etc.

Let’s start by considering a homogeneous gas with constant \(\psi(\vect{x}, t) = \sqrt{n}\). The energy density is thus:

\[\begin{gather*} \mathcal{E}(n) = \frac{g}{2} n^2. \end{gather*}\]

Using thermodynamics, we can derive the chemical potential and pressure:

\[\begin{gather*} \mu = \pdiff{\mathcal{E}(n)}{n} = gn, \qquad P(\mu) = \mu n - \mathcal{E} = \frac{gn^2}{2} = \frac{\mu^2}{2g}. \end{gather*}\]

Bright Solitons#

In the case of an attractive interaction \(g<0\) there is an analytic self-bound solution with variations along one dimension (i.e. this forms a flat wall):

\[\begin{gather*} \psi(x, t) = \sqrt{n_0}\sech[\eta(x-vt)]e^{\I(kx - \omega t)}\\ \eta = \sqrt{\frac{-gmn_0}{\hbar^2}},\qquad k = \frac{mv}{\hbar},\qquad \mu = \hbar \omega = \hbar^2\frac{k^2-\eta^2}{2m} \end{gather*}\]

Dark Solitons#

In a background of constant density, there is an excited state that is an analytical solution to the GPE called a dark (or grey) soliton, which is an extended object with structure in one dimension, forming an “anti-wall” with a density depletion in a plane:

\[\begin{gather*} \psi(x, t) = \sqrt{n_0}\left( \I\frac{v}{c} + \frac{u}{c}\tanh[\eta(x-vt)] \right)e^{-\I\omega t},\\ \eta = \frac{m u}{\hbar}, \qquad \mu = \hbar\omega = gn_0 = mc^2, \qquad u = \sqrt{c^2-v^2},\qquad \end{gather*}\]

This soliton moves with speed \(v\), and \(c = \sqrt{gn_0/m}\) is the speed of sound.

Khalid: Discuss healing length etc.

Harmonic Oscillator#

A common example is a BEC trapped in a 1D harmonic oscillator potential:

\[\begin{gather*} V(x) = \frac{m\omega^2 x^2}{2}. \end{gather*}\]

The introduction of this potential provides another length scale in the problem:

\[\begin{gather*} a_{HO} = \sqrt{\frac{\hbar}{m\omega}}. \end{gather*}\]

This potential is ubiquitous because almost all functions can be expanded about their minimum as a Taylor series and the quadratic term being dominant for small deviations. For example, most optical trapping potentials provide a Gaussian potential which effects a harmonic trap if the power is high enough:

\[\begin{gather*} V(x) = -Ae^{-x^2/2\sigma^2} \approx -A + \frac{A x^2}{2\sigma^2} + \order(x^3). \end{gather*}\]

Unfortunately, this potential does not admit an analytic solution in general, however, if we set \(g=0\), then the GPE reduces to the Schrödinger equation for the quantum harmonic oscillator whose solutions are well known and can be used to test the code:

\[\begin{gather*} \psi(x, t) = \frac{1}{\sqrt{2^n n!}\sqrt[4]{\pi}}\frac{1}{\sqrt{a_{n}}} \exp\left\{-\frac{x^2}{2a_{HO}^2}\right\} H_n\left(\frac{x}{a_{HO}}\right) e^{-\I\omega_n t}, \qquad \mu_n = \hbar\omega_n = \hbar\omega\left(n + \frac{1}{2}\right). \end{gather*}\]

Length Scales#

We start by considering the dimensions of various quantities:

\[\begin{gather*} [m] = M, \qquad [\hbar] = [Et] = [mv^2t] = \frac{M D^2}{T}, \qquad [a_s] = D, \qquad [gn] = [E] = \frac{M D^2}{T^2}, \qquad [g] = [EV] = \frac{M D^5}{T^2}, \qquad [n] = [1/V] = \frac{1}{D^3}, \qquad [\psi] = [\sqrt{n}] = \frac{1}{D^{3/2}}. \end{gather*}\]

To understand the relevant scales in this problem we need to identify the unique dimensionless parameters. To do this, we can do the following: let \(m\) defined the mass scale by setting \(m=1\) and let \(\hbar = 1\) define the time scale. This allows us to express everything in units of length:

\[\begin{gather*} [m] = M = 1, \qquad [\hbar] = 1 = \frac{D^2}{T}, \qquad [t] = T = D^2, \qquad [g] = [EV] = D, \qquad [n] = [1/V] = \frac{1}{D^3}. \end{gather*}\]

Thus, there are only two fundamental dimensionless quantities - the interaction \(g\) and the density \(n\). The corresponding length scales are:

(2)#\[\begin{gather} a_s = \frac{gm}{4\pi\hbar^2}, \tag{scattering length}\\ r_0 = \left(\frac{3}{4\pi n}\right)^{1/3}. \tag{interparticle separation} \end{gather}\]

To use this, one can form dimensionless quantities by first cancelling the length scales, then using factors of \(m\) and \(\hbar\) to get rid of factors of \(M\) and \(T\) respectively. The only dimensionful parameter in the free theory is thus:

(3)#\[\begin{gather} \gamma = n a_s^{3} = n\left(\frac{mg}{4\pi \hbar^2}\right)^3. \tag{gas parameter} \end{gather}\]

The GPE is generally a valid approximation if \(\gamma \ll 0.01\).

Occasionally this is expressed as \(1/k_Fa_s = 1/(3\pi^2 n)^{1/3}a_s\) where \(n = k_F^3/3\pi^2\) which comes from an analogy with a Fermi gas.

If we were to consider the BEC at finite temperature we would have another scale, temperature, which can be equated to an energy through the Boltzmann constant \(k_B=1\) so \([T] = [E] = 1/D^2\). This introduces another length scale – the thermal de Broglie wavelength:

\[\begin{gather*} \lambda_{\text{th}} = \sqrt{\frac{2\pi \hbar^2}{m k_B T}}. \end{gather*}\]

Thus we can introduce another dimensionless constant:

\[\begin{gather*} \ln z = \frac{\lambda_{\text{th}}^2}{a_s^2} = \frac{gn}{k_B T} = \frac{4(2\pi)^3 \hbar^6}{m^2g^2 k_B T} \end{gather*}\]

The exponential of this \(z\) is called the “fugacity”. Again, the GPE is valid only for \(z \gg 1\)

Moving Frames and Twisted Boundaries#

Two related extensions that can be considered as part of our basis allow us to work in a moving frame and to apply twisted boundary conditions. These features are implemented in the StateTwist_x class. We start with moving frames.

Boosts#

Consider a solution to the GPE in 1D:

\[\begin{gather*} \I\hbar \dot{\psi}(x, t) = -\frac{\hbar^2\psi''(x, t)}{2m} + g\abs{\psi(x, t)}^2\psi(x, t). \end{gather*}\]

Suppose we wish to study a solution that travels with velocity \(v\). I.e. something which should be static when considered as a function of \(x_v = x - vt\). As discussed in Galilean Covariance, we may consider several transformations of the form:

\[\begin{gather*} \psi(x, t) = e^{\I\phi(x_v, t)}\psi_v(x_v, t) \end{gather*}\]
  • Simple boost:

    \[\begin{gather*} \hbar\phi(x_v, t) = 0, \\ \I\hbar\dot{\psi}_v(x_v, t) = -\frac{\hbar^2\psi_v''(x_v, t)}{2m} + \overbrace{\I\hbar v \psi_v'(x_v)}^{-v\op{p}\psi_v} + g\abs{\psi_v(x_v, t)}^2\psi_v(x_v, t). \end{gather*}\]
  • Full Galilean Transformation

    \[\begin{gather*} \hbar\phi(x_v, t) = mvx_v + \tfrac{m}{2}v^2t, \\ \I\hbar\dot{\psi}_v(x_v, t) = -\frac{\hbar^2\psi_v''(x_v, t)}{2m} + g\abs{\psi_v(x_v, t)}^2\psi_v(x_v, t). \end{gather*}\]

The full transformation makes the covariance of the original equation manifest - the form of the equation stays the same. However, it does not simplify if the original equations are not Galilean covariant (e.g. if there is a modified dispersion due to a spin-orbit coupling.)

Twists (Bloch waves)#

Bloch’s theorem tells us that if we have a periodic potential with period \(L\), then we can classify the eigenstates in terms of their Bloch quasi-momentum \(\hbar k_B \in [-\hbar\pi/L, \hbar\pi/L)\) where the wavefunction is:

\[\begin{gather*} \psi(x) = e^{\I k_B x}u_{k_B}(x), \qquad \frac{-\pi}{L} \leq k_B < \frac{\pi}{L}, \end{gather*}\]

and \(u_{k_B}(x)\) is periodic. In our code, we store the periodic function \(u_{k_B}(x)\). The implementation must keep track of this twist phase in several places:

  • When setting or getting the physical wavefunction \(\psi(x)\), the twist must be applied or removed. This is done with get_psi() and set_psi() functions.

  • When applying the momentum operator, we must shift the momenta:

    \[\begin{gather*} \op{p}\psi(x) = -\I\hbar\pdiff{}{x}e^{\I k_B x}u(x) = e^{\I k_B x}\left(-\I\hbar\pdiff{}{x} + \hbar k_B\right)u(x) = e^{\I k_B x}(\op{p} + \hbar k_B)u(x). \end{gather*}\]

    When applying the momentum operator directly to \(u(x)\), we must therefore shift the discrete momenta \(k_n \rightarrow k_n + k_B\).

Note

If one works with a box of twice the size, then one has only half of the possible range of twists. This represents a general feature of Bloch waves: the twist allows you to consider solutions that will not fit in your periodic box, but that would fit in a larger box containing multiple unit cells. Thus, allowing for twists allows you to consider solutions to a periodic potential in an infinite universe. If you want to actually consider physics in a periodic universe, you should not allow twists.

Twists and Boosts#

Consider the GPE with general dispersion:

\[\begin{gather*} \I\hbar \dot{\psi}(x, t) = \bigl(E(\op{p}) + g\abs{\psi(x, t)}^2 + V(x, t)\bigr)\psi(x, t). \end{gather*}\]

When combining both twists \(k_B\) and boosts \(x_v = x-vt\), we boost the twisted wavefunction. Thus we have:

\[\begin{gather*} \psi(x, t) = \psi_v(x_v, t) = e^{\I k_B x_v}u_v(x_v, t) = e^{\I k_B x}u_v(x_v, t)\\ \I \hbar \dot{u}_v(x_v, t) = \bigl(E(\op{p} + \hbar k_B) - (\op{p}+\hbar k_B) v + g\abs{u_v(x_v)}^2 + V(x_v + vt, t)\bigr)u_v(x_v, t). \end{gather*}\]

The StateTwist_x thus works internally with \(u_v(x_v, t)\) on the abscissa \(x_v = x - vt\) where \(x\) are the abscissa in the “lab frame” (accessible from the property StateTwist_x.x_lab).

For comparison, here is what happens if we twist the boosted wavefunction:

\[\begin{gather*} \psi(x, t) = e^{\I k_B x}\tilde{u}(x, t) = e^{\I k_B x}\tilde{u}_v(x_v, t)\\ \I \hbar \dot{\tilde{u}}_v(x_v, t) = \bigl(E(\op{p} + \hbar k_B) - \op{p} v + g\abs{\tilde{u}_v(x_v)}^2 + V(x_v + vt, t)\bigr)\tilde{u}_v(x_v, t),\\ \tilde{u}_v(x_v, t) = e^{-\I k_B vt}u_v(x_v, t). \end{gather*}\]

The resulting equation does not have \(\op{p}\) shifted in the boost term. These two solutions are related by a phase.

Numerical Examples#

The base code for solving the GPE in 1D with a single component is provided in the file bec.py. It uses the pytimeode project to implement the time dependence. This requires providing a State class with come methods that tell the evolvers how to compute the time derivative. The compute_dy_dt() method allows one to use generic evolvers like the Adams-Bashforth-Milne (ABM) predictor corrector method, and the apply_exp_K()/apply_exp_V() pair allow one to use the faster (but less accurate) Split-Operator evolvers. See the code and pytimeode documentation for details.

Default Example#

The default example in the code is a BEC in a Harmonic Oscillator potential. We we obtain the initial state using the Thomas-Fermi approximation:

Note: Here we use the code in gpe.bec_basic which is a simplified version of the code for pedagogical purposes. Production code should use gpe.bec. One major difference is the introduction of StateBase.basis which handles the discretization (allowing, for example, axial symmetric abscissa).

%pylab inline --no-import-all
from gpe.imports import *
from IPython.display import display, clear_output
from gpe import bec_basic
%pylab is deprecated, use %matplotlib inline and import the required libraries.
Populating the interactive namespace from numpy and matplotlib
[I 19:59:46 numexpr.utils] NumExpr defaulting to 2 threads.
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[2], line 2
      1 get_ipython().run_line_magic('pylab', 'inline --no-import-all')
----> 2 from gpe.imports import *
      3 from IPython.display import display, clear_output
      4 from gpe import bec_basic

File ~/checkouts/readthedocs.org/user_builds/gpe/checkouts/latest/src/gpe/imports.py:28
     26 from mmfutils.plot import imcontourf  # noqa: E402
     27 from gpe.minimize import MinimizeState  # noqa: E402
---> 28 from gpe.utils import evolve_to, evolve, evolves  # noqa: E402
     29 from gpe.plot_utils import MPLGrid  # noqa: E402
     30 from pytimeode.evolvers import EvolverSplit, EvolverABM  # noqa: E402

File ~/checkouts/readthedocs.org/user_builds/gpe/checkouts/latest/src/gpe/utils.py:35
     32 from persist.objects import Archivable
     33 from persist.archive import Archive
---> 35 from pytimeode.evolvers import EvolverABM
     36 from pytimeode.mixins import ArrayStateMixin
     37 from pytimeode.interfaces import implementer, IStateForABMEvolvers

File ~/checkouts/readthedocs.org/user_builds/gpe/conda/latest/lib/python3.14/site-packages/pytimeode/__init__.py:2
      1 from . import interfaces
----> 2 from . import mixins
      3 from . import evolvers
      5 __all__ = ["interfaces", "mixins", "evolvers"]

File ~/checkouts/readthedocs.org/user_builds/gpe/conda/latest/lib/python3.14/site-packages/pytimeode/mixins.py:39
     30 from .interfaces import (
     31     IState,
     32     IStateApply,
   (...)     36     implementer,
     37 )
     38 from . import interfaces
---> 39 from .utils import expr
     41 __all__ = [
     42     "StateMixin",
     43     "StatesMixin",
   (...)     47     "ArraysStateWithBraketMixin",
     48 ]
     51 @implementer(IState)
     52 class StateMixin:

File ~/checkouts/readthedocs.org/user_builds/gpe/conda/latest/lib/python3.14/site-packages/pytimeode/utils/expr.py:18
     15 import numpy as np
     17 try:
---> 18     import sympy
     19 except ImportError:
     20     sympy = None

File ~/checkouts/readthedocs.org/user_builds/gpe/conda/latest/lib/python3.14/site-packages/sympy/__init__.py:77
     70 from .logic import (to_cnf, to_dnf, to_nnf, And, Or, Not, Xor, Nand, Nor,
     71         Implies, Equivalent, ITE, POSform, SOPform, simplify_logic, bool_map,
     72         true, false, satisfiable)
     74 from .assumptions import (AppliedPredicate, Predicate, AssumptionsContext,
     75         assuming, Q, ask, register_handler, remove_handler, refine)
---> 77 from .polys import (Poly, PurePoly, poly_from_expr, parallel_poly_from_expr,
     78         degree, total_degree, degree_list, LC, LM, LT, pdiv, prem, pquo,
     79         pexquo, div, rem, quo, exquo, half_gcdex, gcdex, invert,
     80         subresultants, resultant, discriminant, cofactors, gcd_list, gcd,
     81         lcm_list, lcm, terms_gcd, trunc, monic, content, primitive, compose,
     82         decompose, sturm, gff_list, gff, sqf_norm, sqf_part, sqf_list, sqf,
     83         factor_list, factor, intervals, refine_root, count_roots, all_roots,
     84         real_roots, nroots, ground_roots, nth_power_roots_poly, cancel,
     85         reduced, groebner, is_zero_dimensional, GroebnerBasis, poly,
     86         symmetrize, horner, interpolate, rational_interpolate, viete, together,
     87         BasePolynomialError, ExactQuotientFailed, PolynomialDivisionFailed,
     88         OperationNotSupported, HeuristicGCDFailed, HomomorphismFailed,
     89         IsomorphismFailed, ExtraneousFactors, EvaluationFailed,
     90         RefinementFailed, CoercionFailed, NotInvertible, NotReversible,
     91         NotAlgebraic, DomainError, PolynomialError, UnificationFailed,
     92         GeneratorsError, GeneratorsNeeded, ComputationFailed,
     93         UnivariatePolynomialError, MultivariatePolynomialError,
     94         PolificationFailed, OptionError, FlagError, minpoly,
     95         minimal_polynomial, primitive_element, field_isomorphism,
     96         to_number_field, isolate, round_two, prime_decomp, prime_valuation,
     97         galois_group, itermonomials, Monomial, lex, grlex,
     98         grevlex, ilex, igrlex, igrevlex, CRootOf, rootof, RootOf,
     99         ComplexRootOf, RootSum, roots, Domain, FiniteField, IntegerRing,
    100         RationalField, RealField, ComplexField, PythonFiniteField,
    101         GMPYFiniteField, PythonIntegerRing, GMPYIntegerRing, PythonRational,
    102         GMPYRationalField, AlgebraicField, PolynomialRing, FractionField,
    103         ExpressionDomain, FF_python, FF_gmpy, ZZ_python, ZZ_gmpy, QQ_python,
    104         QQ_gmpy, GF, FF, ZZ, QQ, ZZ_I, QQ_I, RR, CC, EX, EXRAW,
    105         construct_domain, swinnerton_dyer_poly, cyclotomic_poly,
    106         symmetric_poly, random_poly, interpolating_poly, jacobi_poly,
    107         chebyshevt_poly, chebyshevu_poly, hermite_poly, hermite_prob_poly,
    108         legendre_poly, laguerre_poly, apart, apart_list, assemble_partfrac_list,
    109         Options, ring, xring, vring, sring, field, xfield, vfield, sfield)
    111 from .series import (Order, O, limit, Limit, gruntz, series, approximants,
    112         residue, EmptySequence, SeqPer, SeqFormula, sequence, SeqAdd, SeqMul,
    113         fourier_series, fps, difference_delta, limit_seq)
    115 from .functions import (factorial, factorial2, rf, ff, binomial,
    116         RisingFactorial, FallingFactorial, subfactorial, carmichael,
    117         fibonacci, lucas, motzkin, tribonacci, harmonic, bernoulli, bell, euler,
   (...)    138         Znm, elliptic_k, elliptic_f, elliptic_e, elliptic_pi, beta, mathieus,
    139         mathieuc, mathieusprime, mathieucprime, riemann_xi, betainc, betainc_regularized)

File ~/checkouts/readthedocs.org/user_builds/gpe/conda/latest/lib/python3.14/site-packages/sympy/polys/__init__.py:79
      3 __all__ = [
      4     'Poly', 'PurePoly', 'poly_from_expr', 'parallel_poly_from_expr', 'degree',
      5     'total_degree', 'degree_list', 'LC', 'LM', 'LT', 'pdiv', 'prem', 'pquo',
   (...)     65     'field', 'xfield', 'vfield', 'sfield'
     66 ]
     68 from .polytools import (Poly, PurePoly, poly_from_expr,
     69         parallel_poly_from_expr, degree, total_degree, degree_list, LC, LM,
     70         LT, pdiv, prem, pquo, pexquo, div, rem, quo, exquo, half_gcdex, gcdex,
   (...)     76         nth_power_roots_poly, cancel, reduced, groebner, is_zero_dimensional,
     77         GroebnerBasis, poly)
---> 79 from .polyfuncs import (symmetrize, horner, interpolate,
     80         rational_interpolate, viete)
     82 from .rationaltools import together
     84 from .polyerrors import (BasePolynomialError, ExactQuotientFailed,
     85         PolynomialDivisionFailed, OperationNotSupported, HeuristicGCDFailed,
     86         HomomorphismFailed, IsomorphismFailed, ExtraneousFactors,
   (...)     91         MultivariatePolynomialError, PolificationFailed, OptionError,
     92         FlagError)

File ~/checkouts/readthedocs.org/user_builds/gpe/conda/latest/lib/python3.14/site-packages/sympy/polys/polyfuncs.py:10
      8 from sympy.polys.polyoptions import allowed_flags, build_options
      9 from sympy.polys.polytools import poly_from_expr, Poly
---> 10 from sympy.polys.specialpolys import (
     11     symmetric_poly, interpolating_poly)
     12 from sympy.polys.rings import sring
     13 from sympy.utilities import numbered_symbols, take, public

File ~/checkouts/readthedocs.org/user_builds/gpe/conda/latest/lib/python3.14/site-packages/sympy/polys/specialpolys.py:298
    294     return dmp_mul(f, h, n, K), dmp_mul(g, h, n, K), h
    296 # A few useful polynomials from Wang's paper ('78).
--> 298 from sympy.polys.rings import ring
    300 def _f_0():
    301     R, x, y, z = ring("x,y,z", ZZ)

File ~/checkouts/readthedocs.org/user_builds/gpe/conda/latest/lib/python3.14/site-packages/sympy/polys/rings.py:31
     27 from sympy.polys.polyoptions import (Domain as DomainOpt,
     28                                      Order as OrderOpt, build_options)
     29 from sympy.polys.polyutils import (expr_from_dict, _dict_reorder,
     30                                    _parallel_dict_from_expr)
---> 31 from sympy.printing.defaults import DefaultPrinting
     32 from sympy.utilities import public, subsets
     33 from sympy.utilities.iterables import is_sequence

File ~/checkouts/readthedocs.org/user_builds/gpe/conda/latest/lib/python3.14/site-packages/sympy/printing/__init__.py:27
     23 from .jscode import jscode, print_jscode
     25 from .julia import julia_code
---> 27 from .mathematica import mathematica_code
     29 from .octave import octave_code
     31 from .gtk import print_gtk

File <frozen importlib._bootstrap>:1371, in _find_and_load(name, import_)
   1367         getattr(getattr(module, "__spec__", None), "_initializing", False)):
   1368         with _ModuleLockManager(name):
   1369             module = sys.modules.get(name, _NEEDS_LOADING)
   1370             if module is _NEEDS_LOADING:
-> 1371                 return _find_and_load_unlocked(name, import_)
   1372 
   1373         # Optimization: only call _bootstrap._lock_unlock_module() if
   1374         # module.__spec__._initializing is True.

File <frozen importlib._bootstrap>:1345, in _find_and_load_unlocked(name, import_)
   1341         try:
   1342             module = _load_unlocked(spec)
   1343         finally:
   1344             if parent_spec:
-> 1345                 parent_spec._uninitialized_submodules.pop()
   1346     if parent:
   1347         # Set the module as an attribute on its parent.
   1348         parent_module = sys.modules[parent]

File <frozen importlib._bootstrap>:953, in _load_unlocked(spec)
    949         module = sys.modules.pop(spec.name)
    950         sys.modules[spec.name] = module
    951         _verbose_message('import {!r} # {!r}', spec.name, spec.loader)
    952     finally:
--> 953         spec._initializing = False
    954 
    955     return module

File <frozen importlib._bootstrap_external>:755, in _LoaderBasics.exec_module(self, module)
    753     def exec_module(self, module):
    754         """Execute the module."""
--> 755         code = self.get_code(module.__name__)
    756         if code is None:
    757             raise ImportError(f'cannot load module {module.__name__!r} when '
    758                               'get_code() returns None')

File <frozen importlib._bootstrap_external>:888, in SourceLoader.get_code(self, fullname)
    884                         pass
    885                     else:
    886                         _bootstrap._verbose_message('{} matches {}', bytecode_path,
    887                                                     source_path)
--> 888                         return _compile_bytecode(bytes_data, name=fullname,
    889                                                  bytecode_path=bytecode_path,
    890                                                  source_path=source_path)
    891         if source_bytes is None:

File <frozen importlib._bootstrap_external>:515, in _compile_bytecode(data, name, bytecode_path, source_path)
    511     code = marshal.loads(data)
    512     if isinstance(code, _code_type):
    513         _bootstrap._verbose_message('code object from {!r}', bytecode_path)
    514         if source_path is not None:
--> 515             _imp._fix_co_filename(code, source_path)
    516         return code
    517     else:
    518         raise ImportError(f'Non-code object in {bytecode_path!r}',

KeyboardInterrupt: 
reload(bec_basic)
from gpe.bec_basic import State, u
s = State(Nxyz=(128,), Lxyz=(40 * u.micron,))
s.plot()
from gpe import bec, minimize
from pytimeode.evolvers import EvolverABM
from mmfutils.contexts import FPS
s = bec.State(Nxyz=(256,), Lxyz=(10,), x_TF=5/2)
s = minimize.MinimizeState(s, fix_N=True).minimize()
h = s.hbar / np.sqrt(2*s.m*s.g*s.get_density().max())
s.cooling_phase = 1+0.1j
psi = s.get_psi()
x = s.get_xyz()[0]
x0 = 2.5/2*0
theta = np.where(x < x0, 0, np.pi)
s.set_psi(np.exp(1j*theta)*psi*np.tanh((x-x0)/np.sqrt(2)/h))
ev = EvolverABM(s, dt=0.1*s.t_scale)
steps = 1000
for frame in FPS(1000):
    ev.evolve(steps)
    plt.clf()
    ev.y.plot()
    ax = plt.twinx()
    ax.plot(x, np.angle(ev.y/ev.y[s.basis.Nxyz[0]//2]), 'C1')
    clear_output(wait=True)
    display(plt.gcf())
    

Testing#

To test the code, we will set \(g=0\) and use the exact solution for the Harmonic Oscillator:

\[\begin{gather*} \psi(x) \propto e^{-(x/a)^2/2}, \qquad a^2 = \frac{\hbar}{m\omega} \end{gather*}\]
def get_err(N=128, L=24 * u.micron):
    s = State(Nxyz=(N,), Lxyz=(L,))
    s.g = 0
    a = np.sqrt(u.hbar / u.m / s.ws[0])
    x = s.xyz[0]
    psi_0 = np.exp(-((x / a) ** 2) / 2.0)
    s[...] = psi_0
    s.normalize()
    dy = s.empty()
    s.compute_dy_dt(dy=dy, subtract_mu=True)
    return abs(dy[...]).max()


Ns = 2 ** np.arange(2, 8)
errs = list(map(get_err, Ns))
plt.semilogy(Ns, errs, "-+")

Question#

Why are \(L=23\) microns and \(N\approx 2^6\) optimal?

Partial Solution#

s = State(Nxyz=(46,), Lxyz=(23 * u.micron,))
a = np.sqrt(u.hbar / u.m / s.ws[0])
L, N = s.Lxyz[0], s.Nxyz[0]
k_max = np.pi * (N - 2) / L  # For Khalid...
print(k_max, s.kxyz[0].max())
print(np.exp(-((L / 2 / a) ** 2) / 2))  # Wavefunction drops by factor of macheps
psi_0 = s.xyz[0] * np.exp(-((s.xyz[0] / a) ** 2) / 2)
plt.semilogy(np.fft.fftshift(s.kxyz[0]), np.fft.fftshift(abs(np.fft.fft(psi_0))), "-+")

So we see that for the ground state \(k\) needs to go up to \(6\).

Exact Solution with Interactions#

Although there is no analytic solution for the harmonic oscillator with an interaction, we can construct a potential so as to have an exact solution for an interacting system. In particular, we solve the time-independent GPE for the potential to obtain:

\[\begin{gather*} V_\psi(x) - \mu = \frac{\frac{\hbar^2\nabla^2\psi(x)}{2m} - gn(x)\psi(x)}{\psi(x)} = \frac{\hbar^2\nabla^2\psi(x)}{2m \psi(x)} - gn(x). \end{gather*}\]

Given any real wavefunction \(\psi(x) > 0\) without nodes, we can simply apply this formula to determine an external potential in which this state will be a stationary state with chemical potential \(\mu\). This will also work for functions with nodes as long as \(\nabla^2\psi(x)\) has nodes in the same location.

For example, we can construct a potential with a Gaussian ground state and zero chemical potential:

\[\begin{gather*} \psi(x) = \sqrt{n_0}\exp\left\{\frac{-x^2}{2a^2}\right\}, \qquad \psi''(x) = \frac{x^2-a^2}{a^4}\psi(x),\qquad V_\psi(x) = \frac{\hbar^2}{2m}\frac{x^2-a^2}{a^4} - gn_0e^{-x^2/a^2}. \end{gather*}\]

Notice that if \(g=0\) we end up with shifted harmonic oscillator potential (check that the coefficients agree with the solution given above).

%pylab inline --no-import-all
from IPython.display import display, clear_output
from gpe import bec_basic

reload(bec_basic)
from gpe.bec_basic import State, u

s = State(Nxyz=(64,), Lxyz=(23 * u.micron,))
a = np.sqrt(u.hbar / u.m / s.ws[0])
x = s.xyz[0]
psi_0 = np.exp(-((x / a) ** 2) / 2)


class State1(State):
    def __init__(self, *v, **kw):
        self._V_psi = 0  # Set this because State.__init__() needs a potential
        State.__init__(self, *v, **kw)
        a = np.sqrt(u.hbar / u.m / self.ws[0])
        x = self.xyz[0]
        k = 1.0 / 2.0 / a**2
        psi_0 = 4.0 * np.exp(-((x / a) ** 2) / 2)
        n_0 = abs(psi_0) ** 2
        self._V_psi = u.hbar**2 / 2.0 / u.m * (4 * (k * x) ** 2 - 2 * k) - self.g * n_0
        self.data[...] = psi_0
        self.pre_evolve_hook()

    def get_Vext(self):
        return self._V_psi


s = State1(Nxyz=(64,), Lxyz=(23 * u.micron,))
s.plot()
plt.plot(x, s.get_Vext())
dy = s.empty()
s.compute_dy_dt(dy=dy, subtract_mu=False)
abs(dy[...]).max()

Question#

We explicitly constructed the state to have \(\mu = \hbar\omega = 0\). Why then is the energy \(E\approx -3.3348\) negative?

Here we start from another state (\(\psi(x) = 1\)) and evolve with imaginary time to see if we recover the state we initially constructed the potential from.

from mmfutils.contexts import NoInterrupt
from pytimeode.evolvers import EvolverSplit, EvolverABM
from IPython.display import display, clear_output

s = State1(Nxyz=(64 * 4,), Lxyz=(23 * u.micron,))
assert np.allclose(s._N, s.get_N())

s[...] = 1.0
s.normalize()
s.cooling_phase = 1j

E_max = u.hbar**2 * np.abs(s.kxyz).max() ** 2 / 2.0 / u.m

## e = EvolverSplit(s, dt=0.01*u.hbar/E_max, normalize=True)
e = EvolverABM(s, dt=0.1 * u.hbar / E_max, normalize=True)

with NoInterrupt(ignore=True) as interrupted:
    while e.y.t < 4 * u.ms and not interrupted:
        e.evolve(100)
        plt.clf()
        e.y.plot()
        display(plt.gcf())
        clear_output(wait=True)

Question#

Although the state \(\psi(x)\) is a node-less stationary solution of the GPE with external potential \(V_\psi(x)\) constructed above, it is not necessarily the ground state in this potential. Non-linear problems do not necessarily follow the same rules as the linear Schrödinger equation where one can prove that the ground state (and only the ground state) must have no nodes. Here multiple states can be nodeless.

  • Is this the ground state?

  • Can you construct a different problem where the solution \(\psi(x)\) in the potential \(V_\psi(x)\) is different (i.e. if this is the ground state, find a different potential where the constructed solution is not a ground state, or vice versa.)

  • What are the necessary and sufficient conditions for \(\psi(x)\) to be the ground state of the GPE with external potential \(V_\psi(x)\)? (Hard)

Here is an example of real-time evolution of a state with domain wall phase imprinted. Locally this domain wall has the structure of a dark soliton, but now since we are in a trap, the solution is only approximate. We evolve this state with some imaginary time cooling to allow energy to be removed from the system. As a result, we see the soliton oscillate back and forth with larger and larger amplitude but shallower and shallower depth until it eventually is lost.

from mmfutils.contexts import NoInterrupt
from pytimeode.evolvers import EvolverSplit, EvolverABM
from IPython.display import display, clear_output

s = State1(Nxyz=(64 * 4,), Lxyz=(23 * u.micron,))
s *= np.sign(s.xyz[0] - 0.5)
s.cooling_phase = 1 + 0.01j

E_max = u.hbar**2 * np.abs(s.kxyz).max() ** 2 / 2.0 / u.m
# e = EvolverSplit(s, dt=0.01*u.hbar/E_max, normalize=True)
e = EvolverABM(s, dt=0.5 * u.hbar / E_max, normalize=True)

with NoInterrupt(ignore=True) as interrupted:
    while e.y.t < 40 * u.ms and not interrupted:
        e.evolve(100)
        plt.clf()
        e.y.plot()
        display(plt.gcf())
        clear_output(wait=True)

Bright Solitons#

There is nice integrable 1D model that is useful for testing code in the case of attractive interactions (the so-called focusing NLSEQ):

\[\begin{gather*} \DeclareMathOperator{\sech}{sech} \psi(x) = b\sech\frac{b \sqrt{gm} x}{\hbar}, \qquad -\frac{\hbar^2 \psi''(x)}{2m} - g\abs{\psi}^2\psi = -\frac{b^2 g}{2}\psi \end{gather*}\]
import numpy as np
import sympy
sympy.init_printing()
m, hbar, b, g = sympy.var("m, hbar, b, g", positive=True)
x = sympy.var("x", real=True)
psi = b / sympy.cosh(b * sympy.sqrt(g * m) * x / hbar)
n = psi**2
E = sympy.trigsimp((-(hbar**2) * psi.diff(x, x) / 2 / m - g * n * psi) / psi).together()
N_1D = (psi**2).integrate((x, -sympy.oo, sympy.oo))
sympy.Eq(sympy.S("E_"), E), sympy.Eq(sympy.S("N_1D"), N_1D)

One might ask if similar objects are possible in 3D. Consider a self-bound object with wavefunction \(\psi_{D}(\vect{x})\) and total particle number \(N = \int \d^{d}\vect{x}\abs{\psi_D(x)}^2\). Now scale it as follows so that the particle number is unchanged:

\[\begin{gather*} \psi_{s} = s^{D/2} \psi_{D}(s\vect{x}). \end{gather*}\]

The kinetic energy and potential energy thus scale as:

\[\begin{gather*} K_s = s^{-2}K_D, \qquad V_s = s^{D}V_D. \end{gather*}\]

Thus, such an object will have a minimum energy when \(2s^{-3}K_D = Ds^{D-1}V_D\), or

\[\begin{gather*} s^{D+2} = \frac{2K_D}{DV_D}. \end{gather*}\]

2D Scissor Modes (incomplete)#

We base the physical parameters on a system like [Marago:2001] so we can explore damping of the normal modes of the system. They have trapping frequencies of \(\omega_y=\omega_z = 128\)Hz and \(\omega_x = \sqrt{8}\omega_y\) and \(N = 2\times 10^4\) particles. In the TF approximation at \(T=0\) this corresponds to \(\mu = \)

[Marago:2001]: http://dx.doi.org/10.1103/PhysRevLett.86.3938 (Onofrio Marag`o, Gerald Hechenblaikner, Eleanor Hodby, and Christopher Foot, “Temperature Dependence of Damping and Frequency Shifts of the Scissors Mode of a Trapped Bose-Einstein Condensate”, Phys. Rev. Lett. 86, 3938–3941 (2001) )

%pylab inline --no-import-all
from IPython.display import display, clear_output
from gpe import bec_basic

reload(bec_basic)
from pytimeode.evolvers import EvolverABM
from mmfutils.contexts import NoInterrupt
from gpe.bec_basic import State, u

s = State()
s.cooling_phase = 1j
s.t = -100 * u.ms
e = EvolverABM(s, dt=0.001)
with NoInterrupt(ignore=True) as interrupted:
    while not interrupted:
        e.evolve(500)
        plt.clf()
        e.y.plot()
        display(plt.gcf())
        clear_output(wait=True)
s = e.get_y()
n = s.get_density()
x, y, z = s.xyz
plt.plot(x.ravel(), n.sum(axis=-1).sum(axis=-1))
s.mu, s._mu

Interacting with a BEC#

%pylab inline --no-import-all
from IPython.display import display, clear_output
from gpe import bec_basic

reload(bec_basic)
from gpe.bec_basic import State, u

s = State(Nxyz=(64,), Lxyz=(23 * u.micron,))
a = np.sqrt(u.hbar / u.m / s.ws[0])
x = s.xyz[0]
psi_0 = np.exp(-((x / a) ** 2) / 2)


class State1(State):
    def get_Vext(self):
        V_ext = State.get_Vext(self)
        if self.t > 0:
            V_ext += 0.01 * np.cos(s.ws[0] * self.t) * self.xyz[0]
        return V_ext


s = State1(Nxyz=(64,), Lxyz=(23 * u.micron,))
s.plot()
plt.plot(x, s.get_Vext())
from mmfutils.contexts import NoInterrupt
from pytimeode.evolvers import EvolverSplit, EvolverABM
from IPython.display import display, clear_output

s = State1(Nxyz=(64 * 4,), Lxyz=(23 * u.micron,))
s.cooling_phase = 1 + 0.01j

E_max = u.hbar**2 * np.abs(s.kxyz).max() ** 2 / 2.0 / u.m
# e = EvolverSplit(s, dt=0.01*u.hbar/E_max, normalize=True)
e = EvolverABM(s, dt=0.5 * u.hbar / E_max, normalize=True)

with NoInterrupt(ignore=True) as interrupted:
    while e.y.t < 40 * u.ms and not interrupted:
        e.evolve(100)
        plt.clf()
        e.y.plot()
        plt.plot(s.xyz[0], e.y.get_Vext())
        display(plt.gcf())
        clear_output(wait=True)

Errors in Derivatives#

def f(x=0.5):
    return np.cos(x)


def df(x=0.5):
    return -np.sin(x)


def df1(h, x=0.5):
    return (f(x + h) - f(x)) / h


def err(h):
    return abs(df1(h) - df())


hs = 10 ** (np.linspace(-16, -1, 100))
errs = list(map(err, hs))
%pylab inline --no-import-all
plt.loglog(hs, errs, "-+")
plt.plot(hs, abs(hs * f() / 2.0), "--")
plt.plot(hs, abs(np.finfo(np.double).eps / hs), ":")