Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bosonic engine results #546

Merged
merged 20 commits into from
Feb 26, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion strawberryfields/api/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ class Result:

* ``results.samples``: Measurement samples from any measurements performed.

* ``results.ancilla_samples``: Measurement samples from any ancillary states
used for measurement-based gates.

**Example:**

The following example runs an existing Strawberry Fields
Expand All @@ -58,11 +61,12 @@ class Result:
but the return value of ``Result.state`` will be ``None``.
"""

def __init__(self, samples, all_samples=None, is_stateful=True):
def __init__(self, samples, all_samples=None, is_stateful=True, ancilla_samples=None):
elib20 marked this conversation as resolved.
Show resolved Hide resolved
self._state = None
self._is_stateful = is_stateful
self._samples = samples
self._all_samples = all_samples
self._ancilla_samples = ancilla_samples

@property
def samples(self):
Expand Down Expand Up @@ -90,6 +94,21 @@ def all_samples(self):
"""
return self._all_samples

@property
def ancilla_samples(self):
"""All measurement samples from ancillary modes used for measurement-based
gates.

Returns a dictionary which associates each mode (keys) with the
list of measurements outcomes (values) from all the ancilla-assisted
gates applied to that mode.

Returns:
dict[int, list]: mode index associated with the list of ancilla
measurement outcomes
"""
return self._ancilla_samples

@property
def state(self):
"""The quantum state object.
Expand Down Expand Up @@ -119,6 +138,17 @@ def __repr__(self):
if self.samples.ndim == 2:
# if samples has dim 2, assume they're from a standard Program
shots, modes = self.samples.shape

if self.ancilla_samples is not None:
ancilla_modes = 0
for i in self.ancilla_samples.keys():
ancilla_modes += len(self.ancilla_samples[i])
return (
"<Result: shots={}, num_modes={}, num_ancillae={}, contains state={}>".format(
shots, modes, ancilla_modes, self._is_stateful
elib20 marked this conversation as resolved.
Show resolved Hide resolved
)
)

return "<Result: shots={}, num_modes={}, contains state={}>".format(
shots, modes, self._is_stateful
)
Expand Down
52 changes: 40 additions & 12 deletions strawberryfields/backends/bosonicbackend/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,28 @@

from strawberryfields.backends.bosonicbackend.bosoniccircuit import BosonicModes
from strawberryfields.backends.base import NotApplicableError
from strawberryfields.program_utils import CircuitError
import sympy


def kron_list(l):
"""Take Kronecker products of a list of lists."""
return reduce(np.kron, l)
elib20 marked this conversation as resolved.
Show resolved Hide resolved


def parameter_checker(parameters):
"""Checks if any items in an iterable are sympy objects."""
for item in parameters:
if isinstance(item, sympy.Expr):
return True
elib20 marked this conversation as resolved.
Show resolved Hide resolved

# This checks all the nested items if item is an iterable
if hasattr(item, "__iter__") and not isinstance(item, str):
elib20 marked this conversation as resolved.
Show resolved Hide resolved
if parameter_checker(item):
return True
return False


class BosonicBackend(BaseBosonic):
r"""The BosonicBackend implements a simulation of quantum optical circuits
in NumPy by representing states as linear combinations of Gaussian functions
Expand Down Expand Up @@ -69,6 +84,7 @@ def __init__(self):
self.circuit = None
self.ancillae_samples_dict = {}

# pylint: disable=too-many-branches
elib20 marked this conversation as resolved.
Show resolved Hide resolved
# pylint: disable=import-outside-toplevel
def run_prog(self, prog, **kwargs):
"""Runs a strawberryfields program using the bosonic backend.
Expand All @@ -95,8 +111,9 @@ def run_prog(self, prog, **kwargs):
_New_modes,
)

# Initialize the circuit. This applies all non-Gaussian state-prep
self.init_circuit(prog)
# If a circuit exists, initialize the circuit. This applies all non-Gaussian state-prep
if prog.circuit:
self.init_circuit(prog)

# Apply operations to circuit. For now, copied from LocalEngine;
# only change is to ignore preparation classes and ancilla-assisted gates
Expand Down Expand Up @@ -124,9 +141,9 @@ def run_prog(self, prog, **kwargs):
if val is not None:
for i, r in enumerate(cmd.reg):
if r.ind not in self.ancillae_samples_dict.keys():
self.ancillae_samples_dict[r.ind] = [val[:, i]]
self.ancillae_samples_dict[r.ind] = [val]
elib20 marked this conversation as resolved.
Show resolved Hide resolved
else:
self.ancillae_samples_dict[r.ind].append(val[:, i])
self.ancillae_samples_dict[r.ind].append(val)

applied.append(cmd)

Expand Down Expand Up @@ -162,7 +179,6 @@ def run_prog(self, prog, **kwargs):

return applied, samples_dict, all_samples

# pylint: disable=too-many-branches
# pylint: disable=import-outside-toplevel
def init_circuit(self, prog):
"""Instantiate the circuit and initialize weights, means, and covs
Expand All @@ -173,6 +189,8 @@ def init_circuit(self, prog):

