# 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 statesimulation of qutrit-based quantum computing."""importfunctoolsimportloggingimportnumpyasnpimportpennylaneasqml# pylint: disable=unused-importfrompennylane.loggingimportdebug_logger,debug_logger_initfrompennylane.wiresimportWireErrorfrom.._versionimport__version__from._qutrit_deviceimportQutritDevicelogger=logging.getLogger(__name__)logger.addHandler(logging.NullHandler())# tolerance for numerical errorstolerance=1e-10OMEGA=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_axesidx[axis]=indexreturntuple(idx)# pylint: disable=too-many-arguments
[docs]classDefaultQutrit(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 addedoperations={"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.Identityobservables={"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)@staticmethoddef_reduce_sum(array,axes):returnqml.math.sum(array,tuple(axes))@staticmethoddef_asarray(array,dtype=None):# Support floatifnothasattr(array,"__len__"):returnnp.asarray(array,dtype=dtype)res=qml.math.cast(array,dtype=dtype)returnres@debug_logger_initdef__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 operationsself._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()defmap_wires(self,wires):# temporarily overwrite this method to bypass# wire map that produces Wires objectstry:mapped_wires=[self.wire_map[w]forwinwires]exceptKeyErrorase:raiseWireError(f"Did not find some of the wires {wires.labels} on device with wires {self.wires.labels}.")fromereturnmapped_wires
[docs]defdefine_wire_map(self,wires):# temporarily overwrite this method to bypass# wire map that produces Wires objectsconsecutive_wires=range(self.num_wires)wire_map=zip(wires,consecutive_wires)returndict(wire_map)
[docs]@debug_loggerdefapply(self,operations,rotations=None,**kwargs):# pylint: disable=arguments-differrotations=rotationsor[]# 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.fori,operationinenumerate(operations):# pylint: disable=unused-variableifi>0andisinstance(operation,qml.QutritBasisState):raiseqml.DeviceError(f"Operation {operation.name} cannot be used after other operations have already been applied "f"on a {self.short_name} device.")ifisinstance(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 stateself._pre_rotated_state=self._state# apply the circuit rotationsforoperationinrotations: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 devicedevice_wires=self.map_wires(wires)# length of basis state parametern_basis_state=len(state)ifnotset(state.tolist()).issubset({0,1,2}):raiseValueError("QutritBasisState parameter must consist of 0, 1 or 2 integers.")ifn_basis_state!=len(device_wires):raiseValueError("QutritBasisState parameter and wires must be of equal length.")# get computational basis state numberbasis_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 """ifoperation.name=="Identity":returnstatewires=operation.wiresifoperation.nameinself._apply_ops:# pylint: disable=no-else-returnaxes=self.wires.indices(wires)returnself._apply_ops[operation.name](state,axes)elif(isinstance(operation,qml.ops.Adjoint)# pylint: disable=no-memberandoperation.base.nameinself._apply_ops):axes=self.wires.indices(wires)returnself._apply_ops[operation.base.name](state,axes,inverse=True)matrix=self._asarray(self._get_unitary_matrix(operation),dtype=self.C_DTYPE)returnself._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=-1ifinverseelse1returnself._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)returnself._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)foriinrange(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]ifaxes[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=notinverse)returnself._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]returnself._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)foriinrange(3)]phase=self._conj(phase)ifinverseelsephasestate_slices=[self._const_mul(phaseifi==indexelse1,state[slices[i]])foriinrange(3)]returnself._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. """returnunitary.matrix()
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]=1state=self._asarray(state,dtype=self.C_DTYPE)returnself._reshape(state,[3]*self.num_wires)@propertydefstate(self):returnself._flatten(self._pre_rotated_state)
[docs]@debug_loggerdefdensity_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_wiresstate=self._pre_rotated_state# Return the full density matrix by using numpy tensor productifwires==self.wires:density_matrix=self._tensordot(state,self._conj(state),axes=0)density_matrix=self._reshape(density_matrix,(3**len(wires),3**len(wires)))returndensity_matrixcomplete_system=list(range(0,dim))traced_system=[xforxincomplete_systemifxnotinwires.labels]# Return the reduced density matrix by using numpy tensor productdensity_matrix=self._tensordot(state,self._conj(state),axes=(traced_system,traced_system))density_matrix=self._reshape(density_matrix,(3**len(wires),3**len(wires)))returndensity_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 devicedevice_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 placeunused_idxs=[idxforidxinrange(self.num_wires)ifidxnotindevice_wires]perm=list(device_wires)+unused_idxsinv_perm=np.argsort(perm)# argsort gives inverse permutationreturnself._transpose(tdot,inv_perm)
[docs]@debug_loggerdefreset(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