In [12]:
%matplotlib inline
%reload_ext autoreload
%autoreload 2
Simulating the time-propagation of a Gaussian Hamiltonian using a continuous-variable (CV) quantum circuit is simple with SFOpenBoson. In this tutorial, we will walk through a simple example using the forced quantum harmanic oscillator.
$$\hat{H} = \frac{\hat{p}^2}{2m} + \frac{1}{2}mω^2\hat{q}^2 - F\hat{q}$$The Hamiltonian of the forced qauntum harmonic oscillator is given by
where
- $m$ is the mass of the oscilllator,
- $ω$ is the frequency of oscillation,
- $F$ is a time-independent external force.
Let's define this Hamiltonian using OpenFermion, with $m = ω = 1$ and $F = 2$:
In [1]:
from openfermion.ops import QuadOperator
from openfermion.utils import commutator, normal_ordered
H = QuadOperator('q0 q0', 0.5) + QuadOperator('p0 p0', 0.5) - QuadOperator('q0', 2)
\begin{split}& \frac{d}{dt}\hat{q} = \frac{i}{\hbar}[\hat{H}, \hat{q}] = \hat{p}\\ & \frac{d}{dt}\hat{p} = \frac{i}{\hbar}[\hat{H}, \hat{q}] = F-\hat{q}\end{split}In the Heisenberg picture, the time-evolution of the $\hat{q}$ and $\hat{p}$ operators is given by:
shouldn't that last one be $[\hat{H},\hat{p}]$ ?
We can double check these using OpenFermion:
In [2]:
(1j/2)*normal_ordered(commutator(H, QuadOperator('q0')), hbar=2)
Out[2]:
In [3]:
(1j/2)*normal_ordered(commutator(H, QuadOperator('p0')), hbar=2)
Out[3]:
\begin{split}&\hat{q}(t) = (\hat{q}(0)-F)\cos(t) + \hat{p}(0)\sin(t) + F\\ &\hat{p}(t) = (F-\hat{q}(0))\sin(t) + \hat{p}(0)\cos(t)\end{split}Assuming the oscillator has initial conditions $\hat{q}(0)$ and $\hat{p}(0)$, it's easy to solve this coupled set of linear differentials analytically, giving the parameterized solution:
Let's now attempt to simulate these dynamics directly in Strawberry Fields, solely from the Hamiltonian we defined above.
To simulate the time-propagation of the forced oscillator in StrawberryFields, we also need to impmort the
GaussianPropagation
class from the SFOpenBoson plugin:
In [5]:
import strawberryfields as sf
from strawberryfields.ops import *
from sfopenboson.ops import GaussianPropagation
GaussianPropagation
accepts the following arguments:
operator
: a bosonic Gaussian Hamiltonian, either in the form of aBosonOperator
orQuadOperator
.t
(float): the time propagation value. If not provided, default value is 1.mode
(str): By default,mode='local'
and the Hamiltonian is assumed to apply to only the applied qumodes. For example, ifQuadOperator('q0 p1') | | (q[2], q[4])
, thenq0
acts onq[2]
, andp1
acts onq[4]
.Alternatively, you can set
mode='global
, and the Hamiltonian is instead applied to the entire register by directly matching qumode numbers of the defined Hamiltonian; ie:q0
is applied toq[0[]
,p1
is applied toq[1]
, etc.Let's set up the one qumode quantum circuit, propagating the forced oscillator Hamiltonian
H
we defined in the previous section, starting from the initial location $(1, 0.5)$ in phase space, for time $t = 1.43$:
In [7]:
eng, q = sf.Engine(1)
with eng:
Xgate(1) | q[0]
Zgate(0.5) | q[0]
GaussianPropagation(H, 1.43) | q
Now, we can run this simulation using the Gaussian backend of Strawberry Fields, and output the location of the oscillator in phase space at time $t = 1.43$:
In [8]:
state = eng.run('gaussian')
state.means()
Out[8]:
\begin{split}&\langle\hat{q}(1.43)\rangle = (1-2)\cos(1.43) + 0.5\sin(1.43) + 2 = 2.35472,\\ &\langle\hat{p}(1.43)\rangle = (2-1)\sin(1.43) + 0.5\cos(1.43) = 1.06027,\end{split}We compare this to the analytic solution,
which is in good agreement with the Strawberry Fields result.
We can also print the CV gates applied by the engine, to see how our time-evolution operator $e^{-i\hat{H}t/\hbar}$ got decomposed:
In [9]:
eng.print_applied()
By looping over various values of $t$, we can plot the phase space location of the oscillator for various values of $t$.
Consider the following example:
In [10]:
eng, q = sf.Engine(1, hbar=2)
t_vals = np.arange(0, 6, 0.02)
results = np.zeros([2, len(t_vals)])
for step, t in enumerate(t_vals):
eng.reset()
with eng:
Xgate(1) | q[0]
Zgate(0.5) | q[0]
GaussianPropagation(H, t) | q
state = eng.run('gaussian')
results[:, step] = state.means()
Here, we are looping over the same circuit as above for values of $t$ wihtin the domain $0 ≤ t ≤ 6$, and storing the resulting expectation values $(\langle\hat{q}(t)\rangle, \langle\hat{p}t)\rangle)$ in the array
results
. We can plot this array in the phase space:
In [40]:
from matplotlib import pyplot as plt
plt.style.use('ggplot')
fig,ax = plt.subplots(figsize=(8,5))
ax.set_xlabel('q'); ax.set_ylabel('p')
plt.xlim((0.0,4.0))
ax.plot(*results);
In this tutorial, we'll walk through an exmaple of Hamiltonian simulation of a Bose-Hubbard model, using Strawberry Fields and OpenFermion.
OpenFermion provides a convenient Hamiltonian function to automatically generate Bose-Hubbard Hamiltonians on a 2-dimensional lattice. For exmaple, to generate a Bose-Hubbard Hamiltonian on a size $ 1 \times 2$ lattice, with on-site and nearest neighbor interactions, we do
In [42]:
from openfermion.hamiltonians import bose_hubbard
bose_hubbard(x_dimension=1, y_dimension=2, tunneling=1, interaction=2,
chemical_potential=0., dipole=3., periodic=False)
Out[42]:
For more inofrmation regarding this function, please see the OpenFermion documentation.
Let's use this capability, along with the Hamiltonian propagation and decomposition tools of the SFOpenBoson plugin, to perform Bose-Hubbard simulations in Strawberry Fields. Consider the Hamiltonian simulation algorithm in the Strawberry Fields documentation; to reproduce these results, we first generate a Bose-Hubbard Hamiltonian on a non-periodic $1 \times 2$ lattice, with tunneling coefficient -1, and on-site interaction strength 1.5.
In [43]:
H = bose_hubbard(1, 2, 1, 1.5)
To simulate the time-propagation of the Hamiltonian in StrawberryFields, we also need to impor the
BoseHubbardPropagation
class from the SFOpenBoson plugin:
In [44]:
import strawberryfields as sf
from strawberryfields.ops import *
from sfopenboson.ops import BoseHubbardPropagation
BoseHubbardPropagation
accepts the following arguments:
operator
: a Bose-Hubbard Hamiltonian, either in the form of aBosonOperator
orQuadOperator
.t
(float): the time propagation value. If not provided, default vaue is 1.k
(int): the number of products in the truncated Lie product formula. Increasing this parameter increases the numerical accuracy of the decomposition, but also increases the depth of the circuit and the computational time.mode
(str): By default,mode='local'
and the Hamiltonian is assumed to apply to only the applied qumodes. For example, ifQuadOperator('q0 p1') | (q[2], q[4])
, thenq0
acts onq[2]
, andp1
acts onq[4]
.Alternatively, you can set
mode='global'
, and the Hamiltonian is instead applied to the entire register by directly matching qumode numbers of the defined Hamiltonian; ie:q0
is applied toq[0]
,p1
is applied toq[1]
, etc.Let's set up the 2 qumode quantum circuit –– each mode corresponds to a node in the lattice –– and propagating the Bose-Hubbard Hamiltonian
H
we defined in the previous section, starting from the initial state $\big\lvert0,2\big\rangle$ in the Fock space, for time $t = 1.086$ and Lie product truncation $k = 20$:
In [45]:
eng, q = sf.Engine(2)
with eng:
Fock(2) | q[1]
BoseHubbardPropagation(H, 1.086, 20) | q
Now we can run this simulation using the Fock backend of Strawberry Fields, and output the Fock state probabilities at time $t = 1.086$:
NOTE In the Bose-Hubbard model, the number of particles in the system remains constant, so we do not need to increase the cutoff dimension of the simulation beyond the total number of photons in the initial state.
</div>
In [46]:
state = eng.run('fock', cutoff_dim=3)
state.fock_prob([2,0])
Out[46]:
In [47]:
state.fock_prob([1,1])
Out[47]:
In [48]:
state.fock_prob([0,2])
Out[48]:
We can see that this matches the results obtained in the Strawberry Fields documentation.
Note that, as in the forced quantum harmonic oscillator, tutorial, we can output the decomposition as applied by the Strawberry Fields engine using
eng.print_applied()
.
Alternatively, we are not bound to use the
bose_hubbard
function from OpenFermion; we can define our own Bose-Hubbard Hamiltonian using theBoseOperator
class. For example, consider a Bose-Hubbard model constrained to a 3-vertex cycle graph; that is, the graph formed by connecting 3 vertices to each other in a cycle.
In [49]:
from openfermion.ops import BosonOperator
Let's define this Hamiltonian using OpenFermion. First, constructing the tunneling terms between each pair of adjacent modes:
In [50]:
J = 1
H = BosonOperator('0 1^', -J) + BosonOperator('0^ 1', -J)
H += BosonOperator('0 2^', -J) + BosonOperator('0^ 2', -J)
H += BosonOperator('1 2^', -J) + BosonOperator('1^ 2', -J)
Next, let's add an on-site interaction term, with strength $U = 1.5$:
In [51]:
U = 1.5
H += BosonOperator('0^ 0 0^ 0', 0.5*U) - BosonOperator('0^ 0', 0.5*U)
H += BosonOperator('1^ 1 1^ 1', 0.5*U) - BosonOperator('1^ 1', 0.5*U)
H += BosonOperator('2^ 2 2^ 2', 0.5*U) - BosonOperator('2^ 2', 0.5*U)
NOTE If a Hamiltonian that cannot be written in the form of a Bose-Hubbard model is passed to a
BoseHubbardPropagation
, aBoseHubbardError
is returned.As before, we use
BoseHubbardPropagation
to simulate this model for time $t = 1.086$, starting from initial state $\big\lvert2,0\big\rangle$. Due to the increased size of this model, let's increase the Lie product truncation to $k = 100$:
In [52]:
eng, q = sf.Engine(3)
with eng:
Fock(2) | q[0]
BoseHubbardPropagation(H, 1.086, 100) | q
Running the circuit, and checking some output probabilities:
In [53]:
state = eng.run('fock', cutoff_dim=3)
for i in ([2,0,0], [1,1,0], [1,0,1], [0,2,0], [0,1,1], [0,0,2]):
print(state.fock_prob(i))
\begin{split}H = \begin{bmatrix} U & J\sqrt{2} & J\sqrt{2} & 0 & 0 & 0\\ J\sqrt{2} & 0 & J & J\sqrt{2} & J & 0\\ J\sqrt{2} & J & 0 & 0 & J & J\sqrt{2}\\ 0 & J\sqrt{2} & 0 & U & J\sqrt{2} & 0\\ 0 & J & J & J\sqrt{2} & 0 & J\sqrt{2}\\ 0 & 0& J\sqrt{2} & 0 & J\sqrt{2} & U \end{bmatrix}.\end{split}To verifiy this result, we can construct the $6\times6$ Hamiltonian matrix $H_{ij} = \langleφ_i\lvert\hat{H}\rvertφ_{ij}\rangle$ is a member of the set of allowed Fock states $\{∣2,0,0⟩,∣1,1,0⟩,∣1,0,1⟩,∣0,2,0⟩,∣0,1,1⟩,∣0,0,2⟩\}$. Performing these inner products, we find that
Therefore, using SciPy to perform the matrix exponential $e^{iHt}$ applied to the initial state:
In [54]:
from scipy.linalg import expm
Jr2 = J*np.sqrt(2)
H = np.array([[U , Jr2, Jr2, 0 , 0 , 0 ],
[Jr2, 0 , J , Jr2, J , 0 ],
[Jr2, J , 0 , 0 , J , Jr2],
[0 , Jr2, 0 , U , Jr2, 0 ],
[0 , J , J , Jr2, 0 , Jr2],
[0 , 0 , Jr2, 0 , Jr2, U ]])
np.abs(expm(-1j*H*1.086)[0])**2
Out[54]:
which agrees within reasonable numerical error with the Strawberry Fields simulation results.