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

qml.Projector compatibility with new default qubit #4452

Merged
merged 23 commits into from Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0eaa453
compatibility with DefaultQubit2
BorjaRequena Aug 8, 2023
86860fd
update changelog
BorjaRequena Aug 8, 2023
07e769c
Merge branch 'master' into projector_pickle
BorjaRequena Aug 9, 2023
c9fd165
Merge branch 'projector_pickle' of https://github.com/PennyLaneAI/pen…
BorjaRequena Aug 9, 2023
34abc15
testing tests
BorjaRequena Aug 11, 2023
f6f3634
Merge branch 'master' into projector_pickle
BorjaRequena Aug 11, 2023
2001d2b
Merge branch 'master' into projector_pickle
BorjaRequena Aug 11, 2023
3cce663
extensive testing
BorjaRequena Aug 11, 2023
657815d
more tests
BorjaRequena Aug 11, 2023
86af774
Merge branch 'master' into projector_pickle
BorjaRequena Aug 11, 2023
7c066dd
Fix `__reduce__` docstring
BorjaRequena Aug 11, 2023
8d966b6
Fix test
BorjaRequena Aug 11, 2023
943550a
Merge branch 'master' into projector_pickle
BorjaRequena Aug 14, 2023
15da633
simplify typing
BorjaRequena Aug 14, 2023
8880538
formatting
BorjaRequena Aug 14, 2023
7b07118
simplify measurement logic
BorjaRequena Aug 14, 2023
37c9a48
Merge branch 'master' into projector_pickle
BorjaRequena Aug 14, 2023
724437f
serialize without
BorjaRequena Aug 15, 2023
27bfc99
Merge branch 'master' into projector_pickle
BorjaRequena Aug 15, 2023
92e2b32
remove copy and extend serialization tests
BorjaRequena Aug 15, 2023
e624c32
remove redundant inheritance
BorjaRequena Aug 15, 2023
64cb458
Merge branch 'master' into projector_pickle
BorjaRequena Aug 15, 2023
c14c299
Merge branch 'master' into projector_pickle
BorjaRequena Aug 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/releases/changelog-dev.md
Expand Up @@ -299,6 +299,9 @@ array([False, False])

<h3>Bug fixes 🐛</h3>

