Source code for tno.quantum.devices.azure

"""This module contains functionality related to the Azure quantum provider.

The :py:class:`AzureDevice` class defines a PennyLane device (called ``azure.remote``)
which can be used execute circuits on the Azure backends.
The following example shows how this can be done.

Example:
-------

.. code-block:: python

    # To use any of the Azure backends, make sure your connection string is set.
    # This method sets the connection string in the environment variables, so it needs
    # to be set at the start of every session.
    from tno.quantum.devices import azure

    azure.set_connection_string(
        connection_string="...",
    )

    # An Azure device can be constructed in two ways.
    # (1) Either directly using `qml.device`
    import pennylane as qml

    dev = qml.device("azure.remote", backend_name="quantinuum.sim.h2-1e", wires=2)

    # (2) Using the `DeviceConfig` class.
    from tno.quantum.devices import DeviceConfig

    config = DeviceConfig(
        "azure.remote",
        options={"backend_name": "quantinuum.sim.h2-1e", "wires": 2},
    )
    dev = config.get_instance()

    # Now, we can create and execute a quantum circuit on the defined device.
    @qml.qnode(dev, shots=100)
    def circuit():
        qml.Hadamard(0)
        qml.CNOT([0, 1])
        return qml.sample()

    samples = circuit()
"""

from __future__ import annotations

import importlib.util
import os
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any, cast

from pennylane_qiskit.remote import RemoteDevice

if TYPE_CHECKING:
    from pennylane.devices import ExecutionConfig
    from pennylane.tape import QuantumScriptOrBatch
    from pennylane.typing import Result, ResultBatch
    from pennylane.wires import WiresLike
    from qiskit import QuantumCircuit
    from qiskit.circuit import Instruction


def _is_azure_installed() -> bool:
    return importlib.util.find_spec("azure") is not None


if _is_azure_installed():
    from azure.quantum import Workspace
    from azure.quantum.qiskit import AzureQuantumProvider
    from azure.quantum.qiskit.backends.backend import AzureBackendConfig

    if TYPE_CHECKING:
        from azure.quantum.qiskit.backends import AzureBackendBase

    # Ad-hoc fix for `azure-quantum` bug

[docs] def azure_backend_config_getattr_fixed( # noqa: D103 self: AzureBackendConfig, name: str ) -> Any: if name == "max_experiments": return 1 try: return self.__dict__[name] except KeyError as exc: if name == "metadata": msg = "AzureBackendConfig has no property `metadata`" raise AttributeError(msg) from None if name in self.metadata: return self.metadata[name] msg = f"'{type(self).__name__}' object has no attribute '{name}'" raise AttributeError(msg) from exc
AzureBackendConfig.__getattr__ = azure_backend_config_getattr_fixed _AZURE_SERVICE: AzureQuantumProvider | None = None _AZURE_QUANTUM_CONNECTION_STRING = "AZURE_QUANTUM_CONNECTION_STRING" _AZURE_NOT_INSTALLED_MSG = ( "because Azure quantum packages are not installed. " "These can be installed via `tno.quantum.devices[azure]`." )
[docs] def backends() -> list[str]: """Returns a list of names of all available Azure backends.""" global _AZURE_SERVICE # noqa: PLW0602 connect() return [backend.name for backend in _AZURE_SERVICE.backends()] # type: ignore[union-attr]
[docs] def backend(name: str) -> AzureBackendBase: """Get Azure backend by name. Args: name: Name of the Azure backend. """ global _AZURE_SERVICE # noqa: PLW0602 connect() return cast("AzureBackendBase", _AZURE_SERVICE.get_backend(name)) # type: ignore[union-attr]
[docs] def connect() -> None: """Connect to the Azure service. Before calling this method, a connection string must be set as an environment variable, e.g. using :py:meth:`set_connection_string`. Otherwise, an exception will be raised. Raises: RuntimeError - If no connection string is set. """ global _AZURE_SERVICE # noqa: PLW0603 if not _is_azure_installed(): msg = "Could not connect to Azure service, " + _AZURE_NOT_INSTALLED_MSG raise RuntimeError(msg) if _AZURE_SERVICE is None: connection_string = os.environ.get(_AZURE_QUANTUM_CONNECTION_STRING, None) if connection_string is None: msg = ( "No connection string found. " "Use `tno.quantum.devices.azure.set_connection_string` " "to set the connection string." ) raise RuntimeError(msg) workspace = Workspace.from_connection_string(connection_string) _AZURE_SERVICE = AzureQuantumProvider(workspace)
[docs] def set_connection_string( connection_string: str, ) -> None: """Set the ``AZURE_QUANTUM_CONNECTION_STRING`` environment variable. Args: connection_string: Azure connection string. """ os.environ[_AZURE_QUANTUM_CONNECTION_STRING] = connection_string
[docs] class AzureDevice(RemoteDevice): # type: ignore[misc] """PennyLane device for Azure backends. This device can be constructed using :py:obj:`qml.device("azure.remote", ...)`. """
[docs] def __init__(self, *, backend_name: str, wires: WiresLike, **kwargs: Any) -> None: """Initialize Azure device. Args: backend_name: Name of the Azure backend. wires: Wires. kwargs: Additional keyword arguments to be passed to the underlying :py:class:`~pennylane_qiskit.remote.RemoteDevice`. """ if not _is_azure_installed(): msg = ( "Failed to instantiate device 'azure.remote', " + _AZURE_NOT_INSTALLED_MSG ) raise RuntimeError(msg) self._backend_name = backend_name self._backend = backend(backend_name) super().__init__(backend=self._backend, wires=wires, **kwargs)
[docs] def execute( self, circuits: QuantumScriptOrBatch, execution_config: ExecutionConfig | None = None, ) -> Result | ResultBatch: """Execute circuits with execution config.""" return super().execute(circuits, execution_config)
[docs] def compile_circuits(self, circuits: list[QuantumCircuit]) -> list[QuantumCircuit]: """Compiles multiple circuits one after the other. Args: circuits: List of circuits to compile. Returns: List of compiled circuits """ circuits = cast("list[QuantumCircuit]", super().compile_circuits(circuits)) for circuit in circuits: self._remove_barriers(circuit) return circuits
def _remove_barriers(self, circuit: QuantumCircuit) -> None: circuit.data = [ instruction for instruction in cast("Iterable[Any]", circuit.data) if cast("Instruction", instruction.operation).name != "barrier" ]