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

Define the QiskitDevice.batch_execute method #156

Merged
merged 22 commits into from
Nov 15, 2021
Merged
Show file tree
Hide file tree
Changes from 21 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
76 changes: 69 additions & 7 deletions pennylane_qiskit/qiskit_device.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2019 Xanadu Quantum Technologies Inc.
# Copyright 2019-2021 Xanadu Quantum Technologies Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -202,7 +202,17 @@ def reset(self):
self._current_job = None
self._state = None # statevector of a simulator backend

def apply(self, operations, **kwargs):
def create_circuit_object(self, operations, **kwargs):
"""Builds the circuit objects based on the operations and measurements
specified to apply.

Args:
operations (list[~.Operation]): operations to apply to the device

Keyword args:
rotations (list[~.Operation]): Operations that rotate the circuit
pre-measurement into the eigenbasis of the observables.
"""
rotations = kwargs.get("rotations", [])

applied_operations = self.apply_operations(operations)
Expand All @@ -221,6 +231,10 @@ def apply(self, operations, **kwargs):
elif "aer" in self.backend_name:
self._circuit.save_state()

def apply(self, operations, **kwargs):

self.create_circuit_object(operations, **kwargs)

# These operations need to run for all devices
compiled_circuit = self.compile()
self.run(compiled_circuit)
Expand Down Expand Up @@ -304,20 +318,21 @@ def run(self, qcirc):
if self.backend_name in self._state_backends:
self._state = self._get_state(result)

def _get_state(self, result):
def _get_state(self, result, experiment=None):
"""Returns the statevector for state simulator backends.

Args:
result (qiskit.Result): result object
experiment (str): the name of the experiment to get the state for

Returns:
array[float]: size ``(2**num_wires,)`` statevector
"""
if "statevector" in self.backend_name:
state = np.asarray(result.get_statevector())
state = np.asarray(result.get_statevector(experiment))

elif "unitary" in self.backend_name:
unitary = np.asarray(result.get_unitary())
unitary = np.asarray(result.get_unitary(experiment))
initial_state = np.zeros([2 ** self.num_wires])
initial_state[0] = 1

Expand All @@ -326,15 +341,26 @@ def _get_state(self, result):
# reverse qubit order to match PennyLane convention
return state.reshape([2] * self.num_wires).T.flatten()

def generate_samples(self):
def generate_samples(self, circuit=None):
rmoyard marked this conversation as resolved.
Show resolved Hide resolved
r"""Returns the computational basis samples generated for all wires.

Note that PennyLane uses the convention :math:`|q_0,q_1,\dots,q_{N-1}\rangle` where
:math:`q_0` is the most significant bit.

Args:
circuit (str): the name of the circuit to get the state for

Returns:
array[complex]: array of samples in the shape ``(dev.shots, dev.num_wires)``
"""

# branch out depending on the type of backend
if self.backend_name in self._state_backends:
# software simulator: need to sample from probabilities
return super().generate_samples()

# hardware or hardware simulator
samples = self._current_job.result().get_memory()
samples = self._current_job.result().get_memory(circuit)

# reverse qubit order to match PennyLane convention
return np.vstack([np.array([int(i) for i in s[::-1]]) for s in samples])
Expand All @@ -349,3 +375,39 @@ def analytic_probability(self, wires=None):

prob = self.marginal_prob(np.abs(self._state) ** 2, wires)
return prob

def batch_execute(self, circuits):
rmoyard marked this conversation as resolved.
Show resolved Hide resolved

compiled_circuits = []

# Compile each circuit object
for circuit in circuits:

# We need to reset the device here, else it will
# not start the next computation in the zero state
self.reset()
self.create_circuit_object(circuit.operations, rotations=circuit.diagonalizing_gates)

compiled_circ = self.compile()
compiled_circ.name = f"circ{len(compiled_circuits)}"
compiled_circuits.append(compiled_circ)

# Send the batch of circuit objects using backend.run
self._current_job = self.backend.run(compiled_circuits, shots=self.shots, **self.run_args)
result = self._current_job.result()

# Compute statistics using the state and/or samples
results = []
for circuit, circuit_obj in zip(circuits, compiled_circuits):

if self.backend_name in self._state_backends:
self._state = self._get_state(result, experiment=circuit_obj)

# generate computational basis samples
if self.shots is not None or circuit.is_sampled:
self._samples = self.generate_samples(circuit_obj)

res = self.statistics(circuit.observables)
results.append(res)

