Skip to content

Commit

Permalink
[WIP] Redesign capabilities dictionary (#781)
Browse files Browse the repository at this point in the history
* prototype

* backup

* backup

* finish prototype

* backup

* make values boolean

* backup

* update dictionary keys

* backup

* fix bugs in tensor tests, set tol higher for sampled tests

* add noisy capability

* improve developer information

* remove sampled from tensor device, and add analytic attribute

* fix all tests for base devices

* polish docstring some more

* reverse changing of tolerance

* fix general tests

* fix some more tests

* black

* fix code factor

* black again

* add capabilities tests

* update black version and rerun black

* Update pennylane/_device.py

Co-authored-by: Josh Izaac <josh146@gmail.com>

* Update pennylane/_device.py

* Update pennylane/devices/tests/test_gates.py

Co-authored-by: Josh Izaac <josh146@gmail.com>

* rewrite core to check for old and new keys

* add test for reversible diff

* fix isinstance

* black needs to be applied again

* polish

* fix test bug

* Update doc/development/plugins.rst

Co-authored-by: antalszava <antalszava@gmail.com>

* remove unused capabilities

* remove accidentally commited files

* fix test

* polish docstring

* remove default keys from Device, make better capability device tests, add 'returns_state' and 'returns_probs' argument

* black

* fix codefactor issues

* black

* implement Tom's code review

* fix test

* fix test again

Co-authored-by: Josh Izaac <josh146@gmail.com>
Co-authored-by: antalszava <antalszava@gmail.com>
  • Loading branch information
3 people authored Sep 14, 2020
1 parent 3e293c2 commit 5f8064f
Show file tree
Hide file tree
Showing 29 changed files with 726 additions and 204 deletions.
26 changes: 20 additions & 6 deletions doc/development/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ and use the device. These include:
Defining all these attributes is mandatory.


Supporting operations
---------------------
Device capabilities
-------------------

You must further tell PennyLane about the operations that your device supports
as well as potential further capabilities, by providing the following class attributes/properties:
Expand All @@ -101,15 +101,29 @@ as well as potential further capabilities, by providing the following class attr
conversion between the two conventions takes place automatically
by the plugin device.

* :attr:`.Device._capabilities`: a dictionary containing information about the capabilities of
the device. Keys currently supported include:
* :func:`.Device.capabilities`: A class method which returns the dictionary of capabilities of a device. A
new device should override this method to retrieve the parent classes' capabilities dictionary, make a copy
and update and/or add capabilities before returning the copy.

* ``'model'`` (*str*): either ``'qubit'`` or ``'CV'``.
Examples of capabilities are:

* ``'inverse_operations'`` (*bool*): ``True`` if the device supports
* ``'model'`` (*str*): either ``'qubit'`` or ``'cv'``.

* ``'returns_state'`` (*bool*): ``True`` if the device returns the quantum state via ``dev.state``.

* ``'supports_inverse_operations'`` (*bool*): ``True`` if the device supports
applying the inverse of operations. Operations which should be inverted
have the property ``operation.inverse == True``.

* ``'supports_tensor_observables'`` (*bool*): ``True`` if the device supports observables composed from tensor
products such as ``PauliZ(wires=0) @ PauliZ(wires=1)``.

Some capabilities are queried by PennyLane core to make decisions on how to best run computations, others are used
by external apps built on top of the device ecosystem.

To find out which capabilities are (possibly automatically) defined for your device ``dev = device('my.device')``,
check the output of ``dev.capabilities()``.

Adding arguments to your device
--------------------------------

Expand Down
49 changes: 40 additions & 9 deletions pennylane/_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ class Device(abc.ABC):
"""

# pylint: disable=too-many-public-methods
_capabilities = {} #: dict[str->*]: plugin capabilities
_capabilities = {
"model": None,
}
"""The capabilities dictionary stores the properties of a device. Devices can add their
own custom properties and overwrite existing ones by overriding the ``capabilities()`` method."""

_circuits = {} #: dict[str->Circuit]: circuit templates associated with this API class
_asarray = staticmethod(np.asarray)

Expand Down Expand Up @@ -213,9 +218,20 @@ def map_wires(self, wires):

@classmethod
def capabilities(cls):
"""Get the other capabilities of the plugin.
"""Get the capabilities of this device class.
Inheriting classes that change or add capabilities must override this method, for example via
Measurements, batching etc.
.. code-block:: python
@classmethod
def capabilities(cls):
capabilities = super().capabilities().copy()
capabilities.update(
supports_inverse_operations=False,
supports_a_new_capability=True,
)
return capabilities
Returns:
dict[str->*]: results
Expand Down Expand Up @@ -411,9 +427,12 @@ def supports_operation(self, operation):
if isinstance(operation, str):

if operation.endswith(Operation.string_for_inverse):
return operation[
: -len(Operation.string_for_inverse)
] in self.operations and self.capabilities().get("inverse_operations", False)
in_ops = operation[: -len(Operation.string_for_inverse)] in self.operations
# TODO: update when all capabilities keys changed to "supports_inverse_operations"
supports_inv = self.capabilities().get(
"supports_inverse_operations", False
) or self.capabilities().get("inverse_operations", False)
return in_ops and supports_inv

return operation in self.operations

Expand Down Expand Up @@ -468,7 +487,11 @@ def check_validity(self, queue, observables):
operation_name = o.name

if o.inverse:
if not self.capabilities().get("inverse_operations", False):
# TODO: update when all capabilities keys changed to "supports_inverse_operations"
supports_inv = self.capabilities().get(
"supports_inverse_operations", False
) or self.capabilities().get("inverse_operations", False)
if not supports_inv:
raise DeviceError(
"The inverse of gates are not supported on device {}".format(
self.short_name
Expand All @@ -484,7 +507,11 @@ def check_validity(self, queue, observables):
for o in observables:

if isinstance(o, Tensor):
if not self.capabilities().get("tensor_observables", False):
# TODO: update when all capabilities keys changed to "supports_tensor_observables"
supports_tensor = self.capabilities().get(
"supports_tensor_observables", False
) or self.capabilities().get("tensor_observables", False)
if not supports_tensor:
raise DeviceError(
"Tensor observables not supported on device {}".format(self.short_name)
)
Expand All @@ -501,7 +528,11 @@ def check_validity(self, queue, observables):
observable_name = o.name

if issubclass(o.__class__, Operation) and o.inverse:
if not self.capabilities().get("inverse_operations", False):
# TODO: update when all capabilities keys changed to "supports_inverse_operations"
supports_inv = self.capabilities().get(
"supports_inverse_operations", False
) or self.capabilities().get("inverse_operations", False)
if not supports_inv:
raise DeviceError(
"The inverse of gates are not supported on device {}".format(
self.short_name
Expand Down
32 changes: 7 additions & 25 deletions pennylane/_qubit_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,32 +113,14 @@ def __init__(self, wires=1, shots=1000, analytic=True):

@classmethod
def capabilities(cls):
"""Get the capabilities of the plugin.

Capabilities include:
* ``"model"`` (*str*): either ``"qubit"`` or ``"CV"``.
* ``"inverse_operations"`` (*bool*): ``True`` if the device supports
applying the inverse of operations. Operations which should be inverted
have ``operation.inverse == True``.
* ``"tensor_observables" (*bool*): ``True`` if the device supports
expectation values/variance/samples of :class:`~.Tensor` observables.
The qubit device class has built-in support for tensor observables. As a
result, devices that inherit from this class automatically
have the following items in their capabilities
dictionary:
* ``"model": "qubit"``
* ``"tensor_observables": True``
Returns:
dict[str->*]: results
"""
capabilities = cls._capabilities
capabilities.update(model="qubit", tensor_observables=True)
capabilities = super().capabilities().copy()
capabilities.update(
model="qubit",
supports_finite_shots=True,
supports_tensor_observables=True,
returns_probs=True,
)
return capabilities

def reset(self):
Expand Down
16 changes: 15 additions & 1 deletion pennylane/beta/devices/default_tensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
r"""
Experimental simulator plugin based on tensor network contractions
"""
# pylint: disable=too-many-instance-attributes
import warnings
from itertools import product

Expand Down Expand Up @@ -101,7 +102,6 @@ class DefaultTensor(Device):
pennylane_requires = "0.12"
version = "0.12.0"
author = "Xanadu Inc."
_capabilities = {"model": "qubit", "tensor_observables": True}

_operation_map = {
"BasisState": None,
Expand Down Expand Up @@ -164,6 +164,20 @@ def __init__(self, wires, shots=1000, representation="exact", contraction_method
self._rep = representation
self._contraction_method = contraction_method
self.reset()
self.analytic = False

@classmethod
def capabilities(cls):
capabilities = super().capabilities().copy()
capabilities.update(
model="qubit",
supports_analytic_computation=True,
supports_finite_shots=False,
supports_tensor_observables=True,
returns_state=False,
returns_probs=False,
)
return capabilities

def reset(self):
"""Reset the device."""
Expand Down
15 changes: 9 additions & 6 deletions pennylane/beta/devices/default_tensor_tf.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,6 @@ class DefaultTensorTF(DefaultTensor):
# pylint: disable=too-many-instance-attributes
name = "PennyLane TensorNetwork (TensorFlow) simulator plugin"
short_name = "default.tensor.tf"
_capabilities = {
"model": "qubit",
"tensor_observables": True,
"provides_jacobian": True,
"passthru_interface": "tf",
}

_operation_map = copy.copy(DefaultTensor._operation_map)
_operation_map.update(
Expand Down Expand Up @@ -174,6 +168,15 @@ def __init__(self, wires, shots=1000, representation="exact", contraction_method
contraction_method=contraction_method,
)

@classmethod
def capabilities(cls):
capabilities = super().capabilities().copy()
capabilities.update(
provides_jacobian=True,
passthru_interface="tf",
)
return capabilities

def reset(self):
self.res = None
self.variables = []
Expand Down
3 changes: 3 additions & 0 deletions pennylane/devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,8 @@
autograd_ops
tests
"""
# DefaultQubitTF and DefaultQubitAutograd not imported here since this
# would lead to an automatic import of tensorflow and autograd, which are
# not PennyLane core dependencies
from .default_qubit import DefaultQubit
from .default_gaussian import DefaultGaussian
15 changes: 13 additions & 2 deletions pennylane/devices/default_gaussian.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,8 +656,6 @@ class DefaultGaussian(Device):
version = "0.12.0"
author = "Xanadu Inc."

_capabilities = {"model": "cv"}

_operation_map = {
"Beamsplitter": beamsplitter,
"ControlledAddition": controlled_addition,
Expand Down Expand Up @@ -695,6 +693,19 @@ def __init__(self, wires, *, shots=1000, hbar=2, analytic=True):

self.reset()

@classmethod
def capabilities(cls):
capabilities = super().capabilities().copy()
capabilities.update(
model="cv",
supports_analytic_computation=True,
supports_finite_shots=True,
returns_probs=False,
returns_state=False,
supports_reversible_diff=False,
)
return capabilities

def pre_apply(self):
self.reset()

Expand Down
13 changes: 12 additions & 1 deletion pennylane/devices/default_qubit.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ class DefaultQubit(QubitDevice):
pennylane_requires = "0.12"
version = "0.12.0"
author = "Xanadu Inc."
_capabilities = {"inverse_operations": True, "reversible_diff": True}

operations = {
"BasisState",
Expand Down Expand Up @@ -360,6 +359,18 @@ def _get_unitary_matrix(self, unitary): # pylint: disable=no-self-use

return unitary.matrix

@classmethod
def capabilities(cls):
capabilities = super().capabilities().copy()
capabilities.update(
model="qubit",
supports_reversible_diff=True,
supports_inverse_operations=True,
supports_analytic_computation=True,
returns_state=True,
)
return capabilities

def _create_basis_state(self, index):
"""Return a computational basis state over all wires.
Expand Down
15 changes: 9 additions & 6 deletions pennylane/devices/default_qubit_autograd.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,6 @@ class DefaultQubitAutograd(DefaultQubit):
name = "Default qubit (Autograd) PennyLane plugin"
short_name = "default.qubit.autograd"

_capabilities = {
"model": "qubit",
"provides_jacobian": False,
"passthru_interface": "autograd",
}

parametric_ops = {
"PhaseShift": autograd_ops.PhaseShift,
"RX": autograd_ops.RX,
Expand Down Expand Up @@ -118,6 +112,15 @@ def __init__(self, wires, *, shots=1000, analytic=True):
del self._apply_ops["Hadamard"]
del self._apply_ops["CZ"]

@classmethod
def capabilities(cls):
capabilities = super().capabilities().copy()
capabilities.update(
passthru_interface="autograd",
supports_reversible_diff=False,
)
return capabilities

@staticmethod
def _scatter(indices, array, new_dimensions):
new_array = np.zeros(new_dimensions, dtype=array.dtype.type)
Expand Down
15 changes: 9 additions & 6 deletions pennylane/devices/default_qubit_tf.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,6 @@ class DefaultQubitTF(DefaultQubit):
name = "Default qubit (TensorFlow) PennyLane plugin"
short_name = "default.qubit.tf"

_capabilities = {
"model": "qubit",
"provides_jacobian": False,
"passthru_interface": "tf",
}

parametric_ops = {
"PhaseShift": tf_ops.PhaseShift,
"RX": tf_ops.RX,
Expand Down Expand Up @@ -165,6 +159,15 @@ def __init__(self, wires, *, shots=1000, analytic=True):
# prevent using special apply method for this gate due to slowdown in TF implementation
del self._apply_ops["CZ"]

@classmethod
def capabilities(cls):
capabilities = super().capabilities().copy()
capabilities.update(
passthru_interface="tf",
supports_reversible_diff=False,
)
return capabilities

@staticmethod
def _scatter(indices, array, new_dimensions):
indices = np.expand_dims(indices, 1)
Expand Down
4 changes: 2 additions & 2 deletions pennylane/devices/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
# List of all devices that are included in PennyLane
LIST_CORE_DEVICES = {"default.qubit", "default.qubit.tf", "default.qubit.autograd"}
# TODO: add beta devices "default.tensor", "default.tensor.tf", which currently
# do not have an "analytic" attribute.
# do not return probabilities.


@pytest.fixture(scope="function")
Expand Down Expand Up @@ -104,7 +104,7 @@ def _device(wires):
)

capabilities = dev.capabilities()
if "model" not in capabilities or not capabilities["model"] == "qubit":
if capabilities.get("model", None) != "qubit":
# exit the tests if device based on cv model (currently not supported)
pytest.exit("The device test suite currently only runs on qubit-based devices.")

Expand Down
Loading

0 comments on commit 5f8064f

Please sign in to comment.