diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index 0025978cc..13b2bc4f6 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -2,6 +2,18 @@
New features since last release
+* Rectangular GKP states are now supported.
+ [(#668)](https://github.com/XanaduAI/strawberryfields/pull/668)
+
+ For example,
+
+ ```python
+ prog = sf.Program(1)
+
+ with prog.context as q:
+ sf.ops.GKP(shape="rectangular", alpha=2) | q[0]
+ ```
+
Breaking Changes
Bug fixes
@@ -12,6 +24,8 @@
This release contains contributions from (in alphabetical order):
+J. Eli Bourassa
+
# Release 0.21.0 (current release)
New features since last release
diff --git a/strawberryfields/backends/bosonicbackend/backend.py b/strawberryfields/backends/bosonicbackend/backend.py
index c276c1797..f5a20983c 100644
--- a/strawberryfields/backends/bosonicbackend/backend.py
+++ b/strawberryfields/backends/bosonicbackend/backend.py
@@ -540,7 +540,9 @@ def prepare_cat_real_rep(self, a, theta, p, ampl_cutoff, D):
return weights, means, cov
- def prepare_gkp(self, state, epsilon, ampl_cutoff, representation="real", shape="square"):
+ def prepare_gkp(
+ self, state, epsilon, ampl_cutoff, representation="real", shape="square", alpha=1
+ ):
r"""Prepares the arrays of weights, means and covs for a finite energy GKP state.
GKP states are qubits, with the qubit state defined by:
@@ -555,6 +557,7 @@ def prepare_gkp(self, state, epsilon, ampl_cutoff, representation="real", shape=
ampl_cutoff (float): this determines how many terms to keep
representation (str): ``'real'`` or ``'complex'`` reprsentation
shape (str): shape of the lattice; default 'square'
+ alpha (float): peak spacing in q is given by sqrt(alpha * pi * hbar)
Returns:
tuple: arrays of the weights, means and covariances for the state
@@ -566,16 +569,24 @@ def prepare_gkp(self, state, epsilon, ampl_cutoff, representation="real", shape=
if representation == "complex":
raise NotImplementedError("The complex description of GKP is not implemented")
- if shape != "square":
- raise NotImplementedError("Only square GKP are implemented for now")
+ if shape not in ["square", "rectangular"]:
+ raise NotImplementedError("Only square and rectangular GKP are implemented.")
+
+ if shape == "square":
+ if alpha != 1:
+ raise ValueError(
+ "For square GKPs, alpha must be 1. For alpha not equal to "
+ + "1, use shape='rectangular'."
+ )
theta, phi = state[0], state[1]
- def coeff(peak_loc):
+ def coeff(peak_loc, alpha):
"""Returns the value of the weight for a given peak.
Args:
peak_loc (array): location of the ideal peak in phase space
+ alpha (float): peak spacing in q is given by sqrt(alpha * pi * hbar)
Returns:
float: weight of the peak
@@ -610,7 +621,7 @@ def coeff(peak_loc):
prefactor = np.exp(
-np.pi
* 0.25
- * (l ** 2 + m ** 2)
+ * ((l * np.sqrt(alpha)) ** 2 + (m / np.sqrt(alpha)) ** 2)
* (1 - np.exp(-2 * epsilon))
/ (1 + np.exp(-2 * epsilon))
)
@@ -649,7 +660,7 @@ def coeff(peak_loc):
)
# Calculate the weights for each peak
- weights = coeff(means)
+ weights = coeff(means, alpha)
filt = abs(weights) > ampl_cutoff
weights = weights[filt]
@@ -657,7 +668,11 @@ def coeff(peak_loc):
# Apply finite energy effect to means
means = means[filt]
+ means[:, 0] *= np.sqrt(alpha)
+ means[:, 1] /= np.sqrt(alpha)
+
means *= 0.5 * damping * np.sqrt(np.pi * self.circuit.hbar)
+
# Covariances all the same
covs = (
0.5
diff --git a/strawberryfields/backends/fockbackend/backend.py b/strawberryfields/backends/fockbackend/backend.py
index befa6bd84..71d64da72 100644
--- a/strawberryfields/backends/fockbackend/backend.py
+++ b/strawberryfields/backends/fockbackend/backend.py
@@ -295,7 +295,7 @@ def measure_fock(self, modes, shots=1, select=None, **kwargs):
return self.circuit.measure_fock(self._remap_modes(modes), select=select)
def prepare_gkp(
- self, state, epsilon, ampl_cutoff, representation="real", shape="square", mode=None
+ self, state, epsilon, ampl_cutoff, representation="real", shape="square", alpha=1, mode=None
):
r"""Prepares the Fock representation of a finite energy GKP state.
@@ -311,6 +311,7 @@ def prepare_gkp(
amplcutoff (float): this determines how many terms to keep
representation (str): ``'real'`` or ``'complex'`` reprsentation
shape (str): shape of the lattice; default 'square'
+ alpha (float): peak spacing in q is given by sqrt(alpha * pi * hbar)
Returns:
tuple: arrays of the weights, means and covariances for the state
@@ -322,8 +323,15 @@ def prepare_gkp(
if representation == "complex":
raise NotImplementedError("The complex description of GKP is not implemented")
- if shape != "square":
- raise NotImplementedError("Only square GKP are implemented for now")
+ if shape not in ["square", "rectangular"]:
+ raise NotImplementedError("Only square and rectangular GKP are implemented.")
+
+ if shape == "square":
+ if alpha != 1:
+ raise ValueError(
+ "For square GKPs, alpha must be 1. For alpha not equal to "
+ + "1, use shape='rectangular'."
+ )
theta, phi = state[0], state[1]
- self.circuit.prepare_gkp(theta, phi, epsilon, ampl_cutoff, self._remap_modes(mode))
+ self.circuit.prepare_gkp(theta, phi, epsilon, ampl_cutoff, alpha, self._remap_modes(mode))
diff --git a/strawberryfields/backends/fockbackend/circuit.py b/strawberryfields/backends/fockbackend/circuit.py
index 41868fb2d..b5962b5ae 100644
--- a/strawberryfields/backends/fockbackend/circuit.py
+++ b/strawberryfields/backends/fockbackend/circuit.py
@@ -799,13 +799,15 @@ def measure_homodyne(self, phi, mode, select=None, **kwargs):
# `homodyne_sample` will always be a single value since multiple shots is not supported
return np.array([[homodyne_sample]])
- def prepare_gkp(self, theta, phi, epsilon, ampl_cutoff, mode):
+ def prepare_gkp(self, theta, phi, epsilon, ampl_cutoff, alpha, mode):
"""
Prepares a mode in a GKP state.
"""
if self._pure:
- self.prepare(ops.square_gkp_state(theta, phi, epsilon, ampl_cutoff, self._trunc), mode)
+ self.prepare(
+ ops.rect_gkp_state(theta, phi, epsilon, ampl_cutoff, alpha, self._trunc), mode
+ )
else:
- st = ops.square_gkp_state(theta, phi, epsilon, ampl_cutoff, self._trunc)
+ st = ops.rect_gkp_state(theta, phi, epsilon, ampl_cutoff, alpha, self._trunc)
self.prepare(np.outer(st, st.conjugate()), mode)
diff --git a/strawberryfields/backends/fockbackend/ops.py b/strawberryfields/backends/fockbackend/ops.py
index 476792eef..9d004bb52 100644
--- a/strawberryfields/backends/fockbackend/ops.py
+++ b/strawberryfields/backends/fockbackend/ops.py
@@ -515,7 +515,7 @@ def hermiteVals(q_mag, num_bins, m_omega_over_hbar, trunc):
return q_tensor, Hvals
-def gkp_displacements(t, k, epsilon):
+def gkp_displacements(t, k, epsilon, alpha):
"""
Helper function to generate the displacements parameters associated with the teeth of
GKP computational basis state k.
@@ -524,14 +524,15 @@ def gkp_displacements(t, k, epsilon):
t (array): the teeth of GKP computational basis
k (int): a computational basis state label, can be either 0 or 1
epsilon (float): finite energy parameter of the state
+ alpha (float): peak spacing in q is given by sqrt(alpha * pi * hbar)
Returns:
array: the displacements
"""
- return np.sqrt(0.5 * np.pi) * (2 * t + k) / np.cosh(epsilon)
+ return np.sqrt(0.5 * np.pi * alpha) * (2 * t + k) / np.cosh(epsilon)
-def gkp_coeffs(t, k, epsilon):
+def gkp_coeffs(t, k, epsilon, alpha):
"""
Helper function to generate the coefficient parameters associated with the teeth of
GKP computational basis state k.
@@ -540,22 +541,24 @@ def gkp_coeffs(t, k, epsilon):
t (array): the teeth of GKP computational basis
k (int): a computational basis state label, can be either 0 or 1
epsilon (float): finite energy parameter of the state
+ alpha (float): peak spacing in q is given by sqrt(alpha * pi * hbar)
Returns:
array: the coefficients
"""
- return np.exp(-0.5 * np.pi * np.tanh(epsilon) * (k + 2 * t) ** 2)
+ return np.exp(-0.5 * np.pi * alpha * np.tanh(epsilon) * (k + 2 * t) ** 2)
@functools.lru_cache()
-def square_gkp_basis_state(i, epsilon, ampl_cutoff, cutoff):
+def rect_gkp_basis_state(i, epsilon, ampl_cutoff, alpha, cutoff):
"""
- Generate the Fock expansion of a (subnormalized) computational GKP basis state. Normalization occurs in the ``square_gkp_state`` method.
+ Generate the Fock expansion of a (subnormalized) computational GKP basis state. Normalization occurs in the ``rect_gkp_state`` method.
Args:
i (int): a computational basis state label, can be either 0 or 1
epsilon (float): finite energy parameter of the state
ampl_cutoff (float): this determines how many terms to keep in the Hilbert space expansion
+ alpha (float): peak spacing in q is given by sqrt(alpha * pi * hbar)
cutoff (int): Fock space truncation
Returns
@@ -563,16 +566,16 @@ def square_gkp_basis_state(i, epsilon, ampl_cutoff, cutoff):
"""
z_max = int(np.ceil(np.sqrt(-0.25 / np.pi * np.log(ampl_cutoff) / np.tanh(epsilon))))
- coeffs = [gkp_coeffs(t, i, epsilon) for t in range(-z_max, z_max + 1)]
+ coeffs = [gkp_coeffs(t, i, epsilon, alpha) for t in range(-z_max, z_max + 1)]
r = -0.5 * np.log(np.tanh(epsilon))
- alphas = [gkp_displacements(t, i, epsilon) for t in range(-z_max, z_max + 1)]
- num_kets = len(alphas)
- ket = [coeffs[j] * displacedSqueezed(alphas[j], 0, r, 0, cutoff) for j in range(num_kets)]
+ disps = [gkp_displacements(t, i, epsilon, alpha) for t in range(-z_max, z_max + 1)]
+ num_kets = len(disps)
+ ket = [coeffs[j] * displacedSqueezed(disps[j], 0, r, 0, cutoff) for j in range(num_kets)]
return sum(ket)
@functools.lru_cache()
-def square_gkp_state(theta, phi, epsilon, ampl_cutoff, cutoff):
+def rect_gkp_state(theta, phi, epsilon, ampl_cutoff, alpha, cutoff):
r"""
Generate the Fock expansion of an abitrary GKP state parametrized as
:math:`|\psi\rangle = \cos{\tfrac{\theta}{2}} \vert 0 \rangle_{\rm gkp} + e^{-i \phi} \sin{\tfrac{\theta}{2}} \vert 1 \rangle_{\rm gkp}`.
@@ -582,6 +585,7 @@ def square_gkp_state(theta, phi, epsilon, ampl_cutoff, cutoff):
phi (float): the longitude with respect to the x-axis in the Bloch sphere
epsilon (float): finite energy parameter of the state
ampl_cutoff (float): this determines how many terms to keep
+ alpha (float): peak spacing in q is given by sqrt(alpha * pi * hbar)
cutoff (int): Fock space truncation
Returns:
@@ -589,8 +593,8 @@ def square_gkp_state(theta, phi, epsilon, ampl_cutoff, cutoff):
"""
qubit_coeff0 = np.cos(theta / 2)
qubit_coeff1 = np.sin(theta / 2) * np.exp(-1j * phi)
- ket0 = square_gkp_basis_state(0, epsilon, ampl_cutoff, cutoff)
- ket1 = square_gkp_basis_state(1, epsilon, ampl_cutoff, cutoff)
+ ket0 = rect_gkp_basis_state(0, epsilon, ampl_cutoff, alpha, cutoff)
+ ket1 = rect_gkp_basis_state(1, epsilon, ampl_cutoff, alpha, cutoff)
ket = qubit_coeff0 * ket0 + qubit_coeff1 * ket1
ket /= np.linalg.norm(ket)
return ket
diff --git a/strawberryfields/ops.py b/strawberryfields/ops.py
index a8c563558..8d8389cab 100644
--- a/strawberryfields/ops.py
+++ b/strawberryfields/ops.py
@@ -949,14 +949,21 @@ class GKP(Preparation):
cutoff (float): this determines how many terms to keep
representation (str): ``'real'`` or ``'complex'`` reprsentation
shape (str): shape of the lattice; default ``'square'``
+ alpha (float): peak spacing in q is given by sqrt(alpha * pi * hbar)
"""
def __init__(
- self, state=None, epsilon=0.2, ampl_cutoff=1e-12, representation="real", shape="square"
+ self,
+ state=None,
+ epsilon=0.2,
+ ampl_cutoff=1e-12,
+ representation="real",
+ shape="square",
+ alpha=1,
):
if state is None:
state = [0, 0]
- super().__init__([state, epsilon, ampl_cutoff, representation, shape])
+ super().__init__([state, epsilon, ampl_cutoff, representation, shape, alpha])
def _apply(self, reg, backend, **kwargs):
backend.prepare_gkp(*self.p, mode=reg[0])
diff --git a/tests/backend/test_bosonic_backend.py b/tests/backend/test_bosonic_backend.py
index d70f95ba4..b46abc716 100644
--- a/tests/backend/test_bosonic_backend.py
+++ b/tests/backend/test_bosonic_backend.py
@@ -474,6 +474,35 @@ def test_gkp_complex(self):
with pytest.raises(NotImplementedError):
backend.run_prog(prog)
+ @pytest.mark.parametrize("eps", EPS_VALS)
+ def test_sensor_gkp(self, eps):
+ r"""Checks that the GKP sensor state is invariant under Fourier gate."""
+ x = np.linspace(-3 * np.sqrt(np.pi), 3 * np.sqrt(np.pi), 40)
+ p = np.copy(x)
+
+ # Prepare GKP 0 and apply Hadamard
+ prog_sensor = sf.Program(1)
+ with prog_sensor.context as q:
+ sf.ops.GKP(state=[np.pi / 2, 0], epsilon=eps, shape="rectangular", alpha=2) | q[0]
+
+ backend_sensor = bosonic.BosonicBackend()
+ backend_sensor.run_prog(prog_sensor)
+ state_sensor = backend_sensor.state()
+ wigner_sensor = state_sensor.wigner(0, x, p)
+
+ # Prepare GKP +
+ prog_sensor_F = sf.Program(1)
+ with prog_sensor_F.context as qF:
+ sf.ops.GKP(state=[np.pi / 2, 0], epsilon=eps, shape="rectangular", alpha=2) | qF[0]
+ sf.ops.Rgate(np.pi / 2) | qF[0]
+
+ backend_sensor_F = bosonic.BosonicBackend()
+ backend_sensor_F.run_prog(prog_sensor_F)
+ state_sensor_F = backend_sensor_F.state()
+ wigner_sensor_F = state_sensor_F.wigner(0, x, p)
+
+ assert np.allclose(wigner_sensor, wigner_sensor_F)
+
@pytest.mark.parametrize("fock_eps", [0.1, 0.2, 0.3])
def test_gkp_wigner_compare_backends(self, fock_eps):
"""Test correct Wigner function are generated by comparing fock and bosonic outputs.
@@ -501,6 +530,34 @@ def test_gkp_wigner_compare_backends(self, fock_eps):
W_bosonic = gkp_bosonic.wigner(0, xvec, xvec)
assert np.allclose(W_fock, W_bosonic)
+ @pytest.mark.parametrize("fock_eps", [0.1, 0.2, 0.3])
+ @pytest.mark.parametrize("rect_ratio", [0.8, 1, 1.5])
+ def test_rect_gkp_wigner_compare_backends(self, fock_eps, rect_ratio):
+ """Test that correct Wigner function is generated by comparing fock and bosonic outputs.
+ Since the fock backend cannot simulate very small epsilon, we restrict to fock_eps > 0.1."""
+ theta = np.pi / 8
+ phi = np.pi / 6
+ ket = [theta, phi]
+ xvec = np.linspace(-1, 1, 200)
+ prog_fock = sf.Program(1)
+ with prog_fock.context as q:
+ sf.ops.GKP(epsilon=fock_eps, state=ket, shape="rectangular", alpha=rect_ratio) | q
+ cutoff = 100
+ hbar = 2
+ eng_fock = sf.Engine("fock", backend_options={"cutoff_dim": cutoff, "hbar": hbar})
+ gkp_fock = eng_fock.run(prog_fock).state
+ W_fock = gkp_fock.wigner(0, xvec, xvec)
+
+ prog_bosonic = sf.Program(1)
+
+ with prog_bosonic.context as q:
+ sf.ops.GKP(epsilon=fock_eps, state=ket, shape="rectangular", alpha=rect_ratio) | q
+ hbar = 2
+ eng_bosonic = sf.Engine("bosonic", backend_options={"hbar": hbar})
+ gkp_bosonic = eng_bosonic.run(prog_bosonic).state
+ W_bosonic = gkp_bosonic.wigner(0, xvec, xvec)
+ assert np.allclose(W_fock, W_bosonic)
+
class TestBosonicUserSpecifiedState:
"""Checks the Bosonic preparation method."""
diff --git a/tests/frontend/test_ops_gate.py b/tests/frontend/test_ops_gate.py
index 422e8fd9b..99a4872dd 100644
--- a/tests/frontend/test_ops_gate.py
+++ b/tests/frontend/test_ops_gate.py
@@ -174,12 +174,22 @@ class TestGKPBasics:
@pytest.mark.parametrize("ampl", [0, 0.001])
@pytest.mark.parametrize("eps", [0, 0.001])
@pytest.mark.parametrize("r", ["real", "complex"])
- @pytest.mark.parametrize("s", ["square"])
- def test_gkp_str_representation(self, state, ampl, eps, r, s):
+ @pytest.mark.parametrize("s", ["square", "rectangular"])
+ @pytest.mark.parametrize("alph", [0.8, 1.1])
+ def test_gkp_str_representation(self, state, ampl, eps, r, s, alph):
"""Test the string representation of the GKP operation"""
assert (
- str(ops.GKP(state=state, ampl_cutoff=ampl, epsilon=eps, representation=r, shape=s))
- == f"GKP({str(state)}, {str(eps)}, {str(ampl)}, {r}, {s})"
+ str(
+ ops.GKP(
+ state=state,
+ ampl_cutoff=ampl,
+ epsilon=eps,
+ representation=r,
+ shape=s,
+ alpha=alph,
+ )
+ )
+ == f"GKP({str(state)}, {str(eps)}, {str(ampl)}, {r}, {s}, {str(alph)})"
)