# Copyright 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."""Functions handling quantum tapes for circuit cutting, and their auxillary functions."""importcopyfromcollections.abcimportCallable,SequencefromitertoolsimportproductfromtypingimportUnionfromnetworkximportMultiDiGraphimportpennylaneasqmlfrompennylaneimportexpvalfrompennylane.measurementsimportExpectationMP,MeasurementProcess,SampleMPfrompennylane.operationimportOperatorfrompennylane.ops.metaimportWireCutfrompennylane.pauliimportstring_to_pauli_wordfrompennylane.queuingimportWrappedObjfrompennylane.tapeimportQuantumScriptfrompennylane.wiresimportWiresfrom.utilsimportMeasureNode,PrepareNode
[docs]deftape_to_graph(tape:QuantumScript)->MultiDiGraph:""" Converts a quantum tape to a directed multigraph. .. note:: This operation is designed for use as part of the circuit cutting workflow. Check out the :func:`qml.cut_circuit() <pennylane.cut_circuit>` transform for more details. Args: tape (QuantumTape): tape to be converted into a directed multigraph Returns: nx.MultiDiGraph: a directed multigraph that captures the circuit structure of the input tape. The nodes of the graph are formatted as ``WrappedObj(op)``, where ``WrappedObj.obj`` is the operator. **Example** Consider the following tape: .. code-block:: python ops = [ qml.RX(0.4, wires=0), qml.RY(0.9, wires=0), qml.CNOT(wires=[0, 1]), ] measurements = [qml.expval(qml.Z(1))] tape = qml.tape.QuantumTape(ops,) Its corresponding circuit graph can be found using >>> qml.qcut.tape_to_graph(tape) <networkx.classes.multidigraph.MultiDiGraph at 0x7fe41cbd7210> """graph=MultiDiGraph()wire_latest_node={w:Noneforwintape.wires}fororder,opinenumerate(tape.operations):_add_operator_node(graph,op,order,wire_latest_node)order+=1# pylint: disable=undefined-loop-variableformintape.measurements:obs=getattr(m,"obs",None)ifobsisnotNoneandisinstance(obs,qml.ops.Prod):ifisinstance(m,SampleMP):raiseValueError("Sampling from tensor products of observables ""is not supported in circuit cutting")foroinobs.operandsifisinstance(obs,qml.ops.op_math.Prod)elseobs.obs:m_=m.__class__(obs=o)_add_operator_node(graph,m_,order,wire_latest_node)elifisinstance(m,SampleMP)andobsisNone:forwinm.wires:s_=qml.sample(qml.Projector([1],wires=w))_add_operator_node(graph,s_,order,wire_latest_node)else:_add_operator_node(graph,m,order,wire_latest_node)order+=1returngraph
# pylint: disable=protected-access
[docs]defgraph_to_tape(graph:MultiDiGraph)->QuantumScript:""" Converts a directed multigraph to the corresponding :class:`~.QuantumTape`. To account for the possibility of needing to perform mid-circuit measurements, if any operations follow a :class:`MeasureNode` operation on a given wire then these operations are mapped to a new wire. .. note:: This function is designed for use as part of the circuit cutting workflow. Check out the :func:`qml.cut_circuit() <pennylane.cut_circuit>` transform for more details. Args: graph (nx.MultiDiGraph): directed multigraph to be converted to a tape Returns: QuantumTape: the quantum tape corresponding to the input graph **Example** Consider the following circuit: .. code-block:: python ops = [ qml.RX(0.4, wires=0), qml.RY(0.5, wires=1), qml.CNOT(wires=[0, 1]), qml.qcut.MeasureNode(wires=1), qml.qcut.PrepareNode(wires=1), qml.CNOT(wires=[1, 0]), ] measurements = [qml.expval(qml.Z(0))] tape = qml.tape.QuantumTape(ops, measurements) This circuit contains operations that follow a :class:`~.MeasureNode`. These operations will subsequently act on wire ``2`` instead of wire ``1``: >>> graph = qml.qcut.tape_to_graph(tape) >>> tape = qml.qcut.graph_to_tape(graph) >>> print(tape.draw()) 0: ──RX──────────╭●──────────────╭X─┤ <Z> 1: ──RY──────────╰X──MeasureNode─│──┤ 2: ──PrepareNode─────────────────╰●─┤ """wires=Wires.all_wires([n.obj.wiresforningraph.nodes])ordered_ops=sorted([(order,op.obj)forop,orderingraph.nodes(data="order")],key=lambdax:x[0])wire_map={w:wforwinwires}reverse_wire_map={v:kfork,vinwire_map.items()}copy_ops=[copy.copy(op)for_,opinordered_opsifnotisinstance(op,MeasurementProcess)]copy_meas=[copy.copy(op)for_,opinordered_opsifisinstance(op,MeasurementProcess)]observables=[]operations_from_graph=[]measurements_from_graph=[]foropincopy_ops:op=qml.map_wires(op,wire_map=wire_map,queue=False)operations_from_graph.append(op)ifisinstance(op,MeasureNode):assertlen(op.wires)==1measured_wire=op.wires[0]new_wire=_find_new_wire(wires)wires+=new_wireoriginal_wire=reverse_wire_map[measured_wire]wire_map[original_wire]=new_wirereverse_wire_map[new_wire]=original_wireifcopy_meas:measurement_types={type(meas)formeasincopy_meas}iflen(measurement_types)>1:raiseValueError("Only a single return type can be used for measurement nodes in graph_to_tape")measurement_type=measurement_types.pop()ifmeasurement_typenotin{SampleMP,ExpectationMP}:raiseValueError("Invalid return type. Only expectation value and sampling measurements ""are supported in graph_to_tape")formeasincopy_meas:meas=qml.map_wires(meas,wire_map=wire_map)obs=meas.obsobservables.append(obs)ifmeasurement_typeisSampleMP:measurements_from_graph.append(meas)ifmeasurement_typeisExpectationMP:iflen(observables)>1:measurements_from_graph.append(qml.expval(qml.prod(*observables)))else:measurements_from_graph.append(qml.expval(obs))returnQuantumScript(ops=operations_from_graph,measurements=measurements_from_graph)
def_add_operator_node(graph:MultiDiGraph,op:Operator,order:int,wire_latest_node:dict):""" Helper function to add operators as nodes during tape to graph conversion. """node=WrappedObj(op)graph.add_node(node,order=order)forwireinop.wires:ifwire_latest_node[wire]isnotNone:parent_node=wire_latest_node[wire]graph.add_edge(parent_node,node,wire=wire)wire_latest_node[wire]=nodedef_find_new_wire(wires:Wires)->int:"""Finds a new wire label that is not in ``wires``."""ctr=0whilectrinwires:ctr+=1returnctrdef_create_prep_list():""" Creates a predetermined list for converting PrepareNodes to an associated Operation for use within the expand_fragment_tape function. """def_prep_zero(wire):return[qml.Identity(wire)]def_prep_one(wire):return[qml.X(wire)]def_prep_plus(wire):return[qml.Hadamard(wire)]def_prep_iplus(wire):return[qml.Hadamard(wire),qml.S(wires=wire)]return[_prep_zero,_prep_one,_prep_plus,_prep_iplus]PREPARE_SETTINGS=_create_prep_list()
[docs]defexpand_fragment_tape(tape:QuantumScript,)->tuple[list[QuantumScript],list[PrepareNode],list[MeasureNode]]:""" Expands a fragment tape into a sequence of tapes for each configuration of the contained :class:`MeasureNode` and :class:`PrepareNode` operations. .. note:: This function is designed for use as part of the circuit cutting workflow. Check out the :func:`qml.cut_circuit() <pennylane.cut_circuit>` transform for more details. Args: tape (QuantumTape): the fragment tape containing :class:`MeasureNode` and :class:`PrepareNode` operations to be expanded Returns: Tuple[List[QuantumTape], List[PrepareNode], List[MeasureNode]]: the tapes corresponding to each configuration and the order of preparation nodes and measurement nodes used in the expansion **Example** Consider the following circuit, which contains a :class:`~.MeasureNode` and :class:`~.PrepareNode` operation: .. code-block:: python ops = [ qml.qcut.PrepareNode(wires=0), qml.RX(0.5, wires=0), qml.qcut.MeasureNode(wires=0), ] tape = qml.tape.QuantumTape(ops) We can expand over the measurement and preparation nodes using: >>> tapes, prep, meas = qml.qcut.expand_fragment_tape(tape) >>> for t in tapes: ... print(qml.drawer.tape_text(t, decimals=1)) 0: ──I──RX(0.5)─┤ <I> <Z> 0: ──I──RX(0.5)─┤ <X> 0: ──I──RX(0.5)─┤ <Y> 0: ──X──RX(0.5)─┤ <I> <Z> 0: ──X──RX(0.5)─┤ <X> 0: ──X──RX(0.5)─┤ <Y> 0: ──H──RX(0.5)─┤ <I> <Z> 0: ──H──RX(0.5)─┤ <X> 0: ──H──RX(0.5)─┤ <Y> 0: ──H──S──RX(0.5)─┤ <I> <Z> 0: ──H──S──RX(0.5)─┤ <X> 0: ──H──S──RX(0.5)─┤ <Y> """prepare_nodes=[oforointape.operationsifisinstance(o,PrepareNode)]measure_nodes=[oforointape.operationsifisinstance(o,MeasureNode)]wire_map={mn.wires[0]:ifori,mninenumerate(measure_nodes)}n_meas=len(measure_nodes)ifn_meas>=1:measure_combinations=qml.pauli.partition_pauli_group(len(measure_nodes))else:measure_combinations=[[""]]tapes=[]forprepare_settingsinproduct(range(len(PREPARE_SETTINGS)),repeat=len(prepare_nodes)):formeasure_groupinmeasure_combinations:ifn_meas>=1:group=[string_to_pauli_word(paulis,wire_map=wire_map)forpaulisinmeasure_group]else:group=[]prepare_mapping={id(n):PREPARE_SETTINGS[s]forn,sinzip(prepare_nodes,prepare_settings)}ops_list=[]withqml.QueuingManager.stop_recording():foropintape.operations:ifisinstance(op,PrepareNode):w=op.wires[0]ops_list.extend(prepare_mapping[id(op)](w))elifnotisinstance(op,MeasureNode):ops_list.append(op)measurements=_get_measurements(group,tape.measurements)qs=qml.tape.QuantumScript(ops=ops_list,measurements=measurements)tapes.append(qs)returntapes,prepare_nodes,measure_nodes
def_get_measurements(group:Sequence[Operator],measurements:Sequence[MeasurementProcess])->list[MeasurementProcess]:"""Pairs each observable in ``group`` with the circuit ``measurements``. Only a single measurement of an expectation value is currently supported in ``measurements``. Args: group (Sequence[Operator]): a collection of observables measurements (Sequence[MeasurementProcess]): measurements from the circuit Returns: List[MeasurementProcess]: the expectation values of ``g @ obs``, where ``g`` is iterated over ``group`` and ``obs`` is the observable composing the single measurement in ``measurements`` """iflen(group)==0:# This ensures the measurements of the original tape are carried over to the# following tape configurations in the absence of any MeasureNodes in the fragmentreturnmeasurementsn_measurements=len(measurements)ifn_measurements>1:raiseValueError("The circuit cutting workflow only supports circuits with a single output ""measurement")ifn_measurements==0:return[expval(g)forgingroup]measurement=measurements[0]ifnotisinstance(measurement,ExpectationMP):raiseValueError("The circuit cutting workflow only supports circuits with expectation ""value measurements")obs=measurement.obsreturn[expval(copy.copy(obs)@g)forgingroup]def_qcut_expand_fn(tape:QuantumScript,max_depth:int=1,auto_cutter:Union[bool,Callable]=False,):"""Expansion function for circuit cutting. Expands operations until reaching a depth that includes :class:`~.WireCut` operations. """foropintape.operations:ifisinstance(op,WireCut):returntapeifmax_depth>0:return_qcut_expand_fn(tape.expand(),max_depth=max_depth-1,auto_cutter=auto_cutter)ifnot(auto_cutterisTrueorcallable(auto_cutter)):raiseValueError("No WireCut operations found in the circuit. Consider increasing the max_depth value if ""operations or nested tapes contain WireCut operations.")returntape