Source code for pennylane_pq.devices

# Copyright 2018 Xanadu Quantum Technologies Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# pylint: disable=expression-not-assigned
r"""
Devices
=======

.. currentmodule:: pennylane_pq.devices

This plugin offers access to the following ProjectQ backends by providing
corresponding PennyLane devices:

.. autosummary::
   :nosignatures:

   ProjectQSimulator
   ProjectQIBMBackend
   ProjectQClassicalSimulator

See below for a description of the devices and the supported Operations and Observables.

ProjectQSimulator
#################

.. autoclass:: ProjectQSimulator

ProjectQIBMBackend
##################

.. autoclass:: ProjectQIBMBackend

ProjectQClassicalSimulator
##########################

.. autoclass:: ProjectQClassicalSimulator


"""
import numpy as np
import projectq as pq
from projectq.setups.ibm import get_engine_list
from projectq.ops import (
    HGate,
    XGate,
    YGate,
    ZGate,
    SGate,
    TGate,
    SqrtXGate,
    SwapGate,
    Rx,
    Ry,
    Rz,
    R,
    SqrtSwapGate,
)

from pennylane import Device, DeviceError

from .pqops import CNOT, CZ, Rot, QubitUnitary, BasisState

from ._version import __version__


PROJECTQ_OPERATION_MAP = {
    # native PennyLane operations also native to ProjectQ
    "PauliX": XGate,
    "PauliY": YGate,
    "PauliZ": ZGate,
    "CNOT": CNOT,
    "CZ": CZ,
    "SWAP": SwapGate,
    "RX": Rx,
    "RY": Ry,
    "RZ": Rz,
    "PhaseShift": R,
    "Hadamard": HGate,
    # operations not natively implemented in ProjectQ but provided in pqops.py
    "Rot": Rot,
    "QubitUnitary": QubitUnitary,
    "BasisState": BasisState,
    "S": SGate,
    "T": TGate,
    # additional operations not native to PennyLane but present in ProjectQ
    "SqrtX": SqrtXGate,
    "SqrtSwap": SqrtSwapGate,
    # operations/expectations of ProjectQ that do not work with PennyLane
    #'AllPauliZ': AllZGate, #todo: enable when multiple return values are supported
    # operations/expectations of PennyLane that do not work with ProjectQ
    #'QubitStateVector': StatePreparation,
    # In addition we support the Identity Expectation, but only as an expectation and not as an Operation, which is we we don't put it here.
}