Raises:
NotImplementedError: if ``Ket`` or ``DensityMatrix`` preparation used
CircuitError: if any of the parameters for non-Gaussian state preparation
are symbolic
"""
from strawberryfields.ops import (
Bosonic,
Expand All @@ -184,7 +202,10 @@ def init_circuit(self, prog):
_New_modes,
)

non_gauss_preps = (Bosonic, Catstate, DensityMatrix, Fock, GKP, Ket)
# _New_modes is what gets checked when New() is called in a program circuit.
# It is included here since it could be used to instantiate a mode for non-Gaussian
# state preparation, and it's best to initialize any new modes from the outset.
elib20 marked this conversation as resolved.
Show resolved Hide resolved
non_gauss_preps = (Bosonic, Catstate, DensityMatrix, Fock, GKP, Ket, _New_modes)
elib20 marked this conversation as resolved.
Show resolved Hide resolved
nmodes = prog.init_num_subsystems
self.begin_circuit(nmodes)
# Dummy initial weights, means and covs
Expand All @@ -204,6 +225,12 @@ def init_circuit(self, prog):
if np.any(isitnew):
# Operation parameters
pars = cmd.op.p
# Check if any of the preparations rely on symbolic quantities
if isinstance(cmd.op, non_gauss_preps) and parameter_checker(pars):
elib20 marked this conversation as resolved.
Show resolved Hide resolved
raise CircuitError(
"Symbolic non-Gaussian preparations have not been implemented "
"in the bosonic backend."
)
for reg in labels:
# All the possible preparations should go in this loop
if isinstance(cmd.op, Bosonic):
Expand Down Expand Up @@ -361,7 +388,7 @@ def prepare_cat(self, alpha, phi, representation, cutoff, D):
:math:`\ket{\text{cat}(\alpha)} = \frac{1}{N} (\ket{\alpha} +e^{i\phi\pi} \ket{-\alpha})`.

Args:
alpha (float): alpha value of cat state
alpha (complex): alpha value of cat state
phi (float): phi value of cat state
representation (str): whether to use the ``'real'`` or ``'complex'`` representation
cutoff (float): if using the ``'real'`` representation, this determines
Expand Down Expand Up @@ -422,7 +449,7 @@ def prepare_cat_real_rep(self, alpha, phi, cutoff, D):
For this representation, weights, means and covariances are real-valued.

Args:
alpha (float): alpha value of cat state
alpha (complex): alpha value of cat state
phi (float): phi value of cat state
cutoff (float): this determines how many terms to keep
D (float): quality parameter of approximation
Expand Down Expand Up @@ -739,17 +766,18 @@ def measure_homodyne(self, phi, mode, shots=1, select=None, **kwargs):
self.circuit.phase_shift(-phi, mode)

if select is None:
val = self.circuit.homodyne(mode, **kwargs)[0, 0]
val = self.circuit.homodyne(mode, shots=shots, **kwargs)[:, 0]
else:
val = select * 2 / np.sqrt(2 * self.circuit.hbar)
self.circuit.post_select_homodyne(mode, val, **kwargs)
self.circuit.post_select_homodyne(mode, val)
val = np.array([val])

return np.array([[val * np.sqrt(2 * self.circuit.hbar) / 2]])
return np.array([val]).T * np.sqrt(2 * self.circuit.hbar) / 2

def measure_heterodyne(self, mode, shots=1, select=None):
if select is None:
res = 0.5 * self.circuit.heterodyne(mode, shots=shots)
return np.array([[res[:, 0] + 1j * res[:, 1]]])
return np.array([res[:, 0] + 1j * res[:, 1]]).T

res = select
self.circuit.post_select_heterodyne(mode, select)
Expand Down
4 changes: 2 additions & 2 deletions strawberryfields/backends/bosonicbackend/bosoniccircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ def mb_squeeze_single_shot(self, k, r, phi, r_anc, eta_anc):
self.beamsplitter(theta, 0, k, new_mode)
self.loss(eta_anc, new_mode)
self.phase_shift(np.pi / 2, new_mode)
val = self.homodyne(new_mode)
val = self.homodyne(new_mode)[0][0]

# Delete all record of ancilla mode
self.del_mode(new_mode)
Expand All @@ -367,7 +367,7 @@ def mb_squeeze_single_shot(self, k, r, phi, r_anc, eta_anc):

# Feedforward displacement
prefac = -np.tan(theta) / np.sqrt(2 * self.hbar * eta_anc)
self.displace(prefac * val[0][0], np.pi / 2, k)
self.displace(prefac * val, np.pi / 2, k)

self.phase_shift(phi / 2, k)

Expand Down
27 changes: 25 additions & 2 deletions strawberryfields/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from .backends.base import BaseBackend, NotApplicableError

# for automodapi, do not include the classes that should appear under the top-level strawberryfields namespace
__all__ = ["BaseEngine", "LocalEngine"]
__all__ = ["BaseEngine", "LocalEngine", "BosonicEngine"]


class BaseEngine(abc.ABC):
Expand Down Expand Up @@ -317,6 +317,14 @@ class LocalEngine(BaseEngine):
backend_options (None, Dict[str, Any]): keyword arguments to be passed to the backend
"""

