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):
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:
The GPE follows from the following principle of minimal action:
with the GPE resulting from the Euler-Lagrange equations:
Projections and Cooling#
The standard GPE can be expressed as
where the Hamiltonian operator depends on the state \(\ket{\psi}\) through the density (hence, the non-linear Schr”odinger equation), and effects the operation
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
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:
This is guaranteed to reduce the energy.
Details including Quantum Friction
Consider evolving the state with an operator \(\op{A}\):
Assuming that there is no explicit time dependence in \(E[\ket{\psi}]\), the energy will evolve as
More generally – with explicit time-dependence – this can be expressed as
Complex time evolution evolves with \(\op{A} = \op{H}/(\I \hbar e^{\I\eta})\). Thus, if \(\eta = 0\), then \(\op{A} = \op{H}/\I\hbar\) is anti-hermitian, and we obtain Heisenberg’s equations of motion
where the energy is conserved, unless there is an explicit time-dependence (e.g., a stirring potential) doing work on the system. For imaginary-time evolution, \(\op{A} = -\op{H}/\hbar\) is hermitian:
Since the second term is the expectation of a positive-semi-definite operator \(\op{H}^\dagger\op{H}\), the energy is guaranteed to never increase unless external work is being done on the system through the first term.
This formalism allows for some generalizations as discussed in [Bulgac et al., 2013] that we call quantum friction. The idea here is to express
where \(\op{U}\) is a local operator (i.e. a time-dependent potential). In [Bulgac et al., 2013] we show that if we take \(\op{U}\propto \dot{n}(\op{x})\), then we can lower the energy with a local potential. This is not so useful for the GPE, but becomes very important for distributed multi-orbital functionals (like the SLDA used for fermions) where communication prohibits the action of non-local operators.
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
More generically, if we implement an evolution
and want to constrain a quantity \(\op{C}= \op{C}^\dagger\):
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:
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:
This can be implemented using Lagrange multipliers:
which gives the time-independent non-linear Schrödinger equation:
Solutions to this equation are “stationary” in the sense that their only time-evolution is a continual phase change (unobservable):
where the phase oscillates with frequency:
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\):
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:
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:
Using thermodynamics, we can derive the chemical potential and pressure:
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):
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:
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:
The introduction of this potential provides another length scale in the problem:
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:
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:
Length Scales#
We start by considering the dimensions of various quantities:
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:
Thus, there are only two fundamental dimensionless quantities - the interaction \(g\) and the density \(n\). The corresponding length scales are:
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:
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:
Thus we can introduce another dimensionless constant:
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:
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:
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:
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()andset_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:
When combining both twists \(k_B\) and boosts \(x_v = x-vt\), we boost the twisted wavefunction. Thus we have:
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:
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:
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:
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:
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):
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:
The kinetic energy and potential energy thus scale as:
Thus, such an object will have a minimum energy when \(2s^{-3}K_D = Ds^{D-1}V_D\), or
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), ":")