class _ProjectQDevice(Device):  # pylint: disable=abstract-method
    """ProjectQ device for PennyLane.

    Args:
      wires (int or Iterable[Number, str]]): Number of subsystems represented by the device,
            or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``)
            or strings (``['ancilla', 'q1', 'q2']``). Default 1 if not specified.
        shots (None, int): How many times the circuit should be evaluated (or sampled) to estimate
            the expectation values. Defaults to ``None`` if not specified, which means that the device
            returns analytical results.

    Keyword Args:
      backend (string): Name of the backend, i.e., either "Simulator",
         "ClassicalSimulator", or "IBMBackend".
      verbose (bool): If True, log messages are printed and exceptions are more verbose.

    Keyword Args for Simulator backend:
      gate_fusion (bool): If True, operations are cached and only executed once a
        certain number of operations has been reached (only has an effect for the c++ simulator).
      rnd_seed (int): Random seed (uses random.randint(0, 4294967295) by default).

    Keyword Args for IBMBackend backend:
      use_hardware (bool): If True, the code is run on the IBM quantum chip (instead of using
        the IBM simulator)
      num_runs (int): Number of runs to collect statistics (default is 1024). Is equivalent
        to but takes preference over the shots parameter.
      user (string): IBM Quantum Experience user name
      password (string): IBM Quantum Experience password
      device (string): Device to use (e.g., ‘ibmqx4’ or ‘ibmqx5’) if use_hardware is set to
        True. Default is ibmqx4.
        retrieve_execution (int): Job ID to retrieve instead of re-running the circuit
        (e.g., if previous run timed out).
    """

    name = "ProjectQ PennyLane plugin"
    short_name = "projectq"
    pennylane_requires = ">=0.15.0"
    version = "0.4.2"
    plugin_version = __version__
    author = "Christian Gogolin and Xanadu"
    _capabilities = {
        "backend": list(["Simulator", "ClassicalSimulator", "IBMBackend"]),
        "model": "qubit",
    }

    @property
    def _operation_map(self):
        raise NotImplementedError

    @property
    def _observable_map(self):
        raise NotImplementedError

    @property
    def _backend_kwargs(self):
        raise NotImplementedError

    def __init__(self, wires=1, shots=None, *, backend, **kwargs):
        # overwrite shots with num_runs if given
        if "num_runs" in kwargs:
            shots = kwargs["num_runs"]
            del kwargs["num_runs"]

        super().__init__(wires=wires, shots=shots)

        if "verbose" not in kwargs:
            kwargs["verbose"] = False

        self._backend = backend
        self._kwargs = kwargs
        self._eng = None
        self._reg = None
        self._first_operation = True
        self.reset()  # the actual initialization is done in reset()

    def reset(self):
        """Reset/initialize the device by allocating qubits."""
        self._reg = self._eng.allocate_qureg(self.num_wires)
        self._first_operation = True

    def __repr__(self):
        return super().__repr__() + "Backend: " + self._backend + "\n"

    def __str__(self):
        return super().__str__() + "Backend: " + self._backend + "\n"

    def post_measure(self):
        """Deallocate the qubits after expectation values have been retrieved."""
        self._deallocate()

    def apply(self, operation, wires, par):
        """Apply a quantum operation.

        For plugin developers: this function should apply the operation on the device.

        Args:
            operation (str): name of the operation
            wires (Sequence[int]): subsystems the operation is applied on
            par (tuple): parameters for the operation
        """
        operation = self._operation_map[operation](*par)
        if isinstance(operation, BasisState) and not self._first_operation:
            raise DeviceError(
                "Operation {} cannot be used after other Operations have already "
                "been applied on a {} device.".format(operation, self.short_name)
            )
        self._first_operation = False

        # translate wires to reflect labels on the device
        device_wires = self.map_wires(wires)

        qureg = [self._reg[i] for i in device_wires.labels]
        if isinstance(
            operation,
            (
                pq.ops._metagates.ControlledGate,  # pylint: disable=protected-access
                pq.ops._gates.SqrtSwapGate,  # pylint: disable=protected-access
                pq.ops._gates.SwapGate,  # pylint: disable=protected-access
            ),
        ):  # pylint: disable=protected-access
            qureg = tuple(qureg)
        operation | qureg  # pylint: disable=pointless-statement

    def _deallocate(self):
        """Deallocate all qubits to make ProjectQ happy

        See also: https://github.com/ProjectQ-Framework/ProjectQ/issues/2

        Drawback: This is probably rather resource intensive.
        """
        if self._eng is not None and self._backend == "Simulator":
            # avoid an "unfriendly error message":
            # https://github.com/ProjectQ-Framework/ProjectQ/issues/2
            pq.ops.All(pq.ops.Measure) | self._reg  # pylint: disable=expression-not-assigned

    def filter_kwargs_for_backend(self, kwargs):
        """Filter the given kwargs for those relevant for the respective device/backend."""
        return {key: value for key, value in kwargs.items() if key in self._backend_kwargs}

    @property
    def operations(self):
        """Get the supported set of operations.

        Returns:
            set[str]: the set of PennyLane operation names the device supports
        """
        return set(self._operation_map.keys())

    @property
    def observables(self):
        """Get the supported set of observables.

        Returns:
            set[str]: the set of PennyLane observable names the device supports
        """
        return set(self._observable_map.keys())


