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

Fix issue when using a wire subset with BasisState #61

Merged
merged 12 commits into from
May 27, 2021
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

## Bug fixes

* Fixed issue when using a subset of wires with `BasisState`.
[(#61)](https://github.com/PennyLaneAI/pennylane-cirq/pull/61)

## Breaking changes

## Documentation
Expand All @@ -14,6 +17,8 @@

This release contains contributions from (in alphabetical order):

Theodor Isacsson

---

# Release 0.15.0
Expand Down
37 changes: 32 additions & 5 deletions pennylane_cirq/simulator_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
----
"""
import math
import itertools as it

import cirq
import numpy as np
import pennylane as qml
Expand Down Expand Up @@ -81,26 +83,47 @@ def _apply_basis_state(self, basis_state_operation):
if not self.shots is None:
raise qml.DeviceError("The operation BasisState is only supported in analytic mode.")

basis_state_array = np.array(basis_state_operation.parameters[0])
wires = basis_state_operation.wires

if len(basis_state_array) != len(self.qubits):
if len(basis_state_operation.parameters[0]) != len(wires):
raise qml.DeviceError(
"For BasisState, the state has to be specified for the correct number of qubits. Got a state for {} qubits, expected {}.".format(
len(basis_state_array), len(self.qubits)
len(basis_state_operation.parameters[0]), len(self.qubits)
)
)

if not np.all(np.isin(basis_state_array, np.array([0, 1]))):
if not np.all(np.isin(basis_state_operation.parameters[0], np.array([0, 1]))):
raise qml.DeviceError(
"Argument for BasisState can only contain 0 and 1. Got {}".format(
basis_state_operation.parameters[0]
)
)

# expand basis state to device wires
basis_state_array = np.zeros(self.num_wires, dtype=int)
basis_state_array[wires] = basis_state_operation.parameters[0]

self._initial_state = np.zeros(2 ** len(self.qubits), dtype=np.complex64)
basis_state_idx = np.sum(2 ** np.argwhere(np.flip(basis_state_array) == 1))
Comment on lines 106 to 107
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this is a permutation of converting from base2 to base10 that I hadn't seen before!

Interestingly, however, this seems to be a rare case where the native Python approach is still faster:

>>> bs = np.array([1, 1, 0, 1, 1, 0, 0, 1, 1])
>>> int("".join(str(i) for i in bs), 2)
435
>>> np.sum(2 ** np.argwhere(np.flip(bs) == 1))
435
>>> %timeit np.sum(2 ** np.argwhere(np.flip(bs) == 1))
41.4 µs ± 4.82 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
>>> %timeit int("".join(str(i) for i in bs), 2)
14.4 µs ± 489 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting! I feel like the Python solution is somewhat cleaner. The time spent there is almost all in the "".join while the NumPy solution spends a significant amounts of time in np.argwhere, 2 ** and np.sum. I guess there's just more that's going on there. 🤔

self._initial_state[basis_state_idx] = 1.0

def _expand_state(self, state_vector, wires):
"""Expands state vector to more wires"""
basis_states = np.array(list(it.product([0, 1], repeat=len(wires))))

# get basis states to alter on full set of qubits
unravelled_indices = np.zeros((2 ** len(wires), self.num_wires), dtype=int)
unravelled_indices[:, wires] = basis_states

# get indices for which the state is changed to input state vector elements
ravelled_indices = np.ravel_multi_index(unravelled_indices.T, [2] * self.num_wires)

state_vector = self._scatter(ravelled_indices, state_vector, [2 ** self.num_wires])
state_vector = self._reshape(state_vector, [2] * self.num_wires)
state_vector = self._asarray(state_vector, dtype=self.C_DTYPE)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here as over on the Qulacs PR, recommend changing this to simply use native NumPy functions :)

return state_vector.flatten()

def _apply_qubit_state_vector(self, qubit_state_vector_operation):
# pylint: disable=missing-function-docstring
if not self.shots is None:
Expand All @@ -109,11 +132,15 @@ def _apply_qubit_state_vector(self, qubit_state_vector_operation):
)

state_vector = np.array(qubit_state_vector_operation.parameters[0], dtype=np.complex64)
wires = self.map_wires(qubit_state_vector_operation.wires)

if len(wires) != self.num_wires or sorted(wires) != wires.tolist():
state_vector = self._expand_state(state_vector, wires)

if len(state_vector) != 2 ** len(self.qubits):
raise qml.DeviceError(
"For QubitStateVector, the state has to be specified for the correct number of qubits. Got a state of length {}, expected {}.".format(
len(state_vector), 2 ** len(self.qubits)
len(state_vector), 2 ** len(wires)
)
)

Expand Down
45 changes: 45 additions & 0 deletions tests/test_apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,33 @@ def test_basis_state(self, shots, tol):
expected[np.ravel_multi_index(state, [2] * 4)] = 1
assert np.allclose(res, expected, **tol)

@pytest.mark.parametrize(
"state",
[
np.array([0, 0]),
np.array([1, 0]),
np.array([0, 1]),
np.array([1, 1]),
],
)
@pytest.mark.parametrize("device_wires", [3, 4, 5])
@pytest.mark.parametrize("op_wires", [[0, 1], [1, 0], [2, 0]])
def test_basis_state_on_wires_subset(self, state, device_wires, op_wires, tol):
"""Test basis state initialization on a subset of device wires"""
dev = SimulatorDevice(device_wires)

with mimic_execution_for_apply(dev):
dev.apply([qml.BasisState(state, wires=op_wires)])

res = np.abs(dev.state) ** 2
# compute expected probabilities
expected = np.zeros([2 ** len(op_wires)])
expected[np.ravel_multi_index(state, [2] * len(op_wires))] = 1

expected = dev._expand_state(expected, op_wires)

assert np.allclose(res, expected, **tol)

def test_identity_basis_state(self, shots, tol):
"""Test basis state initialization if identity"""
dev = SimulatorDevice(4, shots=shots)
Expand All @@ -119,6 +146,7 @@ def test_identity_basis_state(self, shots, tol):
expected[np.ravel_multi_index(state, [2] * 4)] = 1
assert np.allclose(res, expected, **tol)


def test_qubit_state_vector(self, init_state, shots, tol):
"""Test PauliX application"""
dev = SimulatorDevice(1, shots=shots)
Expand All @@ -131,6 +159,23 @@ def test_qubit_state_vector(self, init_state, shots, tol):
expected = state
assert np.allclose(res, expected, **tol)

@pytest.mark.parametrize("device_wires", [3, 4, 5])
@pytest.mark.parametrize("op_wires", [[0], [2], [0, 1], [1, 0], [2, 0]])
def test_qubit_state_vector_on_wires_subset(
self, init_state, device_wires, op_wires, shots, tol
):
"""Test QubitStateVector application on a subset of device wires"""
dev = SimulatorDevice(device_wires, shots=shots)
state = init_state(len(op_wires))

with mimic_execution_for_apply(dev):
dev.apply([qml.QubitStateVector(state, wires=op_wires)])

res = dev.state
expected = dev._expand_state(state, op_wires)

assert np.allclose(res, expected, **tol)

def test_invalid_qubit_state_vector(self, shots):
"""Test that an exception is raised if the state
vector is the wrong size"""
Expand Down
4 changes: 2 additions & 2 deletions tests/test_mixed_simulator_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,7 @@ def test_var_single_wire_no_parameters(

simulator_device_1_wire.reset()
simulator_device_1_wire.apply(
[qml.QubitStateVector(np.array(input), wires=[0, 1])],
[qml.QubitStateVector(np.array(input), wires=[0])],
rotations=op.diagonalizing_gates(),
)

Expand Down Expand Up @@ -724,7 +724,7 @@ def test_var_single_wire_with_parameters(

simulator_device_1_wire.reset()
simulator_device_1_wire.apply(
[qml.QubitStateVector(np.array(input), wires=[0, 1])],
[qml.QubitStateVector(np.array(input), wires=[0])],
rotations=op.diagonalizing_gates(),
)

Expand Down
27 changes: 24 additions & 3 deletions tests/test_simulator_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def test_custom_simulator(self):
def circuit():
qml.PauliX(0)
return qml.expval(qml.PauliX(0))

assert circuit() == 0.0


Expand Down Expand Up @@ -485,6 +485,27 @@ def test_qubit_state_vector_not_at_beginning_error(self, simulator_device_1_wire
)


@pytest.mark.parametrize(
"state, device_wires, op_wires, expected",
[
(np.array([1, 0]), 2, [0], [1, 0, 0, 0]),
(np.array([0, 1]), 2, [0], [0, 0, 1, 0]),
(np.array([1, 1]) / np.sqrt(2), 2, [1], np.array([1, 1, 0, 0]) / np.sqrt(2)),
(np.array([1, 1]) / np.sqrt(2), 3, [0], np.array([1, 0, 0, 0, 1, 0, 0, 0]) / np.sqrt(2)),
(np.array([1, 2, 3, 4]) / np.sqrt(48), 3, [0, 1], np.array([1, 0, 2, 0, 3, 0, 4, 0]) / np.sqrt(48)),
(np.array([1, 2, 3, 4]) / np.sqrt(48), 3, [1, 0], np.array([1, 0, 3, 0, 2, 0, 4, 0]) / np.sqrt(48)),
(np.array([1, 2, 3, 4]) / np.sqrt(48), 3, [0, 2], np.array([1, 2, 0, 0, 3, 4, 0, 0]) / np.sqrt(48)),
(np.array([1, 2, 3, 4]) / np.sqrt(48), 3, [1, 2], np.array([1, 2, 3, 4, 0, 0, 0, 0]) / np.sqrt(48)),
],
)
@pytest.mark.parametrize("shots", [None])
def test_expand_state(state, op_wires, device_wires, expected, tol):
"""Test that the expand_state method works as expected."""
dev = SimulatorDevice(device_wires)
res = dev._expand_state(state, op_wires)

assert np.allclose(res, expected, **tol)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great tests!


@pytest.mark.parametrize("shots", [1000])
class TestStatePreparationErrorsNonAnalytic:
"""Tests state preparation errors that occur for non-analytic devices."""
Expand Down Expand Up @@ -690,7 +711,7 @@ def test_var_single_wire_no_parameters(

simulator_device_1_wire.reset()
simulator_device_1_wire.apply(
[qml.QubitStateVector(np.array(input), wires=[0, 1])],
[qml.QubitStateVector(np.array(input), wires=[0])],
rotations=op.diagonalizing_gates(),
)

Expand Down Expand Up @@ -721,7 +742,7 @@ def test_var_single_wire_with_parameters(

simulator_device_1_wire.reset()
simulator_device_1_wire.apply(
[qml.QubitStateVector(np.array(input), wires=[0, 1])],
[qml.QubitStateVector(np.array(input), wires=[0])],
rotations=op.diagonalizing_gates(),
)

Expand Down