Source code for pennylane.transforms.decompositions.clifford_t_transform
# Copyright 2018-2023 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."""Transform function for the Clifford+T decomposition."""importmathimportwarningsfromitertoolsimportproductimportpennylaneasqmlfrompennylane.opsimportAdjointfrompennylane.ops.op_math.decompositions.solovay_kitaevimportsk_decompositionfrompennylane.queuingimportQueuingManagerfrompennylane.tapeimportQuantumScript,QuantumScriptBatchfrompennylane.transforms.coreimporttransformfrompennylane.transforms.optimizationimport(cancel_inverses,commute_controlled,merge_rotations,remove_barrier,)frompennylane.transforms.optimization.optimization_utilsimport_fuse_global_phases,find_next_gatefrompennylane.typingimportPostprocessingFn# Single qubits Clifford+T gates in PL_CLIFFORD_T_ONE_GATES=[qml.Identity,qml.X,qml.Y,qml.Z,qml.Hadamard,qml.S,qml.SX,qml.T,]# Two qubits Clifford+T gates in PL_CLIFFORD_T_TWO_GATES=[qml.CNOT,qml.CY,qml.CZ,qml.SWAP,qml.ISWAP,]# Single-parameter gates in PL# note: _simplify_param makes use of their periodic nature,# any additions to this, should be reflected there as well._PARAMETER_GATES=(qml.RX,qml.RY,qml.RZ,qml.Rot,qml.PhaseShift)# Clifford+T gate set_CLIFFORD_T_GATES=tuple(_CLIFFORD_T_ONE_GATES+_CLIFFORD_T_TWO_GATES)+(qml.GlobalPhase,)# Gates to be skipped during decomposition_SKIP_OP_TYPES=(qml.Barrier,qml.Snapshot,qml.WireCut)def_check_clifford_op(op,use_decomposition=False):r"""Checks if an operator is Clifford or not. For a given unitary operator :math:`U` acting on :math:`N` qubits, this method checks that the transformation :math:`UPU^{\dagger}` maps the Pauli tensor products :math:`P = {I, X, Y, Z}^{\otimes N}` to Pauli tensor products using the decomposition of the matrix for :math:`U` in the Pauli basis. Args: op (~pennylane.operation.Operation): the operator that needs to be tested use_decomposition (bool): if ``True``, use operator's decomposition to compute the matrix, in case it doesn't define a ``compute_matrix`` method. Default is ``False``. Returns: Bool that represents whether the provided operator is Clifford or not. """# Check if matrix can be calculated for the operatorif(notop.has_matrixandnotuse_decomposition)or(use_decompositionandnotqml.tape.QuantumScript(op.decomposition()).wires):returnFalse# Compute the LCUs for the operator in Pauli basispauli_terms=qml.pauli_decompose(qml.matrix(op),wire_order=op.wires,check_hermitian=False)pauli_terms_adj=qml.Hamiltonian(qml.math.conj(pauli_terms.coeffs),pauli_terms.ops)# Build Pauli tensor products P in the Hamiltonian formdefpauli_group(x):return[qml.Identity(x),qml.X(x),qml.Y(x),qml.Z(x)]# Build PauliSentence and Hamiltonians representations for set Ppauli_sens=[qml.pauli.pauli_sentence(qml.prod(*pauli))forpauliinproduct(*(pauli_group(idx)foridxinop.wires))]pauli_ops=(pauli_sen.operation(wire_order=op.wires)forpauli_seninpauli_sens)# Perform U@P@U^\dagger and check if the result exists in set Pforpauli_prodinproduct([pauli_terms],pauli_ops,[pauli_terms_adj]):# hopefully op_math.prod scales better than matrix multiplication, i.e., O((2^N)^3)upu=qml.pauli.pauli_sentence(qml.prod(*pauli_prod))upu.simplify()# Pauli sum always lie outside set Piflen(upu)!=1:returnFalsereturnTruedefcheck_clifford_t(op,use_decomposition=False):r"""Checks whether the gate is in the standard Clifford+T basis. For a given unitary operator :math:`U` acting on :math:`N` qubits, which is not a T-gate, this method checks that the transformation :math:`UPU^{\dagger}` maps the Pauli tensor products :math:`P = {I, X, Y, Z}^{\otimes N}` to Pauli tensor products using the decomposition of the matrix for :math:`U` in the Pauli basis. Args: op (~pennylane.operation.Operation): the operator that needs to be checked use_decomposition (bool): if ``True``, use operator's decomposition to compute the matrix, in case it doesn't define a ``compute_matrix`` method. Default is ``False``. Returns: Bool that represents whether the provided operator is Clifford+T or not. """# Get the base operation for an adjointed operationifisinstance(op,Adjoint):base=op.baseelse:base=None# Save time and check from the pre-computed listifisinstance(op,_CLIFFORD_T_GATES)orisinstance(base,_CLIFFORD_T_GATES):returnTrue# Save time and check from the parameter of rotation gatesifisinstance(op,_PARAMETER_GATES)orisinstance(base,_PARAMETER_GATES):theta=op.data[0]return(Falseifqml.math.is_abstract(theta)elseqml.math.allclose(qml.math.mod(theta,math.pi),0.0))return_check_clifford_op(op,use_decomposition=use_decomposition)def_simplify_param(theta,gate):r"""Check if the parameter allows simplification for the rotation gate. For the cases where theta is an integer multiple of π: (a) returns a global phase when even, and (b) returns combination of provided gate with global phase when odd. In rest of the other cases it would return None. """ifqml.math.is_abstract(theta):# pragma: no coverreturnNoneifqml.math.allclose(theta,0.0,atol=1e-6):return[qml.GlobalPhase(0.0)]rem_,mod_=qml.math.divide(theta,math.pi),qml.math.mod(theta,math.pi)ifqml.math.allclose(mod_,0.0,atol=1e-6):ops=[qml.GlobalPhase(theta/2)]ifqml.math.allclose(qml.math.mod(rem_,2),1.0,atol=1e-6):ops.append(gate)returnopsreturnNone# pylint: disable= too-many-branches,def_rot_decompose(op):r"""Decomposes a rotation operation: :class:`~.Rot`, :class:`~.RX`, :class:`~.RY`, :class:`~.RZ`, :class:`~.PhaseShift` into a basis composed of :class:`~.RZ`, :class:`~.S`, and :class:`~.Hadamard`. """d_ops=[]# Extend for Rot operation with Rz.Ry.Rz decompositionsifisinstance(op,qml.Rot):(phi,theta,omega),wires=op.parameters,op.wiresfordecin[qml.RZ(phi,wires),qml.RY(theta,wires),qml.RZ(omega,wires)]:d_ops.extend(_rot_decompose(dec))returnd_ops(theta,),wires=op.data,op.wiresifisinstance(op,qml.ops.Adjoint):# pylint: disable=no-memberops_=_rot_decompose(op.base.adjoint())elifisinstance(op,qml.RX):ops_=_simplify_param(theta,qml.X(wires))ifops_isNone:# Use Rx = H @ Rz @ Hops_=[qml.Hadamard(wires),qml.RZ(theta,wires),qml.Hadamard(wires)]elifisinstance(op,qml.RY):ops_=_simplify_param(theta,qml.Y(wires))ifops_isNone:# Use Ry = S @ H @ Rz @ H @ S.adjoint()ops_=[qml.S(wires),qml.Hadamard(wires),qml.RZ(theta,wires),qml.Hadamard(wires),qml.adjoint(qml.S(wires)),][::-1]elifisinstance(op,qml.RZ):ops_=_simplify_param(theta,qml.Z(wires))ifops_isNone:ops_=[qml.RZ(theta,wires)]elifisinstance(op,qml.PhaseShift):ops_=_simplify_param(theta,qml.Z(wires))ifops_isNone:ops_=[qml.RZ(theta,wires=wires),qml.GlobalPhase(-theta/2)]ifnotqml.math.is_abstract(theta):# The following loop simplifies the two cases where for all odd intergers `k`,# `PhaseShift(k * pi / 2)` is S / S* and `PhaseShift(k * pi / 4)` is T / T*.forval_in[2,4]:div_=qml.math.divide(theta,math.pi/val_)mod_=qml.math.mod(theta,math.pi/val_)ifqml.math.allclose(mod_,0.0,atol=1e-6)andqml.math.allclose(qml.math.mod(div_,2),1.0,atol=1e-6):vop_=qml.S(wires)ifval_==2elseqml.T(wires)sign=qml.math.mod(qml.math.floor_divide(div_,2),2)ops_=[vop_ifqml.math.allclose(sign,0.0,atol=1e-6)elseqml.adjoint(vop_)]breakelse:ops_.append(qml.GlobalPhase(-theta/2))else:raiseValueError(f"Operation {op} is not a supported Pauli rotation: qml.RX, qml.RY, qml.RZ, qml.Rot and qml.PhaseShift.")d_ops.extend(ops_)returnd_opsdef_one_qubit_decompose(op):r"""Decomposition for single qubit operations using combination of :class:`~.RZ`, :class:`~.S`, and :class:`~.Hadamard`."""withwarnings.catch_warnings():warnings.simplefilter("ignore",RuntimeWarning)sd_ops=qml.ops.one_qubit_decomposition(qml.matrix(op),op.wires,"ZXZ",return_global_phase=True)# Get the global phasegphase_op=sd_ops.pop()# Decompose the rotation gatesd_ops=[]forsd_opinsd_ops:d_ops.extend(_rot_decompose(sd_op)ifsd_op.num_paramselse[sd_op])returnd_ops,gphase_opdef_two_qubit_decompose(op):r"""Decomposition for two qubit operations using combination of :class:`~.RZ`, :class:`~.S`, :class:`~.Hadamard`, and :class:`~.CNOT`."""withwarnings.catch_warnings():warnings.simplefilter("ignore",RuntimeWarning)td_ops=qml.ops.two_qubit_decomposition(qml.matrix(op),op.wires)d_ops=[]fortd_opintd_ops:d_ops.extend(_rot_decompose(td_op)iftd_op.num_paramsandtd_op.num_wires==1else[td_op])returnd_opsdef_merge_param_gates(operations,merge_ops=None):"""Merge the provided parametrized gates on the same wires that are adjacent to each other"""copied_ops=operations.copy()merged_ops,number_ops=[],0whilelen(copied_ops)>0:curr_gate=copied_ops.pop(0)# If gate is not to be merged, let it goifmerge_opsisnotNoneandcurr_gate.namenotinmerge_ops:merged_ops.append(curr_gate)continue# If gate is in the merge_ops, update counterifcurr_gate.nameinmerge_ops:number_ops+=1# Find the next gate that acts on the same wiresnext_gate_idx=find_next_gate(curr_gate.wires,copied_ops)ifnext_gate_idxisNone:merged_ops.append(curr_gate)continue# Initialize the current angle and iterate until end of mergecurr_params=curr_gate.parameterscurr_intrfc=qml.math.get_deep_interface(curr_gate.parameters)cumulative_angles=qml.math.array(curr_params,dtype=float,like=curr_intrfc)next_gate=copied_ops[next_gate_idx]whilecurr_gate.name==next_gate.nameandcurr_gate.wires==next_gate.wires:cumulative_angles+=qml.math.array(next_gate.parameters,like=curr_intrfc)# Check if the subsequent gate exists in the vicinitycopied_ops.pop(next_gate_idx)next_gate_idx=find_next_gate(curr_gate.wires,copied_ops)ifnext_gate_idxisNone:breaknext_gate=copied_ops[next_gate_idx]# Replace the current gate, add it to merged list and pop it from orginal onemerged_ops.append(curr_gate.__class__(*cumulative_angles,wires=curr_gate.wires))returnmerged_ops,number_ops# pylint: disable= too-many-nested-blocks, too-many-branches, too-many-statements, unnecessary-lambda-assignment
[docs]@transformdefclifford_t_decomposition(tape:QuantumScript,epsilon=1e-4,method="sk",**method_kwargs,)->tuple[QuantumScriptBatch,PostprocessingFn]:r"""Decomposes a circuit into the Clifford+T basis. This method first decomposes the gate operations to a basis comprised of Clifford, :class:`~.T`, :class:`~.RZ` and :class:`~.GlobalPhase` operations (and their adjoints). The Clifford gates include the following PennyLane operations: - Single qubit gates - :class:`~.Identity`, :class:`~.PauliX`, :class:`~.PauliY`, :class:`~.PauliZ`, :class:`~.SX`, :class:`~.S`, and :class:`~.Hadamard`. - Two qubit gates - :class:`~.CNOT`, :class:`~.CY`, :class:`~.CZ`, :class:`~.SWAP`, and :class:`~.ISWAP`. Then, the leftover single qubit :class:`~.RZ` operations are approximated in the Clifford+T basis with :math:`\epsilon > 0` error. By default, we use the Solovay-Kitaev algorithm described in `Dawson and Nielsen (2005) <https://arxiv.org/abs/quant-ph/0505030>`_ for this. Args: tape (QNode or QuantumTape or Callable): The quantum circuit to be decomposed. epsilon (float): The maximum permissible operator norm error of the complete circuit decomposition. Defaults to ``0.0001``. method (str): Method to be used for Clifford+T decomposition. Default value is ``"sk"`` for Solovay-Kitaev. **method_kwargs: Keyword argument to pass options for the ``method`` used for decompositions. Returns: qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in the :func:`qml.transform <pennylane.transform>`. **Keyword Arguments** - Solovay-Kitaev decomposition -- **max_depth** (int), **basis_set** (list[str]), **basis_length** (int) -- arguments for the ``"sk"`` method, where the decomposition is performed using the :func:`~.sk_decomposition` method. Raises: ValueError: If a gate operation does not have a decomposition when required. NotImplementedError: If chosen decomposition ``method`` is not supported. .. seealso:: :func:`~.sk_decomposition` for Solovay-Kitaev decomposition. **Example** .. code-block:: python3 @qml.qnode(qml.device("default.qubit")) def circuit(x, y): qml.RX(x, 0) qml.CNOT([0, 1]) qml.RY(y, 0) return qml.expval(qml.Z(0)) x, y = 1.1, 2.2 decomposed_circuit = qml.transforms.clifford_t_decomposition(circuit) result = circuit(x, y) approx = decomposed_circuit(x, y) >>> qml.math.allclose(result, approx, atol=1e-4) True """withQueuingManager.stop_recording():# Build the basis set and the pipeline for initial compilation passbasis_set=[op.__name__foropin_PARAMETER_GATES+_CLIFFORD_T_GATES+_SKIP_OP_TYPES]pipelines=[remove_barrier,commute_controlled,cancel_inverses,merge_rotations]# Compile the tape according to depth provided by the user and expand it[compiled_tape],_=qml.compile(tape,pipelines,basis_set=basis_set)# Now iterate over the expanded tape operationsdecomp_ops,gphase_ops=[],[]foropincompiled_tape.operations:# Check whether operation is to be skippedifisinstance(op,_SKIP_OP_TYPES):decomp_ops.append(op)# Check whether the operation is a global phaseelifisinstance(op,qml.GlobalPhase):gphase_ops.append(op)# Check whether the operation is a Clifford or a T-gateelifop.nameinbasis_setandcheck_clifford_t(op):ifop.num_params:decomp_ops.extend(_rot_decompose(op))else:decomp_ops.append(op)# Decompose and then iteratively go deeper via DFSelse:# Single qubit unitary decomposition with ZXZ rotationsifop.num_wires==1:ifop.nameinbasis_set:d_ops=_rot_decompose(op)else:d_ops,g_op=_one_qubit_decompose(op)gphase_ops.append(g_op)decomp_ops.extend(d_ops)# Two qubit unitary decomposition with SU(4) rotationselifop.num_wires==2:d_ops=_two_qubit_decompose(op)decomp_ops.extend(d_ops)# If we don't know how to decompose the operationelse:raiseValueError(f"Cannot unroll {op} into the Clifford+T basis as no rule exists for its decomposition")# Merge RZ rotations togethermerged_ops,number_ops=_merge_param_gates(decomp_ops,merge_ops=["RZ"])# Squeeze global phases into a single global phasenew_operations=_fuse_global_phases(merged_ops+gphase_ops)# Compute the per-gate epsilon valueepsilon/=number_opsor1# Every decomposition implementation should have the following shape:# def decompose_fn(op: Operator, epsilon: float, **method_kwargs) -> List[Operator]# note: the last operator in the decomposition must be a GlobalPhase# Build the approximation set for Solovay-Kitaev decompositionifmethod=="sk":decompose_fn=sk_decompositionelse:raiseNotImplementedError(f"Currently we only support Solovay-Kitaev ('sk') decomposition, got {method}")decomp_ops=[]phase=new_operations.pop().data[0]foropinnew_operations:ifisinstance(op,qml.RZ):clifford_ops=decompose_fn(op,epsilon,**method_kwargs)phase+=qml.math.convert_like(clifford_ops.pop().data[0],phase)decomp_ops.extend(clifford_ops)else:decomp_ops.append(op)# check if phase is non-zero for non jax-jit casesifqml.math.is_abstract(phase)ornotqml.math.allclose(phase,0.0):decomp_ops.append(qml.GlobalPhase(phase))# Construct a new tape with the expanded set of operationsnew_tape=compiled_tape.copy(operations=decomp_ops)# Perform a final attempt of simplification before return[new_tape],_=cancel_inverses(new_tape)defnull_postprocessing(results):"""A postprocesing function returned by a transform that only converts the batch of results into a result for a single ``QuantumTape``. """returnresults[0]return[new_tape],null_postprocessing