[docs]class ProjectQSimulator(_ProjectQDevice): """A PennyLane :code:`projectq.simulator` device for the `ProjectQ Simulator <https://projectq.readthedocs.io/en/latest/projectq.backends.html#projectq.backends.Simulator>`_ backend. Args: wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) or strings (``['ancilla', 'q1', 'q2']``). shots (None, int): How many times the circuit should be evaluated (or sampled) to estimate the expectation values. Defaults to ``None`` if not specified, which means that the device returns analytical results. Keyword Args: gate_fusion (bool): If True, operations are cached and only executed once a certain number of operations has been reached (only has an effect for the c++ simulator). rnd_seed (int): Random seed (uses random.randint(0, 4294967295) by default). verbose (bool): If True, log messages are printed and exceptions are more verbose. This device can, for example, be instantiated from PennyLane as follows: .. code-block:: python import pennylane as qml dev = qml.device('projectq.simulator', wires=XXX) Supported PennyLane Operations: :class:`pennylane.PauliX`, :class:`pennylane.PauliY`, :class:`pennylane.PauliZ`, :class:`pennylane.CNOT`, :class:`pennylane.CZ`, :class:`pennylane.SWAP`, :class:`pennylane.RX`, :class:`pennylane.RY`, :class:`pennylane.RZ`, :class:`pennylane.PhaseShift`, :class:`pennylane.Hadamard`, :class:`pennylane.Rot`, :class:`pennylane.QubitUnitary`, :class:`pennylane.BasisState`, :class:`pennylane_pq.S <pennylane_pq.ops.S>`, :class:`pennylane_pq.T <pennylane_pq.ops.T>`, Supported PennyLane observables: :class:`pennylane.PauliX`, :class:`pennylane.PauliY`, :class:`pennylane.PauliZ`, :class:`pennylane.Hadamard`, :class:`pennylane.Identity` Extra Operations: :class:`pennylane_pq.SqrtX <pennylane_pq.ops.SqrtX>`, :class:`pennylane_pq.SqrtSwap <pennylane_pq.ops.SqrtSwap>` """ short_name = "projectq.simulator" _operation_map = PROJECTQ_OPERATION_MAP _observable_map = dict( {key: val for key, val in _operation_map.items() if val in [XGate, YGate, ZGate, HGate]}, **{"Identity": None} ) _circuits = {} _backend_kwargs = ["gate_fusion", "rnd_seed"] def __init__(self, wires=1, shots=None, **kwargs): kwargs["backend"] = "Simulator" super().__init__(wires=wires, shots=shots, **kwargs)
[docs] def reset(self): """Reset/initialize the device by initializing the backend and engine, and allocating qubits.""" backend = pq.backends.Simulator(**self.filter_kwargs_for_backend(self._kwargs)) self._eng = pq.MainEngine(backend, verbose=self._kwargs["verbose"]) super().reset()
[docs] def pre_measure(self): """Flush the device before retrieving observable measurements.""" self._eng.flush(deallocate_qubits=False)
[docs] def expval(self, observable, wires, par): """Retrieve the requested observable expectation value.""" device_wires = self.map_wires(wires) if observable in ["PauliX", "PauliY", "PauliZ"]: expval = self._eng.backend.get_expectation_value( pq.ops.QubitOperator(str(observable)[-1] + "0"), [self._reg[device_wires.labels[0]]] ) elif observable == "Hadamard": expval = self._eng.backend.get_expectation_value( 1 / np.sqrt(2) * pq.ops.QubitOperator("X0") + 1 / np.sqrt(2) * pq.ops.QubitOperator("Z0"), [self._reg[device_wires.labels[0]]], ) elif observable == "Identity": expval = 1 # elif observable == 'AllPauliZ': # expval = [self._eng.backend.get_expectation_value( # pq.ops.QubitOperator("Z"+'0'), [qubit]) # for qubit in self._reg] if not self.shots is None and observable != "Identity": p0 = (expval + 1) / 2 p0 = max(min(p0, 1), 0) n0 = np.random.binomial(self.shots, p0) expval = (n0 - (self.shots - n0)) / self.shots return expval
[docs] def var(self, observable, wires, par): """Retrieve the requested observable variance.""" expval = self.expval(observable, wires, par) variance = 1 - expval**2 # TODO: if this plugin supports non-involutory observables in future, may need to refactor this function return variance
[docs]class ProjectQIBMBackend(_ProjectQDevice): """A PennyLane :code:`projectq.ibm` device for the `ProjectQ IBMBackend <https://projectq.readthedocs.io/en/latest/projectq.backends.html#projectq.backends.IBMBackend>`_ backend. .. note:: This device computes expectation values by averaging over a finite number of runs of the quantum circuit. Irrespective of whether this is done on real quantum hardware, or on the IBM simulator, this means that expectation values (and therefore also gradients) will have a finite accuracy and fluctuate from run to run. Args: wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) or strings (``['ancilla', 'q1', 'q2']``). shots (int): number of circuit evaluations used to estimate expectation values of observables. Default value is 1024. Keyword Args: use_hardware (bool): If True, the code is run on the IBM quantum chip (instead of using the IBM simulator) num_runs (int): Number of runs to collect statistics (default is 1024). Is equivalent to but takes preference over the shots parameter. token (string): IBM Quantum Experience API token device (string): IBMQ backend to use (ibmq_16_melbourne’, ‘ibmqx2’, 'ibmq_rome' 'ibmq_qasm_simulator') if :code:`use_hardware` is set to True. Default is '‘ibmqx5’'. retrieve_execution (int): Job ID to retrieve instead of re-running a circuit (e.g., if previous run timed out). verbose (bool): If True, log messages are printed and exceptions are more verbose. This device can, for example, be instantiated from PennyLane as follows: .. code-block:: python import pennylane as qml dev = qml.device('projectq.ibm', wires=XXX, token="XXX") To avoid leaking your user name and password when sharing code, it is better to specify the user name and password in your `PennyLane configuration file <https://pennylane.readthedocs.io/configuration.html>`_. Supported PennyLane Operations: :class:`pennylane.PauliX`, :class:`pennylane.PauliY`, :class:`pennylane.PauliZ`, :class:`pennylane.CNOT`, :class:`pennylane.CZ`, :class:`pennylane.SWAP`, :class:`pennylane.RX`, :class:`pennylane.RY`, :class:`pennylane.RZ`, :class:`pennylane.PhaseShift`, :class:`pennylane.Hadamard`, :class:`pennylane.Rot`, :class:`pennylane.BasisState` Supported PennyLane observables: :class:`pennylane.PauliX`, :class:`pennylane.PauliY`, :class:`pennylane.PauliZ`, :class:`pennylane.Hadamard`, :class:`pennylane.Identity` .. note:: The observables :class:`pennylane.PauliY`, :class:`pennylane.PauliZ`, and :class:`pennylane.Hadamard`, cannot be natively measured on the hardware device. They are implemented by executing a few additional gates on the respective wire before the final measurement, which is always performed in the :class:`pennylane.PauliZ` basis. These measurements may thus be slightly more noisy than native :class:`pennylane.PauliZ` measurement. Extra Operations: :class:`pennylane_pq.S <pennylane_pq.ops.S>`, :class:`pennylane_pq.T <pennylane_pq.ops.T>`, :class:`pennylane_pq.SqrtX <pennylane_pq.ops.SqrtX>`, :class:`pennylane_pq.SqrtSwap <pennylane_pq.ops.SqrtSwap>`, """ short_name = "projectq.ibm" _operation_map = { key: val for key, val in PROJECTQ_OPERATION_MAP.items() if val in [ HGate, XGate, YGate, ZGate, SGate, TGate, SqrtXGate, SwapGate, SqrtSwapGate, Rx, Ry, Rz, R, CNOT, CZ, Rot, BasisState, ] } _observable_map = dict( {key: val for key, val in _operation_map.items() if val in [HGate, XGate, YGate, ZGate]}, **{"Identity": None} ) _circuits = {} _backend_kwargs = [ "use_hardware", "num_runs", "verbose", "token", "device", "retrieve_execution", ] def __init__(self, wires=1, shots=1024, **kwargs): # check that necessary arguments are given if "token" not in kwargs: raise ValueError( 'An IBM Quantum Experience token specified via the "token" keyword argument is required' ) # pylint: disable=line-too-long kwargs["backend"] = "IBMBackend" super().__init__(wires=wires, shots=shots, **kwargs)
[docs] def reset(self): """Reset/initialize the device by initializing the backend and engine, and allocating qubits.""" backend = pq.backends.IBMBackend( num_runs=self.shots, **self.filter_kwargs_for_backend(self._kwargs) ) token = self._kwargs.get("token", "") hw = self._kwargs.get("use_hardware", False) device = self._kwargs.get("device", "ibmq_qasm_simulator" if not hw else "ibmqx2") self._eng = pq.MainEngine( backend, verbose=self._kwargs["verbose"], engine_list=get_engine_list(token=token, device=device), ) super().reset()
[docs] def pre_measure(self): """Rotate qubits to the right basis before measurement, apply a measure all operation and flush the device before retrieving expectation values. """ if hasattr( self, "obs_queue" ): # we raise an except below in case there is no obs_queue but we are asked to measure in a basis different from PauliZ for obs in self.obs_queue: if obs.name == "PauliX": self.apply("Hadamard", obs.wires, list()) elif obs.name == "PauliY": self.apply("PauliZ", obs.wires, list()) self.apply("S", obs.wires, list()) self.apply("Hadamard", obs.wires, list()) elif obs.name == "Hadamard": self.apply("RY", obs.wires, [-np.pi / 4]) elif obs.name == "Hermitian": raise NotImplementedError pq.ops.All(pq.ops.Measure) | self._reg # pylint: disable=expression-not-assigned self._eng.flush()
[docs] def expval(self, observable, wires, par): """Retrieve the requested observable expectation value.""" device_wires = self.map_wires(wires) probabilities = self._eng.backend.get_probabilities(self._reg) if observable in ["PauliX", "PauliY", "PauliZ", "Hadamard"]: if observable != "PauliZ" and not hasattr(self, "obs_queue"): raise DeviceError( "Measurements in basis other than PauliZ are only supported when " "this plugin is used with versions of PennyLane that expose the obs_queue. " "Please update PennyLane and this plugin." ) expval = ( 1 - ( 2 * sum( p for (state, p) in probabilities.items() if state[device_wires.labels[0]] == "1" ) ) - ( 1 - 2 * sum( p for (state, p) in probabilities.items() if state[device_wires.labels[0]] == "0" ) ) ) / 2 elif observable == "Hermitian": raise NotImplementedError elif observable == "Identity": expval = sum(p for (state, p) in probabilities.items()) # elif observable == 'AllPauliZ': # expval = [((1-2*sum(p for (state, p) in probabilities.items() # if state[i] == '1')) # -(1-2*sum(p for (state, p) in probabilities.items() # if state[i] == '0')))/2 for i in range(len(self._reg))] return expval
[docs] def var(self, observable, wires, par): """Retrieve the requested observable variance.""" expval = self.expval(observable, wires, par) variance = 1 - expval**2 # TODO: if this plugin supports non-involutory observables in future, may need to refactor this function return variance
[docs]class ProjectQClassicalSimulator(_ProjectQDevice): """A PennyLane :code:`projectq.classical` device for the `ProjectQ ClassicalSimulator <https://projectq.readthedocs.io/en/latest/projectq.backends.html#projectq.backends.ClassicalSimulator>`_ backend. Args: wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) or strings (``['ancilla', 'q1', 'q2']``). Keyword Args: verbose (bool): If True, log messages are printed and exceptions are more verbose. This device can, for example, be instantiated from PennyLane as follows: .. code-block:: python import pennylane as qml dev = qml.device('projectq.classical', wires=XXX) Supported PennyLane Operations: :class:`pennylane.PauliX`, :class:`pennylane.CNOT`, :class:`pennylane.BasisState` Supported PennyLane observables: :class:`pennylane.PauliZ`, :class:`pennylane.Identity` """ short_name = "projectq.classical" _operation_map = { key: val for key, val in PROJECTQ_OPERATION_MAP.items() if val in [XGate, CNOT, BasisState] } _observable_map = dict( {key: val for key, val in PROJECTQ_OPERATION_MAP.items() if val in [ZGate]}, **{"Identity": None} ) _circuits = {} _backend_kwargs = [] def __init__(self, wires=1, **kwargs): kwargs["backend"] = "ClassicalSimulator" super().__init__(wires=wires, shots=None, **kwargs)
[docs] def reset(self): """Reset/initialize the device by initializing the backend and engine, and allocating qubits.""" backend = pq.backends.ClassicalSimulator(**self.filter_kwargs_for_backend(self._kwargs)) self._eng = pq.MainEngine(backend, verbose=self._kwargs["verbose"]) super().reset()
[docs] def pre_measure(self): """Apply a measure all operation and flush the device before retrieving observable measurements.""" pq.ops.All(pq.ops.Measure) | self._reg # pylint: disable=expression-not-assigned self._eng.flush()
[docs] def expval(self, observable, wires, par): """Retrieve the requested observable expectation values.""" device_wires = self.map_wires(wires) if observable == "PauliZ": wire = device_wires.labels[0] expval = 1 - 2 * int(self._reg[wire]) elif observable == "Identity": expval = 1 # elif observable == 'AllPauliZ': # expval = [ 1 - 2*int(self._reg[wire]) for wire in self._reg] return expval
[docs] def var(self, observable, wires, par): """Retrieve the requested observable variance.""" expval = self.expval(observable, wires, par) variance = 1 - expval**2 # TODO: if this plugin supports non-involutory observables in future, may need to refactor this function return variance