return results
71 changes: 70 additions & 1 deletion tests/test_ibmq.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

@pytest.fixture
def token():
"""A fixture loading the IBMQ token from the IBMQX_TOKEN_TEST environment
variable."""
t = os.getenv("IBMQX_TOKEN_TEST", None)

if t is None:
Expand Down Expand Up @@ -131,13 +133,15 @@ def test_custom_provider_hub_group_project(monkeypatch):


def test_load_from_disk(token):
"""Test loading the account credentials and the device from disk."""
IBMQ.save_account(token)
dev = IBMQDevice(wires=1)
assert dev.provider.credentials.is_ibmq()
IBMQ.delete_account()


def test_account_error(monkeypatch):
"""Test that an error is raised if there is no active IBMQ account."""

# Token is passed such that the test is skipped if no token was provided
with pytest.raises(IBMQAccountError, match="No active IBM Q account"):
Expand All @@ -148,13 +152,14 @@ def test_account_error(monkeypatch):

@pytest.mark.parametrize("shots", [1000])
def test_simple_circuit(token, tol, shots):
"""Test executing a simple circuit submitted to IBMQ."""
IBMQ.enable_account(token)
dev = IBMQDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots)

@qml.qnode(dev)
def circuit(theta, phi):
qml.RX(theta, wires=0)
qml.RX(phi, wires=0)
qml.RX(phi, wires=1)
rmoyard marked this conversation as resolved.
Show resolved Hide resolved
qml.CNOT(wires=[0, 1])
return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))

Expand All @@ -166,6 +171,70 @@ def circuit(theta, phi):
assert np.allclose(res, expected, **tol)


@pytest.mark.parametrize("shots", [1000])
def test_simple_circuit_with_batch_params(token, tol, shots, mocker):
"""Test that executing a simple circuit with batched parameters is
submitted to IBMQ once."""
IBMQ.enable_account(token)
dev = IBMQDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots)
rmoyard marked this conversation as resolved.
Show resolved Hide resolved

@qml.batch_params
@qml.qnode(dev)
def circuit(theta, phi):
qml.RX(theta, wires=0)
qml.RX(phi, wires=1)
qml.CNOT(wires=[0, 1])
return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))

# Check that we run only once
spy1 = mocker.spy(dev, "batch_execute")
spy2 = mocker.spy(dev.backend, "run")

# Batch the input parameters
batch_dim = 3
theta = np.linspace(0, 0.543, batch_dim)
phi = np.linspace(0, 0.123, batch_dim)

res = circuit(theta, phi)
assert np.allclose(res[:, 0], np.cos(theta), **tol)
assert np.allclose(res[:, 1], np.cos(theta) * np.cos(phi), **tol)

# Check that IBMQBackend.run was called once
assert spy1.call_count == 1
assert spy2.call_count == 1


@pytest.mark.parametrize("shots", [1000])
def test_batch_execute_parameter_shift(token, tol, shots, mocker):
"""Test that devices provide correct result computing the gradient of a
circuit using the parameter-shift rule and the batch execution pipeline."""
IBMQ.enable_account(token)
dev = IBMQDevice(wires=3, backend="ibmq_qasm_simulator", shots=shots)

spy1 = mocker.spy(dev, "batch_execute")
spy2 = mocker.spy(dev.backend, "run")

@qml.qnode(dev, diff_method="parameter-shift")
def circuit(x, y):
qml.RX(x, wires=[0])
qml.RY(y, wires=[1])
qml.CNOT(wires=[0, 1])
return qml.expval(qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliZ(2))

x = qml.numpy.array(0.543, requires_grad=True)
y = qml.numpy.array(0.123, requires_grad=True)

res = qml.grad(circuit)(x,y)
expected = np.array([[-np.sin(y) * np.sin(x), np.cos(y) * np.cos(x)]])
assert np.allclose(res, expected, **tol)

# Check that QiskitDevice.batch_execute was called once
assert spy1.call_count == 1

# Check that run was called twice: for the partial derivatives and for
# running the circuit
assert spy2.call_count == 2

@pytest.mark.parametrize("shots", [1000])
def test_probability(token, tol, shots):
"""Test that the probs function works."""
Expand Down
86 changes: 85 additions & 1 deletion tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import qiskit.providers.aer as aer

from pennylane_qiskit import AerDevice, BasicAerDevice
from pennylane_qiskit.qiskit_device import QiskitDevice

from conftest import state_backends

