Skip to content

Commit bb1ef16

Browse files
Conjugate reduction in optimize annotated pass (#11811)
* initial commit for the conjugate reduction optimization * do not go into definitions when unnecessary * release notes * typo * minor * rewriting as list comprehension * improving release notes * removing print statement * changing op_predecessors and op_successor methods to return iterators rather than lists * and removing explcit iter * constructing the optimized circuit using compose rather than append * improving variable names * adding test * adding tests exploring which gates get collected * more renaming --------- Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
1 parent f34fb21 commit bb1ef16

File tree

4 files changed

+504
-12
lines changed

4 files changed

+504
-12
lines changed

qiskit/dagcircuit/dagcircuit.py

+8
Original file line numberDiff line numberDiff line change
@@ -1879,6 +1879,14 @@ def predecessors(self, node):
18791879
"""Returns iterator of the predecessors of a node as DAGOpNodes and DAGInNodes."""
18801880
return iter(self._multi_graph.predecessors(node._node_id))
18811881

1882+
def op_successors(self, node):
1883+
"""Returns iterator of "op" successors of a node in the dag."""
1884+
return (succ for succ in self.successors(node) if isinstance(succ, DAGOpNode))
1885+
1886+
def op_predecessors(self, node):
1887+
"""Returns the iterator of "op" predecessors of a node in the dag."""
1888+
return (pred for pred in self.predecessors(node) if isinstance(pred, DAGOpNode))
1889+
18821890
def is_successor(self, node, node_succ):
18831891
"""Checks if a second node is in the successors of node."""
18841892
return self._multi_graph.has_edge(node._node_id, node_succ._node_id)

qiskit/transpiler/passes/optimization/optimize_annotated.py

+247-11
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,19 @@
1212

1313
"""Optimize annotated operations on a circuit."""
1414

15-
from typing import Optional, List, Tuple
15+
from typing import Optional, List, Tuple, Union
1616

1717
from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES
1818
from qiskit.converters import circuit_to_dag, dag_to_circuit
1919
from qiskit.circuit.annotated_operation import AnnotatedOperation, _canonicalize_modifiers
20-
from qiskit.circuit import EquivalenceLibrary, ControlledGate, Operation, ControlFlowOp
20+
from qiskit.circuit import (
21+
QuantumCircuit,
22+
Instruction,
23+
EquivalenceLibrary,
24+
ControlledGate,
25+
Operation,
26+
ControlFlowOp,
27+
)
2128
from qiskit.transpiler.basepasses import TransformationPass
2229
from qiskit.transpiler.passes.utils import control_flow
2330
from qiskit.transpiler.target import Target
@@ -43,6 +50,11 @@ class OptimizeAnnotated(TransformationPass):
4350
``g2 = AnnotatedOperation(g1, ControlModifier(2))``, then ``g2`` can be replaced with
4451
``AnnotatedOperation(SwapGate(), [InverseModifier(), ControlModifier(2)])``.
4552
53+
* Applies conjugate reduction to annotated operations. As an example,
54+
``control - [P -- Q -- P^{-1}]`` can be rewritten as ``P -- control - [Q] -- P^{-1}``,
55+
that is, only the middle part needs to be controlled. This also works for inverse
56+
and power modifiers.
57+
4658
"""
4759

4860
def __init__(
@@ -51,6 +63,7 @@ def __init__(
5163
equivalence_library: Optional[EquivalenceLibrary] = None,
5264
basis_gates: Optional[List[str]] = None,
5365
recurse: bool = True,
66+
do_conjugate_reduction: bool = True,
5467
):
5568
"""
5669
OptimizeAnnotated initializer.
@@ -67,12 +80,14 @@ def __init__(
6780
not applied when neither is specified since such objects do not need to
6881
be synthesized). Setting this value to ``False`` precludes the recursion in
6982
every case.
83+
do_conjugate_reduction: controls whether conjugate reduction should be performed.
7084
"""
7185
super().__init__()
7286

7387
self._target = target
7488
self._equiv_lib = equivalence_library
7589
self._basis_gates = basis_gates
90+
self._do_conjugate_reduction = do_conjugate_reduction
7691

7792
self._top_level_only = not recurse or (self._basis_gates is None and self._target is None)
7893

@@ -122,7 +137,11 @@ def _run_inner(self, dag) -> Tuple[DAGCircuit, bool]:
122137
# as they may remove annotated gates.
123138
dag, opt2 = self._recurse(dag)
124139

125-
return dag, opt1 or opt2
140+
opt3 = False
141+
if not self._top_level_only and self._do_conjugate_reduction:
142+
dag, opt3 = self._conjugate_reduction(dag)
143+
144+
return dag, opt1 or opt2 or opt3
126145

127146
def _canonicalize(self, dag) -> Tuple[DAGCircuit, bool]:
128147
"""
@@ -148,17 +167,219 @@ def _canonicalize(self, dag) -> Tuple[DAGCircuit, bool]:
148167
did_something = True
149168
return dag, did_something
150169

151-
def _recursively_process_definitions(self, op: Operation) -> bool:
170+
def _conjugate_decomposition(
171+
self, dag: DAGCircuit
172+
) -> Union[Tuple[DAGCircuit, DAGCircuit, DAGCircuit], None]:
152173
"""
153-
Recursively applies optimizations to op's definition (or to op.base_op's
154-
definition if op is an annotated operation).
155-
Returns True if did something.
174+
Decomposes a circuit ``A`` into 3 sub-circuits ``P``, ``Q``, ``R`` such that
175+
``A = P -- Q -- R`` and ``R = P^{-1}``.
176+
177+
This is accomplished by iteratively finding inverse nodes at the front and at the back of the
178+
circuit.
156179
"""
157180

158-
# If op is an annotated operation, we descend into its base_op
159-
if isinstance(op, AnnotatedOperation):
160-
return self._recursively_process_definitions(op.base_op)
181+
front_block = [] # nodes collected from the front of the circuit (aka P)
182+
back_block = [] # nodes collected from the back of the circuit (aka R)
183+
184+
# Stores in- and out- degree for each node. These degrees are computed at the start of this
185+
# function and are updated when nodes are collected into front_block or into back_block.
186+
in_degree = {}
187+
out_degree = {}
188+
189+
# We use dicts to track for each qubit a DAG node at the front of the circuit that involves
190+
# this qubit and a DAG node at the end of the circuit that involves this qubit (when exist).
191+
# Note that for the DAGCircuit structure for each qubit there can be at most one such front
192+
# and such back node.
193+
# This allows for an efficient way to find an inverse pair of gates (one from the front and
194+
# one from the back of the circuit).
195+
# A qubit that was never examined does not appear in these dicts, and a qubit that was examined
196+
# but currently is not involved at the front (resp. at the back) of the circuit has the value of
197+
# None.
198+
front_node_for_qubit = {}
199+
back_node_for_qubit = {}
200+
201+
# Keep the set of nodes that have been moved either to front_block or to back_block
202+
processed_nodes = set()
203+
204+
# Keep the set of qubits that are involved in nodes at the front or at the back of the circuit.
205+
# When looking for inverse pairs of gates we will only iterate over these qubits.
206+
active_qubits = set()
207+
208+
# Keep pairs of nodes for which the inverse check was performed and the nodes
209+
# were found to be not inverse to each other (memoization).
210+
checked_node_pairs = set()
211+
212+
# compute in- and out- degree for every node
213+
# also update information for nodes at the start and at the end of the circuit
214+
for node in dag.op_nodes():
215+
preds = list(dag.op_predecessors(node))
216+
in_degree[node] = len(preds)
217+
if len(preds) == 0:
218+
for q in node.qargs:
219+
front_node_for_qubit[q] = node
220+
active_qubits.add(q)
221+
succs = list(dag.op_successors(node))
222+
out_degree[node] = len(succs)
223+
if len(succs) == 0:
224+
for q in node.qargs:
225+
back_node_for_qubit[q] = node
226+
active_qubits.add(q)
227+
228+
# iterate while there is a possibility to find more inverse pairs
229+
while len(active_qubits) > 0:
230+
to_check = active_qubits.copy()
231+
active_qubits.clear()
232+
233+
# For each qubit q, check whether the gate at the front of the circuit that involves q
234+
# and the gate at the end of the circuit that involves q are inverse
235+
for q in to_check:
236+
237+
if (front_node := front_node_for_qubit.get(q, None)) is None:
238+
continue
239+
if (back_node := back_node_for_qubit.get(q, None)) is None:
240+
continue
241+
242+
# front_node or back_node could be already collected when considering other qubits
243+
if front_node in processed_nodes or back_node in processed_nodes:
244+
continue
245+
246+
# it is possible that the same node is both at the front and at the back,
247+
# it should not be collected
248+
if front_node == back_node:
249+
continue
250+
251+
# have been checked before
252+
if (front_node, back_node) in checked_node_pairs:
253+
continue
254+
255+
# fast check based on the arguments
256+
if front_node.qargs != back_node.qargs or front_node.cargs != back_node.cargs:
257+
continue
258+
259+
# in the future we want to include a more precise check whether a pair
260+
# of nodes are inverse
261+
if front_node.op == back_node.op.inverse():
262+
# update front_node_for_qubit and back_node_for_qubit
263+
for q in front_node.qargs:
264+
front_node_for_qubit[q] = None
265+
for q in back_node.qargs:
266+
back_node_for_qubit[q] = None
267+
268+
# see which other nodes become at the front and update information
269+
for node in dag.op_successors(front_node):
270+
if node not in processed_nodes:
271+
in_degree[node] -= 1
272+
if in_degree[node] == 0:
273+
for q in node.qargs:
274+
front_node_for_qubit[q] = node
275+
active_qubits.add(q)
276+
277+
# see which other nodes become at the back and update information
278+
for node in dag.op_predecessors(back_node):
279+
if node not in processed_nodes:
280+
out_degree[node] -= 1
281+
if out_degree[node] == 0:
282+
for q in node.qargs:
283+
back_node_for_qubit[q] = node
284+
active_qubits.add(q)
285+
286+
# collect and mark as processed
287+
front_block.append(front_node)
288+
back_block.append(back_node)
289+
processed_nodes.add(front_node)
290+
processed_nodes.add(back_node)
291+
292+
else:
293+
checked_node_pairs.add((front_node, back_node))
294+
295+
# if nothing is found, return None
296+
if len(front_block) == 0:
297+
return None
298+
299+
# create the output DAGs
300+
front_circuit = dag.copy_empty_like()
301+
middle_circuit = dag.copy_empty_like()
302+
back_circuit = dag.copy_empty_like()
303+
front_circuit.global_phase = 0
304+
back_circuit.global_phase = 0
305+
306+
for node in front_block:
307+
front_circuit.apply_operation_back(node.op, node.qargs, node.cargs)
308+
309+
for node in back_block:
310+
back_circuit.apply_operation_front(node.op, node.qargs, node.cargs)
311+
312+
for node in dag.op_nodes():
313+
if node not in processed_nodes:
314+
middle_circuit.apply_operation_back(node.op, node.qargs, node.cargs)
315+
316+
return front_circuit, middle_circuit, back_circuit
317+
318+
def _conjugate_reduce_op(
319+
self, op: AnnotatedOperation, base_decomposition: Tuple[DAGCircuit, DAGCircuit, DAGCircuit]
320+
) -> Operation:
321+
"""
322+
We are given an annotated-operation ``op = M [ B ]`` (where ``B`` is the base operation and
323+
``M`` is the list of modifiers) and the "conjugate decomposition" of the definition of ``B``,
324+
i.e. ``B = P * Q * R``, with ``R = P^{-1}`` (with ``P``, ``Q`` and ``R`` represented as
325+
``DAGCircuit`` objects).
326+
327+
Let ``IQ`` denote a new custom instruction with definitions ``Q``.
328+
329+
We return the operation ``op_new`` which a new custom instruction with definition
330+
``P * A * R``, where ``A`` is a new annotated-operation with modifiers ``M`` and
331+
base gate ``IQ``.
332+
"""
333+
p_dag, q_dag, r_dag = base_decomposition
334+
335+
q_instr = Instruction(
336+
name="iq", num_qubits=op.base_op.num_qubits, num_clbits=op.base_op.num_clbits, params=[]
337+
)
338+
q_instr.definition = dag_to_circuit(q_dag)
339+
340+
op_new = Instruction(
341+
"optimized", num_qubits=op.num_qubits, num_clbits=op.num_clbits, params=[]
342+
)
343+
num_control_qubits = op.num_qubits - op.base_op.num_qubits
344+
345+
circ = QuantumCircuit(op.num_qubits, op.num_clbits)
346+
qubits = circ.qubits
347+
circ.compose(
348+
dag_to_circuit(p_dag), qubits[num_control_qubits : op.num_qubits], inplace=True
349+
)
350+
circ.append(
351+
AnnotatedOperation(base_op=q_instr, modifiers=op.modifiers), range(op.num_qubits)
352+
)
353+
circ.compose(
354+
dag_to_circuit(r_dag), qubits[num_control_qubits : op.num_qubits], inplace=True
355+
)
356+
op_new.definition = circ
357+
return op_new
358+
359+
def _conjugate_reduction(self, dag) -> Tuple[DAGCircuit, bool]:
360+
"""
361+
Looks for annotated operations whose base operation has a nontrivial conjugate decomposition.
362+
In such cases, the modifiers of the annotated operation can be moved to the "middle" part of
363+
the decomposition.
161364
365+
Returns the modified DAG and whether it did something.
366+
"""
367+
did_something = False
368+
for node in dag.op_nodes(op=AnnotatedOperation):
369+
base_op = node.op.base_op
370+
if not self._skip_definition(base_op):
371+
base_dag = circuit_to_dag(base_op.definition, copy_operations=False)
372+
base_decomposition = self._conjugate_decomposition(base_dag)
373+
if base_decomposition is not None:
374+
new_op = self._conjugate_reduce_op(node.op, base_decomposition)
375+
dag.substitute_node(node, new_op)
376+
did_something = True
377+
return dag, did_something
378+
379+
def _skip_definition(self, op: Operation) -> bool:
380+
"""
381+
Returns True if we should not recurse into a gate's definition.
382+
"""
162383
# Similar to HighLevelSynthesis transpiler pass, we do not recurse into a gate's
163384
# `definition` for a gate that is supported by the target or in equivalence library.
164385

@@ -170,7 +391,22 @@ def _recursively_process_definitions(self, op: Operation) -> bool:
170391
else op.name in self._device_insts
171392
)
172393
if inst_supported or (self._equiv_lib is not None and self._equiv_lib.has_entry(op)):
173-
return False
394+
return True
395+
return False
396+
397+
def _recursively_process_definitions(self, op: Operation) -> bool:
398+
"""
399+
Recursively applies optimizations to op's definition (or to op.base_op's
400+
definition if op is an annotated operation).
401+
Returns True if did something.
402+
"""
403+
404+
# If op is an annotated operation, we descend into its base_op
405+
if isinstance(op, AnnotatedOperation):
406+
return self._recursively_process_definitions(op.base_op)
407+
408+
if self._skip_definition(op):
409+
return False
174410

