Skip to content

Commit

Permalink
Move qml.device to qml.devices (#6030)
Browse files Browse the repository at this point in the history
**Context:**
The root `__init__` file is bloated with device instantiation code
through the `qml.device` function and its helper functions. It is also
contaminating the namespace with unrelated third-part packages imports
needed for its functionality. The code doesn't need to live in this file
since we have a dedicated `qml.devices` module now.

**Description of the Change:**
The relevant code is moved to the `__init__` of the `qml.devices` module
and reference in the root `__init__` file and the docs are adapted. The
imports are also pushed inside the relevant functions to avoid
contaminating the namespace.

**Benefits:**
More localized functional scopes.

**Possible Drawbacks:**
None

[[sc-65678](https://app.shortcut.com/xanaduai/story/65678)]
  • Loading branch information
Shiro-Raven committed Jul 24, 2024
1 parent be355e0 commit a456fce
Show file tree
Hide file tree
Showing 5 changed files with 327 additions and 275 deletions.
2 changes: 1 addition & 1 deletion doc/code/qml.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ qml
.. automodapi:: pennylane
:no-heading:
:include-all-objects:
:skip: Version, SimpleSpec, plugin_devices, plugin_converters, default_config, reload, version_info, defaultdict
:skip: plugin_converters, default_config, version_info, defaultdict
277 changes: 5 additions & 272 deletions pennylane/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,9 @@
This is the top level module from which all basic functions and classes of
PennyLane can be directly imported.
"""
from importlib import reload, metadata
from sys import version_info

import warnings
import numpy as _np

from semantic_version import SimpleSpec, Version

from pennylane.boolean_fn import BooleanFn
import pennylane.numpy
Expand Down Expand Up @@ -155,6 +151,8 @@
import pennylane.noise
from pennylane.noise import NoiseModel

from pennylane.devices.device_constructor import device, refresh_devices

# Look for an existing configuration file
default_config = Configuration("config.toml")

Expand All @@ -176,275 +174,10 @@ def __getattr__(name):
return pennylane.ops.LinearCombination
return pennylane.ops.Hamiltonian

raise AttributeError(f"module 'pennylane' has no attribute '{name}'")


def _get_device_entrypoints():
"""Returns a dictionary mapping the device short name to the
loadable entrypoint"""
entries = (
metadata.entry_points()["pennylane.plugins"]
if version_info[:2] == (3, 9)
# pylint:disable=unexpected-keyword-arg
else metadata.entry_points(group="pennylane.plugins")
)
return {entry.name: entry for entry in entries}


def refresh_devices():
"""Scan installed PennyLane plugins to refresh the device list."""

# This function does not return anything; instead, it has a side effect
# which is to update the global plugin_devices variable.

# We wish to retain the behaviour of a global plugin_devices dictionary,
# as re-importing metadata can be a very slow operation on systems
# with a large number of installed packages.
global plugin_devices # pylint:disable=global-statement

reload(metadata)
plugin_devices = _get_device_entrypoints()


# get list of installed devices
plugin_devices = _get_device_entrypoints()


# pylint: disable=protected-access
def device(name, *args, **kwargs):
r"""
Load a device and return the instance.
This function is used to load a particular quantum device,
which can then be used to construct QNodes.
PennyLane comes with support for the following devices:
* :mod:`'default.qubit' <pennylane.devices.default_qubit>`: a simple
state simulator of qubit-based quantum circuit architectures.
* :mod:`'default.mixed' <pennylane.devices.default_mixed>`: a mixed-state
simulator of qubit-based quantum circuit architectures.
* ``'lightning.qubit'``: a more performant state simulator of qubit-based
quantum circuit architectures written in C++.
* :mod:`'default.qutrit' <pennylane.devices.default_qutrit>`: a simple
state simulator of qutrit-based quantum circuit architectures.
* :mod:`'default.qutrit.mixed' <pennylane.devices.default_qutrit_mixed>`: a
mixed-state simulator of qutrit-based quantum circuit architectures.
* :mod:`'default.gaussian' <pennylane.devices.default_gaussian>`: a simple simulator
of Gaussian states and operations on continuous-variable circuit architectures.
* :mod:`'default.clifford' <pennylane.devices.default_clifford>`: an efficient
simulator of Clifford circuits.
* :mod:`'default.tensor' <pennylane.devices.default_tensor>`: a simulator
of quantum circuits based on tensor networks.
Additional devices are supported through plugins — see
the `available plugins <https://pennylane.ai/plugins.html>`_ for more
details. To list all currently installed devices, run
:func:`qml.about <pennylane.about>`.
Args:
name (str): the name of the device to load
wires (int): the number of wires (subsystems) to initialise
the device with. Note that this is optional for certain
devices, such as ``default.qubit``
Keyword Args:
config (pennylane.Configuration): a PennyLane configuration object
that contains global and/or device specific configurations.
custom_decomps (Dict[Union(str, Operator), Callable]): Custom
decompositions to be applied by the device at runtime.
decomp_depth (int): For when custom decompositions are specified,
the maximum expansion depth used by the expansion function.
.. warning::
The ``decomp_depth`` argument is deprecated and will be removed in version 0.39.
All devices must be loaded by specifying their **short-name** as listed above,
followed by the **wires** (subsystems) you wish to initialize. The ``wires``
argument can be an integer, in which case the wires of the device are addressed
by consecutive integers:
.. code-block:: python
dev = qml.device('default.qubit', wires=5)
def circuit():
qml.Hadamard(wires=1)
qml.Hadamard(wires=[0])
qml.CNOT(wires=[3, 4])
...
The ``wires`` argument can also be a sequence of unique numbers or strings, specifying custom wire labels
that the user employs to address the wires:
.. code-block:: python
if name == "plugin_devices":
return pennylane.devices.device_constructor.plugin_devices

dev = qml.device('default.qubit', wires=['ancilla', 'q11', 'q12', -1, 1])
def circuit():
qml.Hadamard(wires='q11')
qml.Hadamard(wires=['ancilla'])
qml.CNOT(wires=['q12', -1])
...
On some newer devices, such as ``default.qubit``, the ``wires`` argument can be omitted altogether,
and instead the wires will be computed when executing a circuit depending on its contents.
>>> dev = qml.device("default.qubit")
Most devices accept a ``shots`` argument which specifies how many circuit executions
are used to estimate stochastic return values. As an example, ``qml.sample()`` measurements
will return as many samples as specified in the shots argument. The shots argument can be
changed on a per-call basis using the built-in ``shots`` keyword argument. Note that the
``shots`` argument can be a single integer or a list of shot values.
.. code-block:: python
dev = qml.device('default.qubit', wires=1, shots=10)
@qml.qnode(dev)
def circuit(a):
qml.RX(a, wires=0)
return qml.sample(qml.Z(0))
>>> circuit(0.8) # 10 samples are returned
array([ 1, 1, 1, 1, -1, 1, 1, -1, 1, 1])
>>> circuit(0.8, shots=[3, 4, 4]) # default is overwritten for this call
(array([1, 1, 1]), array([ 1, -1, 1, 1]), array([1, 1, 1, 1]))
>>> circuit(0.8) # back to default of 10 samples
array([ 1, -1, 1, 1, -1, 1, 1, 1, 1, 1])
When constructing a device, we may optionally pass a dictionary of custom
decompositions to be applied to certain operations upon device execution.
This is useful for enabling support of gates on devices where they would normally
be unsupported.
For example, suppose we are running on an ion trap device which does not
natively implement the CNOT gate, but we would still like to write our
circuits in terms of CNOTs. On a ion trap device, CNOT can be implemented
using the ``IsingXX`` gate. We first define a decomposition function
(such functions have the signature ``decomposition(*params, wires)``):
.. code-block:: python
def ion_trap_cnot(wires, **_):
return [
qml.RY(np.pi/2, wires=wires[0]),
qml.IsingXX(np.pi/2, wires=wires),
qml.RX(-np.pi/2, wires=wires[0]),
qml.RY(-np.pi/2, wires=wires[0]),
qml.RY(-np.pi/2, wires=wires[1])
]
Next, we create a device, and a QNode for testing. When constructing the
QNode, we can set the expansion strategy to ``"device"`` to ensure the
decomposition is applied and will be viewable when we draw the circuit.
Note that custom decompositions should accept keyword arguments even when
it is not used.
.. code-block:: python
# As the CNOT gate normally has no decomposition, we can use default.qubit
# here for expository purposes.
dev = qml.device(
'default.qubit', wires=2, custom_decomps={"CNOT" : ion_trap_cnot}
)
@qml.qnode(dev, expansion_strategy="device")
def run_cnot():
qml.CNOT(wires=[0, 1])
return qml.expval(qml.X(1))
>>> print(qml.draw(run_cnot)())
0: ──RY(1.57)─╭IsingXX(1.57)──RX(-1.57)──RY(-1.57)─┤
1: ───────────╰IsingXX(1.57)──RY(-1.57)────────────┤ <X>
Some devices may accept additional arguments. For instance,
``default.gaussian`` accepts the keyword argument ``hbar``, to set
the convention used in the commutation relation :math:`[\x,\p]=i\hbar`
(by default set to 2).
Please refer to the documentation for the individual devices to see any
additional arguments that might be required or supported.
"""
if name not in plugin_devices:
# Device does not exist in the loaded device list.
# Attempt to refresh the devices, in case the user
# installed the plugin during the current Python session.
refresh_devices()

if name in plugin_devices:
options = {}

# load global configuration settings if available
config = kwargs.get("config", default_config)

if config:
# combine configuration options with keyword arguments.
# Keyword arguments take preference, followed by device options,
# followed by plugin options, followed by global options.
options.update(config["main"])
options.update(config[name.split(".")[0] + ".global"])
options.update(config[name])

# Pop the custom decomposition keyword argument; we will use it here
# only and not pass it to the device.
custom_decomps = kwargs.pop("custom_decomps", None)
decomp_depth = kwargs.pop("decomp_depth", None)

if decomp_depth is not None:
warnings.warn(
"The decomp_depth argument is deprecated and will be removed in version 0.39. ",
PennyLaneDeprecationWarning,
)
else:
decomp_depth = 10

kwargs.pop("config", None)
options.update(kwargs)

# loads the device class
plugin_device_class = plugin_devices[name].load()

if hasattr(plugin_device_class, "pennylane_requires") and Version(
version()
) not in SimpleSpec(plugin_device_class.pennylane_requires):
raise DeviceError(
f"The {name} plugin requires PennyLane versions {plugin_device_class.pennylane_requires}, "
f"however PennyLane version {__version__} is installed."
)

# Construct the device
dev = plugin_device_class(*args, **options)

# Once the device is constructed, we set its custom expansion function if
# any custom decompositions were specified.

if custom_decomps is not None:
if isinstance(dev, pennylane.devices.LegacyDevice):
custom_decomp_expand_fn = pennylane.transforms.create_decomp_expand_fn(
custom_decomps, dev, decomp_depth=decomp_depth
)
dev.custom_expand(custom_decomp_expand_fn)
else:
custom_decomp_preprocess = (
pennylane.transforms.tape_expand._create_decomp_preprocessing(
custom_decomps, dev, decomp_depth=decomp_depth
)
)
dev.preprocess = custom_decomp_preprocess

return dev

raise DeviceError(f"Device {name} does not exist. Make sure the required plugin is installed.")
raise AttributeError(f"module 'pennylane' has no attribute '{name}'")


def version():
Expand Down
11 changes: 11 additions & 0 deletions pennylane/devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ def execute(self, circuits, execution_config = qml.devices.DefaultExecutionConfi
"""

from .execution_config import ExecutionConfig, DefaultExecutionConfig, MCMConfig
from .device_constructor import device, refresh_devices
from .device_api import Device
from .default_qubit import DefaultQubit

Expand All @@ -163,5 +164,15 @@ def execute(self, circuits, execution_config = qml.devices.DefaultExecutionConfi
from .default_clifford import DefaultClifford
from .default_tensor import DefaultTensor
from .null_qubit import NullQubit
from .default_qutrit import DefaultQutrit
from .default_qutrit_mixed import DefaultQutritMixed
from .._device import Device as LegacyDevice
from .._device import DeviceError


# pylint: disable=undefined-variable
def __getattr__(name):
if name == "plugin_devices":
return device_constructor.plugin_devices

raise AttributeError(f"module 'pennylane.devices' has no attribute '{name}'")
Loading

0 comments on commit a456fce

Please sign in to comment.