Expand Down Expand Up @@ -54,6 +55,10 @@ def test_args(self):
@pytest.mark.parametrize("shots", [None, 8192])
def test_one_qubit_circuit(self, shots, d, backend, tol):
"""Test that devices provide correct result for a simple circuit"""
if (d[0] == "qiskit.aer" and "aer" not in backend) \
or (d[0] == "qiskit.basicaer" and "aer" in backend):
pytest.skip("Only the AerSimulator is supported on AerDevice")

if backend not in state_backends and shots is None:
pytest.skip("Hardware simulators do not support analytic mode")

Expand All @@ -75,7 +80,7 @@ def circuit(x, y, z):

@pytest.mark.parametrize("d", pldevices)
@pytest.mark.parametrize("shots", [8192])
def test_one_qubit_circuit(self, shots, d, backend, tol):
def test_basis_state_and_rot(self, shots, d, backend, tol):
rmoyard marked this conversation as resolved.
Show resolved Hide resolved
"""Integration test for the BasisState and Rot operations for non-analytic mode."""

if (d[0] == "qiskit.aer" and "aer" not in backend) \
Expand Down Expand Up @@ -479,3 +484,82 @@ def circuit():
return qml.expval(qml.PauliZ(wires=0))

assert circuit() == -1

class TestBatchExecution:
"""Test the devices work correctly with the batch execution pipeline."""

@pytest.mark.parametrize("d", pldevices)
@pytest.mark.parametrize("shots", [None, 8192])
def test_one_qubit_circuit_batch_params(self, shots, d, backend, tol, mocker):
"""Test that devices provide correct result for a simple circuit using
the batch_params transform."""
if (d[0] == "qiskit.aer" and "aer" not in backend) \
or (d[0] == "qiskit.basicaer" and "aer" in backend):
pytest.skip("Only the AerSimulator is supported on AerDevice")

if backend not in state_backends and shots is None:
pytest.skip("Hardware simulators do not support analytic mode")

dev = qml.device(d[0], wires=1, backend=backend, shots=shots)

# Batch the input parameters
batch_dim = 3
a = np.linspace(0, 0.543, batch_dim)
b = np.linspace(0, 0.123, batch_dim)
c = np.linspace(0, 0.987, batch_dim)

spy1 = mocker.spy(QiskitDevice, "batch_execute")
spy2 = mocker.spy(dev.backend, "run")

@qml.batch_params
@qml.qnode(dev)
def circuit(x, y, z):
"""Reference QNode"""
qml.PauliX(0)
qml.Hadamard(wires=0)
qml.Rot(x, y, z, wires=0)
return qml.expval(qml.PauliZ(0))

assert np.allclose(circuit(a, b, c), np.cos(a) * np.sin(b), **tol)

# Check that QiskitDevice.batch_execute was called
assert spy1.call_count == 1
assert spy2.call_count == 1

@pytest.mark.parametrize("d", pldevices)
@pytest.mark.parametrize("shots", [None, 8192])
def test_batch_execute_parameter_shift(self, shots, d, backend, tol, mocker):
"""Test that devices provide correct result computing the gradient of a
circuit using the parameter-shift rule and the batch execution pipeline."""
if (d[0] == "qiskit.aer" and "aer" not in backend) \
or (d[0] == "qiskit.basicaer" and "aer" in backend):
pytest.skip("Only the AerSimulator is supported on AerDevice")

if backend not in state_backends and shots is None:
pytest.skip("Hardware simulators do not support analytic mode")

dev = qml.device(d[0], wires=3, backend=backend, shots=shots)

spy1 = mocker.spy(QiskitDevice, "batch_execute")
rmoyard marked this conversation as resolved.
Show resolved Hide resolved
spy2 = mocker.spy(dev.backend, "run")

@qml.qnode(dev, diff_method="parameter-shift")
def circuit(x, y):
qml.RX(x, wires=[0])
qml.RY(y, wires=[1])
qml.CNOT(wires=[0, 1])
return qml.expval(qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliZ(2))

x = qml.numpy.array(0.543, requires_grad=True)
y = qml.numpy.array(0.123, requires_grad=True)

res = qml.grad(circuit)(x,y)
expected = np.array([[-np.sin(y) * np.sin(x), np.cos(y) * np.cos(x)]])
assert np.allclose(res, expected, **tol)

# Check that QiskitDevice.batch_execute was called once
assert spy1.call_count == 1

# Check that run was called twice: for the partial derivatives and for
# running the circuit
assert spy2.call_count == 2
rmoyard marked this conversation as resolved.
Show resolved Hide resolved
Loading