Note

Click here to download the full example code

# An introduction to the bosonic backend¶

Note

This tutorial is first in a series on the `bosonic`

backend. After getting acquainted
with the backend in this tutorial, go to part 2
to learn about how the backend can be used to sample non-Gaussian states. After that,
check out part 3 for a deep dive into using the `bosonic`

backend to simulate qubits encoded in photonic modes. These tutorials accompany
our research paper [1].

So far, Strawberry Fields allows its users to simulate photonic circuits using three
backends. Two of them, the `fock`

and `tf`

backends, represent the quantum state of the modes using the Fock or particle basis. These backends allow one to represent
arbitrary quantum states up to a photon cutoff, but this generality comes with an automatic
exponential increase in the space complexity as a function of the number of modes
with the base of the exponent scaling as the energy of the state of light.
At the exact opposite end of this trade-off is the `gaussian`

backend, in which there is no
exponential scaling in the memory required, but at the same time there is only a subset of states
that can be represented.

In this tutorial, we introduce the new `bosonic`

backend which borrows some of the best features
of all the previous backends. Specifically, the `bosonic`

backend is tailored to simulate states
which can be represented as a linear combination of Gaussian functions in phase space.
It provides very succinct descriptions of Gaussian states, just
like the `gaussian`

backend, but it can also provide descriptions of non-Gaussian states as well.
Moreover, like in the `gaussian`

backend, the application of the most common active and passive linear
optical operations, like the displacement `Dgate`

, squeezing `Sgate`

and beamsplitter `BSgate`

gates,
is extremely efficient.

To motivate the ideas behind the backend, we will investigate how to represent the highly non-classical
and non-Gaussian cat state. Once we build our intuition we will introduce the `bosonic`

backend and
investigate how it represents quantum states. Finally, we discuss why
it can provide advantages over other backends for many applications.

Note

We assume the reader is familiar with the basics of the phase-space picture of quantum mechanics, including Wigner functions. For the uninitiated, a good place to start is the CV quantum gate visualizations tutorial.

## Of Cats and Kets¶

Cat states are defined to be linear superpositions of two coherent states

where \(| \alpha \rangle\) is a coherent state with amplitude \(\alpha\), \(k\) is the phase parameter of the cat state and

is a normalization constant.

Given the universality of the Fock backend we can use it to simulate the preparation of this state and plot its Wigner function:

```
# Usual imports
import strawberryfields as sf
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
from matplotlib import cm
# Simulation and cat state parameters
nmodes = 1
cutoff = 30
q = 4.0
p = 0.0
hbar = 2
alpha = (q + 1j * p) / np.sqrt(2 * hbar)
k = 1
phi = np.pi * k
# SF program
prog_cat_fock = sf.Program(nmodes)
with prog_cat_fock.context as q:
sf.ops.Catstate(alpha=alpha, p=phi) | q
eng = sf.Engine("fock", backend_options={"cutoff_dim": cutoff, "hbar": hbar})
state = eng.run(prog_cat_fock).state
# We now plot it
xvec = np.linspace(-15, 15, 401)
W = state.wigner(mode=0, xvec=xvec, pvec=xvec)
Wp = np.round(W.real, 4)
scale = np.max(Wp.real)
nrm = mpl.colors.Normalize(-scale, scale)
plt.axes().set_aspect("equal")
plt.contourf(xvec, xvec, Wp, 60, cmap=cm.RdBu, norm=nrm)
plt.show()
```

Some parts of the Wigner function are easy to interpret: since the state
is a coherent superposition of two widely separated coherent states,
we expect to see two circular blobs centered around \((q,p)=\pm\sqrt{2 \hbar}(\Re(\alpha), \Im(\alpha))\).
Because a cat state is a *coherent* superposition of the two coherent states,
we also see interference fringes in between the “classical” states of the cat.
Mathematically, one can determine the nature of these interference fringes by
looking at the density matrix of a cat state given by

The transformation from density matrix to Wigner function is linear, so the first two terms exactly correspond to the Wigner functions of the two coherent states \(|\pm \alpha \rangle\) and are thus Gaussian functions centered around \(\boldsymbol{\mu} = \sqrt{2 \hbar}(\Re(\alpha), \Im(\alpha))\), with the usual vacuum noise giving them some finite width. The last two terms in the equation above are more complicated to interpret. We know that they are the Hermitian conjugates of each other \((\left[ e^{i \pi k} |-\alpha \rangle \langle \alpha |\right]^\dagger = e^{-i \pi k} |\alpha \rangle \langle -\alpha |)\) , and by elimination they must correspond to the interference features present close to the origin of phase space.

