Source code for pennylane.devices.default_qutrit
# Copyright 2018-2022 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.
r"""
The default.qutrit device is PennyLane's standard qutrit-based device.
It implements the :class:`~pennylane.devices._legacy_device.Device` methods as well as some built-in
:mod:`qutrit operations <pennylane.ops.qutrit>`, and provides simple pure state
simulation of qutrit-based quantum computing.
"""
import functools
import logging
import numpy as np
import pennylane as qml # pylint: disable=unused-import
from pennylane.logging import debug_logger, debug_logger_init
from pennylane.wires import WireError
from .._version import __version__
from ._qutrit_device import QutritDevice
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
# tolerance for numerical errors
tolerance = 1e-10
OMEGA = qml.math.exp(2 * np.pi * 1j / 3)
def _get_slice(index, axis, num_axes):
"""Allows slicing along an arbitrary axis of an array or tensor.
Args:
index (int): the index to access
axis (int): the axis to slice into
num_axes (int): total number of axes
Returns:
tuple[slice or int]: a tuple that can be used to slice into an array or tensor
**Example:**
Accessing the 2 index along axis 1 of a 3-axis array:
>>> sl = _get_slice(2, 1, 3)
>>> sl
(slice(None, None, None), 2, slice(None, None, None))
>>> a = np.arange(27).reshape((3, 3, 3))
>>> a[sl]
array([[ 6, 7, 8],
[15, 16, 17],
[24, 25, 26]])
"""
idx = [slice(None)] * num_axes
idx[axis] = index
return tuple(idx)
# pylint: disable=too-many-arguments
[docs]class DefaultQutrit(QutritDevice):
"""Default qutrit device for PennyLane.
.. warning::
The API of ``DefaultQutrit`` will be updated soon to follow a new device interface described
in :class:`pennylane.devices.Device`.
This change will not alter device behaviour for most workflows, but may have implications for
plugin developers and users who directly interact with device methods. Please consult
:class:`pennylane.devices.Device` and the implementation in
:class:`pennylane.devices.DefaultQubit` for more information on what the new
interface will look like and be prepared to make updates in a coming release. If you have any
feedback on these changes, please create an
`issue <https://github.com/PennyLaneAI/pennylane/issues>`_ or post in our
`discussion forum <https://discuss.pennylane.ai/>`_.
Args:
wires (int, 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.
"""
name = "Default qutrit PennyLane plugin"
short_name = "default.qutrit"
pennylane_requires = __version__
version = __version__
author = "Mudit Pandey, UBC Quantum Software and Algorithms Research Group, and Xanadu"
# TODO: Update list of operations and observables once more are added
operations = {
"Identity",
"QutritUnitary",
"ControlledQutritUnitary",
"TShift",
"Adjoint(TShift)",
"TClock",
"Adjoint(TClock)",
"TAdd",
"Adjoint(TAdd)",
"TSWAP",
"THadamard",
"Adjoint(THadamard)",
"TRX",
"TRY",
"TRZ",
"QutritBasisState",
}
# Identity is supported as an observable for qml.state() to work correctly. However, any
# measurement types that rely on eigenvalue decomposition will not work with qml.Identity
observables = {"THermitian", "GellMann", "Identity", "Prod"}
# Static methods to use qml.math to allow for backprop differentiation
_reshape = staticmethod(qml.math.reshape)
_flatten = staticmethod(qml.math.flatten)
_transpose = staticmethod(qml.math.transpose)
_dot = staticmethod(qml.math.dot)
_stack = staticmethod(qml.math.stack)
_conj = staticmethod(qml.math.conj)
_roll = staticmethod(qml.math.roll)
_cast = staticmethod(qml.math.cast)
_tensordot = staticmethod(qml.math.tensordot)
_real = staticmethod(qml.math.real)
_imag = staticmethod(qml.math.imag)
@staticmethod
def _reduce_sum(array, axes):
return qml.math.sum(array, tuple(axes))
@staticmethod
def _asarray(array, dtype=None):
# Support float
if not hasattr(array, "__len__"):
return np.asarray(array, dtype=dtype)
res = qml.math.cast(array, dtype=dtype)
return res
@debug_logger_init
def __init__(
self,
wires,
*,
r_dtype=np.float64,
c_dtype=np.complex128,
shots=None,
analytic=None,
):
super().__init__(wires, shots, r_dtype=r_dtype, c_dtype=c_dtype, analytic=analytic)
# TODO: add support for snapshots
# self._debugger = None
# Create the initial state. Internally, we store the
# state as an array of dimension [3]*wires.
self._state = self._create_basis_state(0)
self._pre_rotated_state = self._state
# TODO: Add operations
self._apply_ops = {
# All operations that can be applied on the `default.qutrit` device by directly
# manipulating the internal state array will be included in this dictionary
"TShift": self._apply_tshift,
"TClock": self._apply_tclock,
"TAdd": self._apply_tadd,
"TSWAP": self._apply_tswap,
}
[docs] @functools.lru_cache()
def map_wires(self, wires):
# temporarily overwrite this method to bypass
# wire map that produces Wires objects
try:
mapped_wires = [self.wire_map[w] for w in wires]
except KeyError as e:
raise WireError(
f"Did not find some of the wires {wires.labels} on device with wires {self.wires.labels}."
) from e
return mapped_wires
[docs] def define_wire_map(self, wires):
# temporarily overwrite this method to bypass
# wire map that produces Wires objects
consecutive_wires = range(self.num_wires)
wire_map = zip(wires, consecutive_wires)
return dict(wire_map)
[docs] @debug_logger
def apply(self, operations, rotations=None, **kwargs): # pylint: disable=arguments-differ
rotations = rotations or []
# apply the circuit operations
# Operations are enumerated so that the order of operations can eventually be used
# for correctly applying basis state / state vector / snapshot operations which will
# be added later.
for i, operation in enumerate(operations): # pylint: disable=unused-variable
if i > 0 and isinstance(operation, qml.QutritBasisState):
raise qml.DeviceError(
f"Operation {operation.name} cannot be used after other operations have already been applied "
f"on a {self.short_name} device."
)
if isinstance(operation, qml.QutritBasisState):
self._apply_basis_state(operation.parameters[0], operation.wires)
else:
self._state = self._apply_operation(self._state, operation)
# store the pre-rotated state
self._pre_rotated_state = self._state
# apply the circuit rotations
for operation in rotations:
self._state = self._apply_operation(self._state, operation)
def _apply_basis_state(self, state, wires):
"""Initialize the state vector in a specified computational basis state.
Args:
state (array[int]): computational basis state of shape ``(wires,)``
consisting of 0s, 1s and 2s.
wires (Wires): wires that the provided computational state should be initialized on
Note: This function does not support broadcasted inputs yet.
"""
# translate to wire labels used by device
device_wires = self.map_wires(wires)
# length of basis state parameter
n_basis_state = len(state)
if not set(state.tolist()).issubset({0, 1, 2}):
raise ValueError("QutritBasisState parameter must consist of 0, 1 or 2 integers.")
if n_basis_state != len(device_wires):
raise ValueError("QutritBasisState parameter and wires must be of equal length.")
# get computational basis state number
basis_states = 3 ** (self.num_wires - 1 - np.array(device_wires))
basis_states = qml.math.convert_like(basis_states, state)
num = int(qml.math.dot(state, basis_states))
self._state = self._create_basis_state(num)
def _apply_operation(self, state, operation):
"""Applies operations to the input state.
Args:
state (array[complex]): input state
operation (~.Operation): operation to apply on the device
Returns:
array[complex]: output state
"""
if operation.name == "Identity":
return state
wires = operation.wires
if operation.name in self._apply_ops: # pylint: disable=no-else-return
axes = self.wires.indices(wires)
return self._apply_ops[operation.name](state, axes)
elif (
isinstance(operation, qml.ops.Adjoint) # pylint: disable=no-member
and operation.base.name in self._apply_ops
):
axes = self.wires.indices(wires)
return self._apply_ops[operation.base.name](state, axes, inverse=True)
matrix = self._asarray(self._get_unitary_matrix(operation), dtype=self.C_DTYPE)
return self._apply_unitary(state, matrix, wires)
def _apply_tshift(self, state, axes, inverse=False):
"""Applies a ternary Shift gate by rolling 1 unit along the axis specified in ``axes``.
Rolling by 1 unit along the axis means that the :math:`|0 \rangle` state with index ``0`` is
shifted to the :math:`|1 \rangle` state with index ``1``. Likewise, since rolling beyond
the last index loops back to the first, :math:`|2 \rangle` is transformed to
:math:`|0 \rangle`.
Args:
state (array[complex]): input state
axes (List[int]): target axes to apply transformation
inverse (bool): whether to apply the inverse operation
Returns:
array[complex]: output state
"""
shift = -1 if inverse else 1
return self._roll(state, shift, axes[0])
def _apply_tclock(self, state, axes, inverse=False):
"""Applies a ternary Clock gate by adding appropriate phases to the 1 and 2 indices
along the axis specified in ``axes``
Args:
state (array[complex]): input state
axes (List[int]): target axes to apply transformation
inverse (bool): whether to apply the inverse operation
Returns:
array[complex]: output state
"""
partial_state = self._apply_phase(state, axes, 1, OMEGA, inverse)
return self._apply_phase(partial_state, axes, 2, OMEGA**2, inverse)
def _apply_tadd(self, state, axes, inverse=False):
"""Applies a controlled ternary add gate by slicing along the first axis specified in ``axes`` and
applying a TShift transformation along the second axis. The ternary add gate acts on the computational
basis states like :math:`\text{TAdd}\vert i, j\rangle \rightarrow \vert i, i+j \rangle`, where addition
is taken modulo 3.
By slicing along the first axis, we are able to select all of the amplitudes with corresponding
:math:`|1\rangle` and :math:`|2\rangle` for the control qutrit. This means we just need to apply
a :class:`~.TShift` gate when slicing along index 1, and a :class:`~.TShift` adjoint gate when
slicing along index 2
Args:
state (array[complex]): input state
axes (List[int]): target axes to apply transformation
Returns:
array[complex]: output state
"""
slices = [_get_slice(i, axes[0], self.num_wires) for i in range(3)]
# We will be slicing into the state according to state[slices[1]] and state[slices[2]],
# giving us all of the amplitudes with a |1> and |2> for the control qutrit. The resulting
# array has lost an axis relative to state and we need to be careful about the axis we
# roll. If axes[1] is larger than axes[0], then we need to shift the target axis down by
# one, otherwise we can leave as-is. For example: a state has [0, 1, 2, 3], control=1,
# target=3. Then, state[slices[1]] has 3 axes and target=3 now corresponds to the second axis.
target_axes = [axes[1] - 1] if axes[1] > axes[0] else [axes[1]]
state_1 = self._apply_tshift(state[slices[1]], axes=target_axes, inverse=inverse)
state_2 = self._apply_tshift(state[slices[2]], axes=target_axes, inverse=not inverse)
return self._stack([state[slices[0]], state_1, state_2], axis=axes[0])
def _apply_tswap(self, state, axes, **kwargs): # pylint: disable=unused-argument
"""Applies a ternary SWAP gate by performing a partial transposition along the
specified axes. The ternary SWAP gate acts on the computational basis states like
:math:`\vert i, j\rangle \rightarrow \vert j, i \rangle`.
Args:
state (array[complex]): input state
axes (List[int]): target axes to apply transformation
Returns:
array[complex]: output state
"""
all_axes = list(range(len(state.shape)))
all_axes[axes[0]] = axes[1]
all_axes[axes[1]] = axes[0]
return self._transpose(state, all_axes)
def _apply_phase(
self, state, axes, index, phase, inverse=False
): # pylint: disable=too-many-arguments
"""Applies a phase onto the specified index along the axis specified in ``axes``.
Args:
state (array[complex]): input state
axes (List[int]): target axes to apply transformation
index (int): target index of axis to apply phase to
phase (float): phase to apply
inverse (bool): whether to apply the inverse phase
Returns:
array[complex]: output state
"""
num_wires = len(state.shape)
slices = [_get_slice(i, axes[0], num_wires) for i in range(3)]
phase = self._conj(phase) if inverse else phase
state_slices = [
self._const_mul(phase if i == index else 1, state[slices[i]]) for i in range(3)
]
return self._stack(state_slices, axis=axes[0])
def _get_unitary_matrix(self, unitary): # pylint: disable=no-self-use
"""Return the matrix representing a unitary operation.
Args:
unitary (~.Operation): a PennyLane unitary operation
Returns:
array[complex]: Returns a 2D matrix representation of
the unitary in the computational basis.
"""
return unitary.matrix()
[docs] @classmethod
def capabilities(cls):
capabilities = super().capabilities().copy()
capabilities.update(
model="qutrit",
supports_inverse_operations=True,
supports_analytic_computation=True,
returns_state=True,
passthru_devices={
"autograd": "default.qutrit",
"tf": "default.qutrit",
"torch": "default.qutrit",
"jax": "default.qutrit",
},
)
return capabilities
def _create_basis_state(self, index):
"""Return a computational basis state over all wires.
Args:
index (int): integer representing the computational basis state
Returns:
array[complex]: complex array of shape ``[3]*self.num_wires``
representing the statevector of the basis state
"""
state = np.zeros(3**self.num_wires, dtype=np.complex128)
state[index] = 1
state = self._asarray(state, dtype=self.C_DTYPE)
return self._reshape(state, [3] * self.num_wires)
@property
def state(self):
return self._flatten(self._pre_rotated_state)
[docs] @debug_logger
def density_matrix(self, wires):
"""Returns the reduced density matrix of a given set of wires.
Args:
wires (Wires): wires of the reduced system.
Returns:
array[complex]: complex tensor of shape ``(3 ** len(wires), 3 ** len(wires))``
representing the reduced density matrix.
"""
dim = self.num_wires
state = self._pre_rotated_state
# Return the full density matrix by using numpy tensor product
if wires == self.wires:
density_matrix = self._tensordot(state, self._conj(state), axes=0)
density_matrix = self._reshape(density_matrix, (3 ** len(wires), 3 ** len(wires)))
return density_matrix
complete_system = list(range(0, dim))
traced_system = [x for x in complete_system if x not in wires.labels]
# Return the reduced density matrix by using numpy tensor product
density_matrix = self._tensordot(
state, self._conj(state), axes=(traced_system, traced_system)
)
density_matrix = self._reshape(density_matrix, (3 ** len(wires), 3 ** len(wires)))
return density_matrix
def _apply_unitary(self, state, mat, wires):
r"""Apply multiplication of a matrix to subsystems of the quantum state.
Args:
state (array[complex]): input state
mat (array): matrix to multiply
wires (Wires): target wires
Returns:
array[complex]: output state
"""
# translate to wire labels used by device
device_wires = self.map_wires(wires)
mat = self._cast(self._reshape(mat, [3] * len(device_wires) * 2), dtype=self.C_DTYPE)
axes = (list(range(len(device_wires), 2 * len(device_wires))), device_wires)
tdot = self._tensordot(mat, state, axes=axes)
# tensordot causes the axes given in `wires` to end up in the first positions
# of the resulting tensor. This corresponds to a (partial) transpose of
# the correct output state
# We'll need to invert this permutation to put the indices in the correct place
unused_idxs = [idx for idx in range(self.num_wires) if idx not in device_wires]
perm = list(device_wires) + unused_idxs
inv_perm = np.argsort(perm) # argsort gives inverse permutation
return self._transpose(tdot, inv_perm)
[docs] @debug_logger
def reset(self):
"""Reset the device"""
super().reset()
# init the state vector to |00..0>
self._state = self._create_basis_state(0)
self._pre_rotated_state = self._state
[docs] @debug_logger
def analytic_probability(self, wires=None):
if self._state is None:
return None
flat_state = self._flatten(self._state)
real_state = self._real(flat_state)
imag_state = self._imag(flat_state)
prob = self.marginal_prob(real_state**2 + imag_state**2, wires)
return prob
_modules/pennylane/devices/default_qutrit
Download Python script
Download Notebook
View on GitHub