175411
try:
176412
# extract definition
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
features:
2+
- |
3+
Added a new reduction to the :class:`.OptimizeAnnotated` transpiler pass.
4+
This reduction looks for annotated operations (objects of type :class:`.AnnotatedOperation`
5+
that consist of a base operation ``B`` and a list ``M`` of control, inverse and power
6+
modifiers) with the following properties:
7+
8+
* the base operation ``B`` needs to be synthesized (i.e. it's not already supported
9+
by the target or belongs to the equivalence library)
10+
11+
* the definition circuit for ``B`` can be expressed as ``P -- Q -- R`` with :math:`R = P^{-1}`
12+
13+
In this case the modifiers can be moved to the ``Q``-part only. As a specific example,
14+
controlled QFT-based adders have the form ``control - [QFT -- U -- IQFT]``, which can be
15+
simplified to ``QFT -- control-[U] -- IQFT``. By removing the controls over ``QFT`` and
16+
``IQFT`` parts of the circuit, one obtains significantly fewer gates in the transpiled
17+
circuit.
18+
- |
19+
Added two new methods to the :class:`~qiskit.dagcircuit.DAGCircuit` class:
20+
:meth:`qiskit.dagcircuit.DAGCircuit.op_successors` returns an iterator to
21+
:class:`.DAGOpNode` successors of a node, and
22+
:meth:`qiskit.dagcircuit.DAGCircuit.op_successors` returns an iterator to
23+
:class:`.DAGOpNode` predecessors of a node.
24+

0 commit comments

Comments
 (0)