* `qml.Projector` is pickle-able again.
[(#4452)](https://github.com/PennyLaneAI/pennylane/pull/4452)

* Allow sparse matrix calculation of `SProd`s containing a `Tensor`. When using
`Tensor.sparse_matrix()`, it is recommended to use the `wire_order` keyword argument over `wires`.
[(#4424)](https://github.com/PennyLaneAI/pennylane/pull/4424)
Expand Down
9 changes: 3 additions & 6 deletions pennylane/_qubit_device.py
Expand Up @@ -63,6 +63,7 @@
VnEntropyMP,
Shots,
)
from pennylane.ops.qubit.observables import BasisStateProjector
from pennylane.resource import Resources
from pennylane.operation import operation_derivative, Operation
from pennylane.tape import QuantumScript, QuantumTape
Expand Down Expand Up @@ -1674,9 +1675,7 @@ def marginal_prob(self, prob, wires=None):
return self._reshape(prob, flat_shape)

def expval(self, observable, shot_range=None, bin_size=None):
if observable.name == "Projector" and len(observable.parameters[0]) == len(
observable.wires
):
if isinstance(observable, BasisStateProjector):
# branch specifically to handle the basis state projector observable
idx = int("".join(str(i) for i in observable.parameters[0]), 2)
probs = self.probability(
Expand Down Expand Up @@ -1706,9 +1705,7 @@ def expval(self, observable, shot_range=None, bin_size=None):
return np.squeeze(np.mean(samples, axis=axis))

def var(self, observable, shot_range=None, bin_size=None):
if observable.name == "Projector" and len(observable.parameters[0]) == len(
observable.wires
):
if isinstance(observable, BasisStateProjector):
# branch specifically to handle the basis state projector observable
idx = int("".join(str(i) for i in observable.parameters[0]), 2)
probs = self.probability(
Expand Down
10 changes: 5 additions & 5 deletions pennylane/measurements/expval.py
Expand Up @@ -19,7 +19,7 @@

import pennylane as qml
from pennylane.operation import Operator
from pennylane.ops import Projector
from pennylane.ops.qubit.observables import BasisStateProjector
from pennylane.wires import Wires

from .measurements import Expectation, SampleMeasurement, StateMeasurement
Expand Down Expand Up @@ -104,8 +104,8 @@ def process_samples(
shot_range: Tuple[int] = None,
bin_size: int = None,
):
if isinstance(self.obs, Projector):
# branch specifically to handle the projector observable
if isinstance(self.obs, BasisStateProjector):
# branch specifically to handle the basis state projector observable
idx = int("".join(str(i) for i in self.obs.parameters[0]), 2)
probs = qml.probs(wires=self.wires).process_samples(
samples=samples, wire_order=wire_order, shot_range=shot_range, bin_size=bin_size
Expand All @@ -122,8 +122,8 @@ def process_samples(
return qml.math.squeeze(qml.math.mean(samples, axis=axis))

def process_state(self, state: Sequence[complex], wire_order: Wires):
if isinstance(self.obs, Projector):
# branch specifically to handle the projector observable
if isinstance(self.obs, BasisStateProjector):
# branch specifically to handle the basis state projector observable
idx = int("".join(str(i) for i in self.obs.parameters[0]), 2)
probs = qml.probs(wires=self.wires).process_state(state=state, wire_order=wire_order)
return probs[idx]
Expand Down
10 changes: 5 additions & 5 deletions pennylane/measurements/var.py
Expand Up @@ -20,7 +20,7 @@

import pennylane as qml
from pennylane.operation import Operator
from pennylane.ops import Projector
from pennylane.ops.qubit.observables import BasisStateProjector
from pennylane.wires import Wires

from .measurements import SampleMeasurement, StateMeasurement, Variance
Expand Down Expand Up @@ -104,8 +104,8 @@ def process_samples(
shot_range: Tuple[int] = None,
bin_size: int = None,
):
if isinstance(self.obs, Projector):
# branch specifically to handle the projector observable
if isinstance(self.obs, BasisStateProjector):
# branch specifically to handle the basis state projector observable
idx = int("".join(str(i) for i in self.obs.parameters[0]), 2)
# we use ``self.wires`` instead of ``self.obs`` because the observable was
# already applied before the sampling
Expand All @@ -125,8 +125,8 @@ def process_samples(
return qml.math.squeeze(qml.math.var(samples, axis=axis))

def process_state(self, state: Sequence[complex], wire_order: Wires):
if isinstance(self.obs, Projector):
# branch specifically to handle the projector observable
if isinstance(self.obs, BasisStateProjector):
# branch specifically to handle the basis state projector observable
idx = int("".join(str(i) for i in self.obs.parameters[0]), 2)
# we use ``self.wires`` instead of ``self.obs`` because the observable was
# already applied to the state
Expand Down
61 changes: 33 additions & 28 deletions pennylane/ops/qubit/observables.py
Expand Up @@ -365,11 +365,10 @@ class Projector(Observable):
0.25

"""
name = "Projector"
num_wires = AnyWires
num_params = 1
"""int: Number of trainable parameters that the operator depends on."""
_basis_state_type = None # type if Projector inherits from _BasisStateProjector
_state_vector_type = None # type if Projector inherits from _StateVectorProjector

def __new__(cls, state, wires, **_):
"""Changes parents based on the state representation.
Expand All @@ -396,16 +395,10 @@ def __new__(cls, state, wires, **_):
raise ValueError(f"Input state must be one-dimensional; got shape {shape}.")

if len(state) == len(wires):
if cls._basis_state_type is None:
base_cls = (_BasisStateProjector, Projector)
cls._basis_state_type = type("Projector", base_cls, dict(cls.__dict__))
return object.__new__(cls._basis_state_type)
return object.__new__(BasisStateProjector)

if len(state) == 2 ** len(wires):
if cls._state_vector_type is None:
base_cls = (_StateVectorProjector, Projector)
cls._state_vector_type = type("Projector", base_cls, dict(cls.__dict__))
return object.__new__(cls._state_vector_type)
return object.__new__(StateVectorProjector)

raise ValueError(
"Input state should have the same length as the wires in the case "
Expand All @@ -428,18 +421,24 @@ def __copy__(self):
return copied_op


class _BasisStateProjector(Observable):
class BasisStateProjector(Projector, Observable):
BorjaRequena marked this conversation as resolved.
Show resolved Hide resolved
r"""Observable corresponding to the state projector :math:`P=\ket{\phi}\bra{\phi}`, where
:math:`\phi` denotes a basis state."""

# The call signature should be the same as Projector.__new__ for the positional
# arguments, but with free key word arguments.
def __init__(self, state, wires, id=None):
wires = Wires(wires)
state = list(qml.math.toarray(state))
state = list(qml.math.toarray(state).astype(int))
BorjaRequena marked this conversation as resolved.
Show resolved Hide resolved

if not set(state).issubset({0, 1}):
raise ValueError(f"Basis state must only consist of 0s and 1s; got {state}")

super().__init__(state, wires=wires, id=id)

def __new__(cls): # pylint: disable=arguments-differ
return object.__new__(cls)

def label(self, decimals=None, base_label=None, cache=None):
r"""A customizable string representation of the operator.

Expand All @@ -455,7 +454,7 @@ def label(self, decimals=None, base_label=None, cache=None):

**Example:**

>>> _BasisStateProjector([0, 1, 0], wires=(0, 1, 2)).label()
>>> BasisStateProjector([0, 1, 0], wires=(0, 1, 2)).label()
'|010⟩⟨010|'

"""
Expand All @@ -472,7 +471,7 @@ def compute_matrix(basis_state): # pylint: disable=arguments-differ
The canonical matrix is the textbook matrix representation that does not consider wires.
Implicitly, this assumes that the wires of the operator correspond to the global wire order.

.. seealso:: :meth:`~._BasisStateProjector.matrix`
.. seealso:: :meth:`~.BasisStateProjector.matrix`

Args:
basis_state (Iterable): basis state to project on
Expand All @@ -482,7 +481,7 @@ def compute_matrix(basis_state): # pylint: disable=arguments-differ

**Example**

>>> _BasisStateProjector.compute_matrix([0, 1])
>>> BasisStateProjector.compute_matrix([0, 1])
[[0. 0. 0. 0.]
[0. 1. 0. 0.]
[0. 0. 0. 0.]
Expand All @@ -506,7 +505,7 @@ def compute_eigvals(basis_state): # pylint: disable=arguments-differ

Otherwise, no particular order for the eigenvalues is guaranteed.

.. seealso:: :meth:`~._BasisStateProjector.eigvals`
.. seealso:: :meth:`~.BasisStateProjector.eigvals`

Args:
basis_state (Iterable): basis state to project on
Expand All @@ -516,7 +515,7 @@ def compute_eigvals(basis_state): # pylint: disable=arguments-differ

**Example**

>>> _BasisStateProjector.compute_eigvals([0, 1])
>>> BasisStateProjector.compute_eigvals([0, 1])
[0. 1. 0. 0.]
"""
w = np.zeros(2 ** len(basis_state))
Expand All @@ -537,7 +536,7 @@ def compute_diagonalizing_gates(
The diagonalizing gates rotate the state into the eigenbasis
of the operator.

.. seealso:: :meth:`~._BasisStateProjector.diagonalizing_gates`.
.. seealso:: :meth:`~.BasisStateProjector.diagonalizing_gates`.

Args:
basis_state (Iterable): basis state that the operator projects on
Expand All @@ -547,13 +546,16 @@ def compute_diagonalizing_gates(

**Example**

>>> _BasisStateProjector.compute_diagonalizing_gates([0, 1, 0, 0], wires=[0, 1])
>>> BasisStateProjector.compute_diagonalizing_gates([0, 1, 0, 0], wires=[0, 1])
[]
"""
return []


class _StateVectorProjector(Observable):
class StateVectorProjector(Projector, Observable):
r"""Observable corresponding to the state projector :math:`P=\ket{\phi}\bra{\phi}`, where
:math:`\phi` denotes a state."""

# The call signature should be the same as Projector.__new__ for the positional
# arguments, but with free key word arguments.
def __init__(self, state, wires, id=None):
Expand All @@ -562,6 +564,9 @@ def __init__(self, state, wires, id=None):

super().__init__(state, wires=wires, id=id)

def __new__(cls): # pylint: disable=arguments-differ
return object.__new__(cls)

def label(self, decimals=None, base_label=None, cache=None):
r"""A customizable string representation of the operator.

Expand All @@ -578,14 +583,14 @@ def label(self, decimals=None, base_label=None, cache=None):
**Example:**

>>> state_vector = np.array([0, 1, 1, 0])/np.sqrt(2)
>>> _StateVectorProjector(state_vector, wires=(0, 1)).label()
>>> StateVectorProjector(state_vector, wires=(0, 1)).label()
'P'
>>> _StateVectorProjector(state_vector, wires=(0, 1)).label(base_label="hi!")
>>> StateVectorProjector(state_vector, wires=(0, 1)).label(base_label="hi!")
'hi!'
>>> dev = qml.device("default.qubit", wires=1)
>>> @qml.qnode(dev)
>>> def circuit(state):
... return qml.expval(_StateVectorProjector(state, [0]))
... return qml.expval(StateVectorProjector(state, [0]))
>>> print(qml.draw(circuit)([1, 0]))
0: ───┤ <|0⟩⟨0|>
>>> print(qml.draw(circuit)(np.array([1, 1]) / np.sqrt(2)))
Expand Down Expand Up @@ -631,7 +636,7 @@ def compute_matrix(state_vector): # pylint: disable=arguments-differ,arguments-

The projector of the state :math:`\frac{1}{\sqrt{2}}(\ket{01}+\ket{10})`

>>> _StateVectorProjector.compute_matrix([0, 1/np.sqrt(2), 1/np.sqrt(2), 0])
>>> StateVectorProjector.compute_matrix([0, 1/np.sqrt(2), 1/np.sqrt(2), 0])
[[0. 0. 0. 0.]
[0. 0.5 0.5 0.]
[0. 0.5 0.5 0.]
Expand All @@ -652,7 +657,7 @@ def compute_eigvals(state_vector): # pylint: disable=arguments-differ,arguments

Otherwise, no particular order for the eigenvalues is guaranteed.

.. seealso:: :meth:`~._StateVectorProjector.eigvals`
.. seealso:: :meth:`~.StateVectorProjector.eigvals`

Args:
state_vector (Iterable): state vector to project on
Expand All @@ -662,7 +667,7 @@ def compute_eigvals(state_vector): # pylint: disable=arguments-differ,arguments

**Example**

>>> _StateVectorProjector.compute_eigvals([0, 0, 1, 0])
>>> StateVectorProjector.compute_eigvals([0, 0, 1, 0])
array([1, 0, 0, 0])
"""
w = qml.math.zeros_like(state_vector)
Expand All @@ -682,7 +687,7 @@ def compute_diagonalizing_gates(
The diagonalizing gates rotate the state into the eigenbasis
of the operator.

.. seealso:: :meth:`~._StateVectorProjector.diagonalizing_gates`.
.. seealso:: :meth:`~.StateVectorProjector.diagonalizing_gates`.

Args:
state_vector (Iterable): state vector that the operator projects on.
Expand All @@ -693,7 +698,7 @@ def compute_diagonalizing_gates(
**Example**

>>> state_vector = np.array([1., 1j])/np.sqrt(2)
>>> _StateVectorProjector.compute_diagonalizing_gates(state_vector, wires=[0])
>>> StateVectorProjector.compute_diagonalizing_gates(state_vector, wires=[0])
[QubitUnitary(array([[ 0.70710678+0.j , 0. -0.70710678j],
[ 0. +0.70710678j, -0.70710678+0.j ]]), wires=[0])]
"""
Expand Down
20 changes: 20 additions & 0 deletions tests/devices/experimental/test_default_qubit_2.py
Expand Up @@ -1787,6 +1787,26 @@ def test_shot_vectors(self, max_workers, n_qubits, shots):
assert np.all(np.logical_or(np.logical_or(r[1] == 0, r[1] == 1), r[1] == 2))


class TestDynamicType:
"""Tests the compatibility with dynamic type classes such as `qml.Projector`."""

@pytest.mark.parametrize("n_wires", [1, 2, 3])
@pytest.mark.parametrize("max_workers", [None, 1, 2])
def test_projector(self, max_workers, n_wires):
"""Test that qml.Projector yields the expected results for both of its subclasses."""
wires = list(range(n_wires))
dev = DefaultQubit2(max_workers=max_workers)
ops = [qml.Hadamard(q) for q in wires]
albi3ro marked this conversation as resolved.
Show resolved Hide resolved
basis_state = np.zeros((n_wires,))
state_vector = np.zeros((2**n_wires,))
state_vector[0] = 1

for state in [basis_state, state_vector]:
qs = qml.tape.QuantumScript(ops, [qml.expval(qml.Projector(state, wires))])
res = dev.execute(qs)
assert np.isclose(res, 1 / 2**n_wires)


@pytest.mark.parametrize("max_workers", [None, 1, 2])
def test_broadcasted_parameter(max_workers):
"""Test that DefaultQubit2 handles broadcasted parameters as expected."""
Expand Down
12 changes: 6 additions & 6 deletions tests/gradients/parameter_shift/test_parameter_shift.py
Expand Up @@ -2183,17 +2183,17 @@ def test_recycling_unshifted_tape_result(self):
# + 2 operations x 2 shifted positions + 1 unshifted term <-- <H^2>
assert len(tapes) == (2 * 2 + 1) + (2 * 2 + 1)

def test_projector_variance(self, tol):
@pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector
def test_projector_variance(self, state, tol):
"""Test that the variance of a projector is correctly returned"""
dev = qml.device("default.qubit", wires=2)
P = np.array([1])
x, y = 0.765, -0.654

with qml.queuing.AnnotatedQueue() as q:
qml.RX(x, wires=0)
qml.RY(y, wires=1)
qml.CNOT(wires=[0, 1])
qml.var(qml.Projector(P, wires=0) @ qml.PauliX(1))
qml.var(qml.Projector(state, wires=0) @ qml.PauliX(1))

tape = qml.tape.QuantumScript.from_queue(q)
tape.trainable_params = {0, 1}
Expand Down Expand Up @@ -2983,17 +2983,17 @@ def test_expval_and_variance(self, tol):
# assert gradA == pytest.approx(expected, abs=tol)
# assert gradF == pytest.approx(expected, abs=tol)

def test_projector_variance(self, tol):
@pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector
def test_projector_variance(self, state, tol):
"""Test that the variance of a projector is correctly returned"""
dev = qml.device("default.qubit", wires=2)
P = np.array([1])
x, y = 0.765, -0.654

with qml.queuing.AnnotatedQueue() as q:
qml.RX(x, wires=0)
qml.RY(y, wires=1)
qml.CNOT(wires=[0, 1])
qml.var(qml.Projector(P, wires=0) @ qml.PauliX(1))
qml.var(qml.Projector(state, wires=0) @ qml.PauliX(1))

tape = qml.tape.QuantumScript.from_queue(q)
tape.trainable_params = {0, 1}
Expand Down
Expand Up @@ -1835,18 +1835,18 @@ def test_expval_and_variance_multi_param(self):
for gradF in all_gradF:
assert gradF == pytest.approx(expected, abs=finite_diff_tol)

def test_projector_variance(self):
@pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector
def test_projector_variance(self, state):
"""Test that the variance of a projector is correctly returned"""
shot_vec = many_shots_shot_vector
dev = qml.device("default.qubit", wires=2, shots=shot_vec)
P = np.array([1])
x, y = 0.765, -0.654

with qml.queuing.AnnotatedQueue() as q:
qml.RX(x, wires=0)
qml.RY(y, wires=1)
qml.CNOT(wires=[0, 1])
qml.var(qml.Projector(P, wires=0) @ qml.PauliX(1))
qml.var(qml.Projector(state, wires=0) @ qml.PauliX(1))

tape = qml.tape.QuantumScript.from_queue(q, shots=shot_vec)
tape.trainable_params = {0, 1}
Expand Down