# Copyright 2020-2024 Quantinuum
#
# 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.
from typing import cast, Any, Dict, Iterable, List, Optional, Union
import numpy as np
from pennylane import QubitDevice # type: ignore
from pennylane.operation import Operation # type: ignore
from pytket.backends.backend import Backend
from pytket.backends.backendresult import BackendResult
from pytket.passes import BasePass
from pytket.extensions.qiskit import AerStateBackend
from pytket.extensions.pennylane import __extension_version__
from pytket.circuit import OpType, Circuit
from .pennylane_convert import (
OPERATION_MAP,
pennylane_to_tk,
)
[docs]
class PytketDevice(QubitDevice):
"""PytketDevice allows pytket backends and compilation to be used as Pennylane
devices."""
name = "pytket-pennylane plugin"
short_name = "pytket.pytketdevice"
pennylane_requires = ">=0.35.0"
version = "0.15.0"
plugin_version = __extension_version__
author = "KN"
_operation_map = OPERATION_MAP
operations = set(_operation_map.keys())
observables = {"PauliX", "PauliY", "PauliZ", "Identity", "Hadamard", "Prod"}
[docs]
def __init__(
self,
wires: int,
shots: Optional[int] = None,
pytket_backend: Backend = AerStateBackend(),
optimisation_level: Optional[int] = None,
compilation_pass: Optional[BasePass] = None,
):
"""Construct a device that use a Pytket Backend and compilation to
execute circuits.
:param wires: Number of wires
:type wires: int
:param shots: Number of shots to use (only relevant for sampling backends),
defaults to None
:type shots: Optional[int], optional
:param pytket_backend: Pytket Backend class to use, defaults to
AerStateBackend() to facilitate automated pennylane testing of this backend
:type pytket_backend: Backend, optional
:param optimisation_level: Backend default compilation optimisation level,
ignored if `compilation_pass` is set, defaults to None
:type optimisation_level: int, optional
:param compilation_pass: Pytket compiler pass with which to compile circuits,
defaults to None
:type compilation_pass: Optional[BasePass], optional
:raises ValueError: If the Backend does not support shots or state results
"""
if not (pytket_backend.supports_shots or pytket_backend.supports_state):
raise ValueError("pytket Backend must support shots or state.")
self.pytket_backend = pytket_backend
if compilation_pass is None:
if optimisation_level is None:
self.compilation_pass = self.pytket_backend.default_compilation_pass()
else:
self.compilation_pass = self.pytket_backend.default_compilation_pass(
optimisation_level
)
else:
self.compilation_pass = compilation_pass
super().__init__(wires=wires, shots=shots)
[docs]
def capabilities(self) -> Dict[str, Any]:
cap_dic: Dict[str, Any] = super().capabilities().copy()
cap_dic.update(
{
"supports_finite_shots": self.pytket_backend.supports_shots,
"returns_state": self.pytket_backend.supports_state,
"supports_inverse_operations": True,
}
)
return cap_dic
[docs]
def reset(self) -> None:
# Reset only internal data, not the options that are determined on
# device creation
self._circuit = Circuit(name="temp")
self._reg = self._circuit.add_q_register("q", self.num_wires)
self._creg = self._circuit.add_c_register("c", self.num_wires)
self._backres: Optional[BackendResult] = None
self._state: Optional[np.ndarray] = None # statevector of a simulator backend
self._samples: Optional[np.ndarray] = None
super().reset()
[docs]
def apply(
self, operations: List[Operation], rotations: Optional[List[Operation]] = None
) -> None:
self._circuit = pennylane_to_tk(
operations if rotations is None else operations + rotations,
self._wire_map,
self._reg,
self._creg,
measure=(not self.pytket_backend.supports_state),
)
# These operations need to run for all devices
compiled_c = self.compile(self._circuit)
self.run(compiled_c)
def compile(self, circuit: Circuit) -> Circuit:
compile_c = circuit.copy()
self.compilation_pass.apply(compile_c)
return compile_c
[docs]
def run(self, compiled_c: Circuit) -> None:
"""Run the compiled circuit, and query the result."""
shots = None
if self.pytket_backend.supports_shots:
shots = self.shots
if compiled_c.n_gates_of_type(OpType.Measure) == 0:
compiled_c.measure_all()
handle = self.pytket_backend.process_circuit(compiled_c, n_shots=shots)
self._backres = self.pytket_backend.get_result(handle)
[docs]
def analytic_probability(
self, wires: Optional[Union[int, Iterable[int]]] = None
) -> np.ndarray:
prob = self.marginal_prob(np.abs(self.state) ** 2, wires)
return cast(np.ndarray, prob)
[docs]
def generate_samples(self) -> np.ndarray:
if self.pytket_backend.supports_shots:
if self._backres is None:
raise RuntimeError("Result does not exist.")
self._samples = np.asarray(
self._backres.get_shots(self._creg.to_list()), dtype=int
)
return self._samples
else:
return cast(np.ndarray, super().generate_samples())
@property
def state(self) -> np.ndarray:
if self.pytket_backend.supports_state:
if self._state is None:
if self._backres is None:
raise RuntimeError("Result does not exist.")
self._state = self._backres.get_state(self._reg.to_list())
return self._state
raise AttributeError("Device does not support state.")