class PauliRot(Operation): r"""PauliRot(theta, pauli_word, wires) Arbitrary Pauli word rotation. .. math:: RP(\theta, P) = \exp(-i \frac{\theta}{2} P) **Details:** * Number of wires: Any * Number of parameters: 2 (1 differentiable parameter) * Gradient recipe: :math:`\frac{d}{d\theta}f(RP(\theta)) = \frac{1}{2}\left[f(RP(\theta +\pi/2)) - f(RP(\theta-\pi/2))\right]` where :math:`f` is an expectation value depending on :math:`RP(\theta)`. .. note:: If the ``PauliRot`` gate is not supported on the targeted device, PennyLane will decompose the gate using :class:`~.RX`, :class:`~.Hadamard`, :class:`~.RZ` and :class:`~.CNOT` gates. Args: theta (float): rotation angle :math:`\theta` pauli_word (string): the Pauli word defining the rotation wires (Sequence[int] or int): the wire the operation acts on **Example** >>> dev = qml.device('default.qubit', wires=1) >>> @qml.qnode(dev) ... def example_circuit(): ... qml.PauliRot(0.5, 'X', wires=0) ... return qml.expval(qml.PauliZ(0)) >>> print(example_circuit()) 0.8775825618903724 """ num_wires = AnyWires do_check_domain = False grad_method = "A" _ALLOWED_CHARACTERS = "IXYZ" _PAULI_CONJUGATION_MATRICES = { "X": Hadamard._matrix(), "Y": RX._matrix(np.pi / 2), "Z": np.array([[1, 0], [0, 1]]), } def __init__(self, theta, pauli_word, wires=None, do_queue=True): super().__init__(theta, pauli_word, wires=wires, do_queue=do_queue) if not PauliRot._check_pauli_word(pauli_word): raise ValueError( f'The given Pauli word "{pauli_word}" contains characters that are not allowed.' " Allowed characters are I, X, Y and Z") num_wires = 1 if isinstance(wires, int) else len(wires) if not len(pauli_word) == num_wires: raise ValueError( f"The given Pauli word has length {len(pauli_word)}, length {num_wires} was expected for wires {wires}" ) @property def num_params(self): return 2 def label(self, decimals=None, base_label=None): r"""A customizable string representation of the operator. Args: decimals=None (int): If ``None``, no parameters are included. Else, specifies how to round the parameters. base_label=None (str): overwrite the non-parameter component of the label Returns: str: label to use in drawings **Example:** >>> op = qml.PauliRot(0.1, "XYY", wires=(0,1,2)) >>> op.label() 'R(XYY)' >>> op.label(decimals=2) 'R(XYY)\n(0.10)' >>> op.label(base_label="PauliRot") 'PauliRot\n(0.10)' """ op_label = base_label or ("R(" + self.parameters[1] + ")") if self.inverse: op_label += "⁻¹" if decimals is not None: param_string = f"\n({qml.math.asarray(self.parameters[0]):.{decimals}f})" op_label += param_string return op_label @staticmethod def _check_pauli_word(pauli_word): """Check that the given Pauli word has correct structure. Args: pauli_word (str): Pauli word to be checked Returns: bool: Whether the Pauli word has correct structure. """ return all(pauli in PauliRot._ALLOWED_CHARACTERS for pauli in pauli_word) @classmethod def _matrix(cls, *params): pauli_word = params[1] if not PauliRot._check_pauli_word(pauli_word): raise ValueError( f'The given Pauli word "{pauli_word}" contains characters that are not allowed.' " Allowed characters are I, X, Y and Z") theta = params[0] interface = qml.math.get_interface(theta) if interface == "tensorflow": theta = qml.math.cast_like(theta, 1j) # Simplest case is if the Pauli is the identity matrix if pauli_word == "I" * len(pauli_word): exp = qml.math.exp(-1j * theta / 2) iden = qml.math.eye(2**len(pauli_word)) if interface == "torch": # Use convert_like to ensure that the tensor is put on the correct # Torch device iden = qml.math.convert_like(iden, theta) return exp * iden return qml.math.array(exp * iden, like=interface) # We first generate the matrix excluding the identity parts and expand it afterwards. # To this end, we have to store on which wires the non-identity parts act non_identity_wires, non_identity_gates = zip( *[(wire, gate) for wire, gate in enumerate(pauli_word) if gate != "I"]) multi_Z_rot_matrix = MultiRZ._matrix(theta, len(non_identity_gates)) # now we conjugate with Hadamard and RX to create the Pauli string conjugation_matrix = functools.reduce( qml.math.kron, [ PauliRot._PAULI_CONJUGATION_MATRICES[gate] for gate in non_identity_gates ], ) return expand( qml.math.dot( qml.math.conj(conjugation_matrix), qml.math.dot(multi_Z_rot_matrix, conjugation_matrix), ), non_identity_wires, list(range(len(pauli_word))), ) _generator = None @property def generator(self): if self._generator is None: pauli_word = self.parameters[1] # Simplest case is if the Pauli is the identity matrix if pauli_word == "I" * len(pauli_word): self._generator = [np.eye(2**len(pauli_word)), -1 / 2] return self._generator # We first generate the matrix excluding the identity parts and expand it afterwards. # To this end, we have to store on which wires the non-identity parts act non_identity_wires, non_identity_gates = zip( *[(wire, gate) for wire, gate in enumerate(pauli_word) if gate != "I"]) # get MultiRZ's generator multi_Z_rot_generator = qml.math.diag( pauli_eigs(len(non_identity_gates))) # now we conjugate with Hadamard and RX to create the Pauli string conjugation_matrix = functools.reduce( qml.math.kron, [ PauliRot._PAULI_CONJUGATION_MATRICES[gate] for gate in non_identity_gates ], ) self._generator = [ expand( qml.math.dot( qml.math.conj(qml.math.T(conjugation_matrix)), qml.math.dot(multi_Z_rot_generator, conjugation_matrix), ), non_identity_wires, list(range(len(pauli_word))), ), -1 / 2, ] return self._generator @classmethod def _eigvals(cls, theta, pauli_word): if qml.math.get_interface(theta) == "tensorflow": theta = qml.math.cast_like(theta, 1j) # Identity must be treated specially because its eigenvalues are all the same if pauli_word == "I" * len(pauli_word): return qml.math.exp(-1j * theta / 2) * qml.math.ones( 2**len(pauli_word)) return MultiRZ._eigvals(theta, len(pauli_word)) @staticmethod def decomposition(theta, pauli_word, wires): # Catch cases when the wire is passed as a single int. if isinstance(wires, int): wires = [wires] with qml.tape.OperationRecorder() as rec: # Check for identity and do nothing if pauli_word == "I" * len(wires): return [] active_wires, active_gates = zip( *[(wire, gate) for wire, gate in zip(wires, pauli_word) if gate != "I"]) for wire, gate in zip(active_wires, active_gates): if gate == "X": Hadamard(wires=[wire]) elif gate == "Y": RX(np.pi / 2, wires=[wire]) MultiRZ(theta, wires=list(active_wires)) for wire, gate in zip(active_wires, active_gates): if gate == "X": Hadamard(wires=[wire]) elif gate == "Y": RX(-np.pi / 2, wires=[wire]) return rec.queue def adjoint(self): return PauliRot(-self.parameters[0], self.parameters[1], wires=self.wires)
class PauliRot(Operation): r"""PauliRot(theta, pauli_word, wires) Arbitrary Pauli word rotation. .. math:: RP(\theta, P) = \exp(-i \frac{\theta}{2} P) **Details:** * Number of wires: Any * Number of parameters: 2 (1 differentiable parameter) * Gradient recipe: :math:`\frac{d}{d\theta}f(RP(\theta)) = \frac{1}{2}\left[f(RP(\theta +\pi/2)) - f(RP(\theta-\pi/2))\right]` where :math:`f` is an expectation value depending on :math:`RP(\theta)`. .. note:: If the ``PauliRot`` gate is not supported on the targeted device, PennyLane will decompose the gate using :class:`~.RX`, :class:`~.Hadamard`, :class:`~.RZ` and :class:`~.CNOT` gates. Args: theta (float): rotation angle :math:`\theta` pauli_word (string): the Pauli word defining the rotation wires (Sequence[int] or int): the wire the operation acts on """ num_params = 2 num_wires = AnyWires do_check_domain = False par_domain = "R" grad_method = "A" _ALLOWED_CHARACTERS = "IXYZ" _PAULI_CONJUGATION_MATRICES = { "X": Hadamard._matrix(), "Y": RX._matrix(np.pi / 2), "Z": np.array([[1, 0], [0, 1]]), } def __init__(self, *params, wires=None, do_queue=True): super().__init__(*params, wires=wires, do_queue=do_queue) pauli_word = params[1] if not PauliRot._check_pauli_word(pauli_word): raise ValueError( 'The given Pauli word "{}" contains characters that are not allowed.' " Allowed characters are I, X, Y and Z".format(pauli_word) ) num_wires = 1 if isinstance(wires, int) else len(wires) if not len(pauli_word) == num_wires: raise ValueError( "The given Pauli word has length {}, length {} was expected for wires {}".format( len(pauli_word), num_wires, wires ) ) @staticmethod def _check_pauli_word(pauli_word): """Check that the given Pauli word has correct structure. Args: pauli_word (str): Pauli word to be checked Returns: bool: Whether the Pauli word has correct structure. """ return all(pauli in PauliRot._ALLOWED_CHARACTERS for pauli in pauli_word) @classmethod def _matrix(cls, *params): theta = params[0] pauli_word = params[1] if not PauliRot._check_pauli_word(pauli_word): raise ValueError( 'The given Pauli word "{}" contains characters that are not allowed.' " Allowed characters are I, X, Y and Z".format(pauli_word) ) # Simplest case is if the Pauli is the identity matrix if pauli_word == "I" * len(pauli_word): return np.exp(-1j * theta / 2) * np.eye(2 ** len(pauli_word)) # We first generate the matrix excluding the identity parts and expand it afterwards. # To this end, we have to store on which wires the non-identity parts act non_identity_wires, non_identity_gates = zip( *[(wire, gate) for wire, gate in enumerate(pauli_word) if gate != "I"] ) multi_Z_rot_matrix = MultiRZ._matrix(theta, len(non_identity_gates)) # now we conjugate with Hadamard and RX to create the Pauli string conjugation_matrix = functools.reduce( np.kron, [PauliRot._PAULI_CONJUGATION_MATRICES[gate] for gate in non_identity_gates], ) return expand( conjugation_matrix.T.conj() @ multi_Z_rot_matrix @ conjugation_matrix, non_identity_wires, list(range(len(pauli_word))), ) _generator = None @property def generator(self): if self._generator is None: pauli_word = self.parameters[1] # Simplest case is if the Pauli is the identity matrix if pauli_word == "I" * len(pauli_word): self._generator = [np.eye(2 ** len(pauli_word)), -1 / 2] return self._generator # We first generate the matrix excluding the identity parts and expand it afterwards. # To this end, we have to store on which wires the non-identity parts act non_identity_wires, non_identity_gates = zip( *[(wire, gate) for wire, gate in enumerate(pauli_word) if gate != "I"] ) # get MultiRZ's generator multi_Z_rot_generator = np.diag(pauli_eigs(len(non_identity_gates))) # now we conjugate with Hadamard and RX to create the Pauli string conjugation_matrix = functools.reduce( np.kron, [PauliRot._PAULI_CONJUGATION_MATRICES[gate] for gate in non_identity_gates], ) self._generator = [ expand( conjugation_matrix.T.conj() @ multi_Z_rot_generator @ conjugation_matrix, non_identity_wires, list(range(len(pauli_word))), ), -1 / 2, ] return self._generator @classmethod def _eigvals(cls, theta, pauli_word): # Identity must be treated specially because its eigenvalues are all the same if pauli_word == "I" * len(pauli_word): return np.exp(-1j * theta / 2) * np.ones(2 ** len(pauli_word)) return MultiRZ._eigvals(theta, len(pauli_word)) @staticmethod @template def decomposition(theta, pauli_word, wires): # Catch cases when the wire is passed as a single int. if isinstance(wires, int): wires = [wires] # Check for identity and do nothing if pauli_word == "I" * len(wires): return active_wires, active_gates = zip( *[(wire, gate) for wire, gate in zip(wires, pauli_word) if gate != "I"] ) for wire, gate in zip(active_wires, active_gates): if gate == "X": Hadamard(wires=[wire]) elif gate == "Y": RX(np.pi / 2, wires=[wire]) MultiRZ(theta, wires=list(active_wires)) for wire, gate in zip(active_wires, active_gates): if gate == "X": Hadamard(wires=[wire]) elif gate == "Y": RX(-np.pi / 2, wires=[wire]) def adjoint(self): return PauliRot(-self.parameters[0], self.parameters[1], wires=self.wires)