def __new__(cls, backend, *, backend_options=None):
if backend == "bosonic":
bos_eng = super().__new__(BosonicEngine)
bos_eng.__init__(backend, backend_options=backend_options)
return bos_eng

return super().__new__(cls)
elib20 marked this conversation as resolved.
Show resolved Hide resolved

def __str__(self):
return self.__class__.__name__ + "({})".format(self.backend_name)

Expand Down Expand Up @@ -501,7 +509,8 @@ def run(self, program, *, args=None, compile_options=None, **kwargs):
# session and feed_dict are needed by TF backend both during simulation (if program
# contains measurements) and state object construction.
result._state = self.backend.state(**temp_run_options)

if self.backend_name == "bosonic":
result._ancilla_samples = self.backend.ancillae_samples_dict
elib20 marked this conversation as resolved.
Show resolved Hide resolved
return result


Expand Down Expand Up @@ -723,3 +732,17 @@ class Engine(LocalEngine):

# alias for backwards compatibility
__doc__ = LocalEngine.__doc__


class BosonicEngine(LocalEngine):
elib20 marked this conversation as resolved.
Show resolved Hide resolved
"""Local quantum program executor engine for programs executed on the bosonic backend.

The BosonicEngine is used to execute :class:`.Program` instances on the bosonic backend,
and makes the results available via :class:`.Result`.
"""

def _run_program(self, prog, **kwargs):
# Custom Bosonic run code
applied, samples_dict, all_samples = self.backend.run_prog(prog, **kwargs)
samples = self._combine_and_sort_samples(samples_dict)
return applied, samples, all_samples
42 changes: 41 additions & 1 deletion tests/backend/test_bosonic_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import strawberryfields as sf
import strawberryfields.backends.bosonicbackend.backend as bosonic
import pytest
import sympy

pytestmark = pytest.mark.bosonic

Expand All @@ -29,6 +30,7 @@
r_fock = 0.05
EPS_VALS = np.array([0.01, 0.05, 0.1, 0.5])
R_VALS = np.linspace(-1, 1, 5)
SHOTS_VALS = np.array([1, 19, 100])


class TestKronList:
Expand All @@ -41,6 +43,24 @@ def test_kron_list(self):
assert np.allclose(list_compare, bosonic.kron_list([l1, l2]))


class TestParameterChecker:
"""Test parameter_checker function from the bosonic backend."""

def test_parameter_checker(self):
elib20 marked this conversation as resolved.
Show resolved Hide resolved
symbolic_param = sympy.Expr()
params = []
assert not bosonic.parameter_checker(params)

params = [1, "real", 3.0, [4 + 1j, 5], [[3.5, 4.8, "complex"]], np.array([7, 9]), range(3)]
elib20 marked this conversation as resolved.
Show resolved Hide resolved
assert not bosonic.parameter_checker(params)

params = [1, "real", symbolic_param]
assert bosonic.parameter_checker(params)

params = [1, [3, symbolic_param]]
assert bosonic.parameter_checker(params)


class TestBosonicCatStates:
r"""Tests cat state method of the BosonicBackend class."""

Expand Down Expand Up @@ -493,7 +513,7 @@ def test_measurement(self, alpha, r):
sf.ops.Catstate(alpha) | q[0]
sf.ops.Squeezed(r) | q[1]
sf.ops.MeasureX | q[0]
sf.ops.MeasureX | q[1]
sf.ops.MeasureHD | q[1]
elib20 marked this conversation as resolved.
Show resolved Hide resolved
backend = bosonic.BosonicBackend()
applied, samples, all_samples = backend.run_prog(prog)
state = backend.state()
Expand All @@ -506,6 +526,26 @@ def test_measurement(self, alpha, r):
assert i in samples.keys()
assert samples[i].shape == (1,)

@pytest.mark.parametrize("alpha", ALPHA_VALS)
@pytest.mark.parametrize("shots", SHOTS_VALS)
def test_measurement_many_shots(self, alpha, shots):
"""Runs a program with measurements."""
prog = sf.Program(1)
with prog.context as q:
sf.ops.Catstate(alpha) | q[0]
sf.ops.MeasureHomodyne(0) | q[0]
elib20 marked this conversation as resolved.
Show resolved Hide resolved

backend = bosonic.BosonicBackend()
applied, samples, all_samples = backend.run_prog(prog, shots=shots)
state = backend.state()

# Check output is vacuum since everything was measured
assert np.allclose(state.fidelity_vacuum(), 1)

# Check samples
assert 0 in samples.keys()
assert samples[0].shape == (int(shots),)

@pytest.mark.parametrize("alpha", ALPHA_VALS)
@pytest.mark.parametrize("r", R_VALS)
def test_mb_gates(self, alpha, r):
Expand Down
Loading