In our recent preprint [1], we show that the Wigner function of terms like \(|-\alpha \rangle \langle \alpha |\)
is also a Gaussian function, but with *complex* means \(\boldsymbol{\mu} = \sqrt{2 \hbar} i (\Im(\alpha),-\Re(\alpha))\).
Therefore, we can express the Wigner function of the cat state as a weighted sum of four Gaussians in phase space —
as long as we allow the Gaussians to have complex means.
Since a cat state can be written as a linear combination of Gaussian functions in phase space, we can simulate
the same circuit presented above using the `bosonic`

backend. Since this backend does not
use Fock states, we don’t need to pass a cutoff argument, thus we can write:

```
prog_cat_bosonic = sf.Program(nmodes)
with prog_cat_bosonic.context as q:
sf.ops.Catstate(alpha=alpha, p=phi) | q
eng = sf.Engine("bosonic", backend_options={"hbar": hbar})
state = eng.run(prog_cat_bosonic).state
```

We can now inspect the internal representation of the cat state inside
the `bosonic`

backend. To this end, we can print the attributes `means`

,
`covs`

and `weights`

.
The `means`

variable is a NumPy array containing the means of the
four Gaussians needed to describe the state.

```
means = state.means()
print(means)
```

Out:

```
[[ 4.+0.j 0.+0.j]
[-4.+0.j -0.+0.j]
[ 0.+0.j 0.-4.j]
[ 0.+0.j 0.+4.j]]
```

The first axis of the array is the one labelling the different Gaussians.
Similarly, `covs`

contains the covariance matrices of the four Gaussians:

```
covs = state.covs()
print(covs)
```

Out:

```
[[[1. 0.]
[0. 1.]]
[[1. 0.]
[0. 1.]]
[[1. 0.]
[0. 1.]]
[[1. 0.]
[0. 1.]]]
```

Finally, as noted earlier the Wigner function is a *weighted* sum of the
four different Gaussians, the actual `weights`

or coefficients are given by

```
weights = state.weights()
print(weights)
```

Out:

```
[ 5.0015e-01+0.0000e+00j 5.0015e-01+0.0000e+00j -1.5145e-04+7.2197e-05j
-1.5145e-04-7.2197e-05j]
```

Note that both the `weights`

and `means`

are complex.
With the information provided from the backend, we are ready to verify
that we got the correct cat. To this end, we will first create a simple
wrapper function to generate a Gaussian with mean `mu`

and covariance
matrix `V`

and a convenience function to evaluate it on a grid of points:

```
def gaussian_func_gen(mu, V):
"""Generates a function that when evaluated returns
the value of a normalized Gaussian specified in terms
of a vector of means and a covariance matrix.
Args:
mu (array): vector of means
V (array): covariance matrix
Returns:
(callable): a normalized Gaussian function
"""
Vi = np.linalg.inv(V)
norm = 1.0 / np.sqrt(np.linalg.det(2 * np.pi * V))
fun = lambda x: norm * np.exp(-0.5 * (x - mu) @ Vi @ (x - mu))
return fun
def evaluate_fun(fun, xvec, yvec):
"""Evaluate a function a 2D in a grid of points.
Args:
fun (callable): function to evaluate
xvec (array): values of the first variable of the function
yvec (array): values of the second variable of the function
Returns:
(array): value of the function in the grid
"""
return np.array([[fun(np.array([x, y])) for x in xvec] for y in xvec])
funs = [gaussian_func_gen(means[i], covs[i]) for i in range(len(means))]
Wps = [weights[i] * evaluate_fun(funs[i], xvec, xvec) for i in range(len(weights))]
```

The list `Wps`

contains the values of the different Gaussians making
the Wigner function of the cat state. We can easily verify that when we
sum the different components for every point in phase space we obtain
a zero imaginary part:

```
print(np.allclose(sum(Wps).imag, 0))
```

Out:

```
True
```

In fact, we can verify that the last two components are the complex conjugate of each other (thus their sum is real), and that the first two components are strictly real:

```
print(np.allclose(Wps[2], Wps[3].conj()))
print(np.allclose(Wps[0].imag, 0))
print(np.allclose(Wps[1].imag, 0))
```

