12
12
13
13
"""Optimize annotated operations on a circuit."""
14
14
15
- from typing import Optional , List , Tuple
15
+ from typing import Optional , List , Tuple , Union
16
16
17
17
from qiskit .circuit .controlflow import CONTROL_FLOW_OP_NAMES
18
18
from qiskit .converters import circuit_to_dag , dag_to_circuit
19
19
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
+ )
21
28
from qiskit .transpiler .basepasses import TransformationPass
22
29
from qiskit .transpiler .passes .utils import control_flow
23
30
from qiskit .transpiler .target import Target
@@ -43,6 +50,11 @@ class OptimizeAnnotated(TransformationPass):
43
50
``g2 = AnnotatedOperation(g1, ControlModifier(2))``, then ``g2`` can be replaced with
44
51
``AnnotatedOperation(SwapGate(), [InverseModifier(), ControlModifier(2)])``.
45
52
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
+
46
58
"""
47
59
48
60
def __init__ (
@@ -51,6 +63,7 @@ def __init__(
51
63
equivalence_library : Optional [EquivalenceLibrary ] = None ,
52
64
basis_gates : Optional [List [str ]] = None ,
53
65
recurse : bool = True ,
66
+ do_conjugate_reduction : bool = True ,
54
67
):
55
68
"""
56
69
OptimizeAnnotated initializer.
@@ -67,12 +80,14 @@ def __init__(
67
80
not applied when neither is specified since such objects do not need to
68
81
be synthesized). Setting this value to ``False`` precludes the recursion in
69
82
every case.
83
+ do_conjugate_reduction: controls whether conjugate reduction should be performed.
70
84
"""
71
85
super ().__init__ ()
72
86
73
87
self ._target = target
74
88
self ._equiv_lib = equivalence_library
75
89
self ._basis_gates = basis_gates
90
+ self ._do_conjugate_reduction = do_conjugate_reduction
76
91
77
92
self ._top_level_only = not recurse or (self ._basis_gates is None and self ._target is None )
78
93
@@ -122,7 +137,11 @@ def _run_inner(self, dag) -> Tuple[DAGCircuit, bool]:
122
137
# as they may remove annotated gates.
123
138
dag , opt2 = self ._recurse (dag )
124
139
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
126
145
127
146
def _canonicalize (self , dag ) -> Tuple [DAGCircuit , bool ]:
128
147
"""
@@ -148,17 +167,219 @@ def _canonicalize(self, dag) -> Tuple[DAGCircuit, bool]:
148
167
did_something = True
149
168
return dag , did_something
150
169
151
- def _recursively_process_definitions (self , op : Operation ) -> bool :
170
+ def _conjugate_decomposition (
171
+ self , dag : DAGCircuit
172
+ ) -> Union [Tuple [DAGCircuit , DAGCircuit , DAGCircuit ], None ]:
152
173
"""
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.
156
179
"""
157
180
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.
161
364
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
+ """
162
383
# Similar to HighLevelSynthesis transpiler pass, we do not recurse into a gate's
163
384
# `definition` for a gate that is supported by the target or in equivalence library.
164
385
@@ -170,7 +391,22 @@ def _recursively_process_definitions(self, op: Operation) -> bool:
170
391
else op .name in self ._device_insts
171
392
)
172
393
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
174
410
175
411
try :
176
412
# extract definition
0 commit comments