Out:

```
True
True
True
```

We can look individually at each of the Gaussians as well:

```
fig, axs = plt.subplots(1, 4, figsize=(10, 2.2))
for i in range(4):
Wp = np.round(Wps[i].real, 4)
axs[i].contourf(xvec, xvec, Wp, 60, cmap=cm.RdBu, norm=nrm)
if i != 0:
axs[i].set_yticks([])
plt.show()
```

And also put them together to obtain exactly the same Wigner function from before:

```
Wcat = sum(Wps)
plt.axes().set_aspect("equal")
plt.contourf(xvec, xvec, Wcat.real, 60, cmap=cm.RdBu, norm=nrm)
plt.show()
```

## Why is the bosonic backend useful?¶

Now that we understand the internal workings of the new `bosonic`

backend, let’s illustrate its utility. To this end, let us consider
a slightly more complicated circuit where we displace a cat state.
This is how it looks in the Fock backend:

```
x = 6
y = 0
beta = (x + 1j * y) / np.sqrt(2 * hbar)
prog_cat_fock_displaced = sf.Program(nmodes)
with prog_cat_fock_displaced.context as q:
sf.ops.Catstate(alpha=alpha, p=phi) | q
sf.ops.Dgate(beta) | q
eng = sf.Engine("fock", backend_options={"cutoff_dim": cutoff, "hbar": hbar})
state = eng.run(prog_cat_fock_displaced).state
```

Plotting the Wigner function of the returned state:

```
W = state.wigner(mode=0, xvec=xvec, pvec=xvec)
plt.axes().set_aspect("equal")
plt.contourf(xvec, xvec, W, 60, cmap=cm.RdBu, norm=nrm)
plt.show()
```

This plot does not meet our expectation that a `Dgate`

gate simply
displaces the Wigner function of a state. Indeed, we see that significant distortion occurs in the
right-hand-side .
This happened because the `cutoff`

we choose is not sufficient to faithfully
represent the *displaced* cat. A simple solution to this problem is to increase
the `cutoff`

in the simulation, but that will lead to an increase in memory and
a slower simulation.
The behaviour of the `fock`

backend can be contrasted
with the one of the `bosonic`

backend where we obtain

```
prog_cat_bosonic_displaced = sf.Program(nmodes)
with prog_cat_bosonic_displaced.context as q:
sf.ops.Catstate(alpha=alpha, p=phi) | q
sf.ops.Dgate(beta) | q
eng = sf.Engine("bosonic", backend_options={"hbar": hbar})
state = eng.run(prog_cat_bosonic_displaced).state
```

Just like in the case of the `fock`

backend,
we can also use the `wigner`

method to generate Wigner functions:

```
Wps = state.wigner(mode=0, xvec=xvec, pvec=xvec)
plt.axes().set_aspect("equal")
plt.contourf(xvec, xvec, Wps, 60, cmap=cm.RdBu, norm=nrm)
plt.show()
```

We can easily verify that in the internal representation in the backend we have only modified the displacement relative to the first program we investigated:

```
print(state.means())
```

Out:

```
[[10.+0.j 0.+0.j]
[ 2.+0.j 0.+0.j]
[ 6.+0.j 0.-4.j]
[ 6.+0.j 0.+4.j]]
```

## Conclusions and Outlook¶

In this tutorial, we have introduced the new `bosonic`

backend of Strawberry Fields,
explained the basic idea of how it represents quantum states and
showcased some of the advantages it has with respect to other backends.
We observed that when the energy of the cat state being considered was
increased by displacement, the Fock backend gave unexpected results,
and it actually requires higher cutoff and more memory for accurate results.
On the other hand, the bosonic backend can quickly and easily deal with
situations such as this on by merely changing the means and covariance
matrices of the represented state.
For a more advanced feature, check out part 2
of this series to see how the bosonic backend can be used for sampling.

## References¶

- 1(1,2)
J. Eli Bourassa, Nicolás Quesada, Ilan Tzitrin, Antal Száva, Theodor Isacsson, Josh Izaac, Krishna Kumar Sabapathy, Guillaume Dauphinais, and Ish Dhand. Fast simulation of bosonic qubits via Gaussian functions in phase space. arXiv:2103.05530, 2021.

**Total running time of the script:** ( 0 minutes 13.532 seconds)

## Contents

## Downloads

## Related tutorials