13
13
"""Layout selection using the SABRE bidirectional search approach from Li et al.
14
14
"""
15
15
16
+ from collections import defaultdict
16
17
import copy
17
18
import logging
19
+ import time
20
+
18
21
import numpy as np
19
22
import rustworkx as rx
20
23
24
27
from qiskit .transpiler .passes .layout .enlarge_with_ancilla import EnlargeWithAncilla
25
28
from qiskit .transpiler .passes .layout .apply_layout import ApplyLayout
26
29
from qiskit .transpiler .passes .layout import disjoint_utils
30
+ from qiskit .transpiler .passes .layout import vf2_utils
27
31
from qiskit .transpiler .passmanager import PassManager
28
32
from qiskit .transpiler .layout import Layout
29
33
from qiskit .transpiler .basepasses import TransformationPass
38
42
from qiskit .transpiler .target import Target
39
43
from qiskit .transpiler .coupling import CouplingMap
40
44
from qiskit .tools .parallel import CPU_COUNT
45
+ from qiskit .circuit .controlflow import ControlFlowOp , ForLoopOp
46
+ from qiskit .converters import circuit_to_dag
41
47
42
48
logger = logging .getLogger (__name__ )
43
49
44
50
45
51
class SabreLayout (TransformationPass ):
46
52
"""Choose a Layout via iterative bidirectional routing of the input circuit.
47
53
48
- Starting with a random initial `Layout`, the algorithm does a full routing
49
- of the circuit (via the `routing_pass` method) to end up with a
50
- `final_layout`. This final_layout is then used as the initial_layout for
51
- routing the reverse circuit. The algorithm iterates a number of times until
52
- it finds an initial_layout that reduces full routing cost.
54
+ The algorithm does a full routing of the circuit (via the `routing_pass`
55
+ method) to end up with a `final_layout`. This final_layout is then used as
56
+ the initial_layout for routing the reverse circuit. The algorithm iterates a
57
+ number of times until it finds an initial_layout that reduces full routing cost.
58
+
59
+ Prior to running the SABRE algorithm this transpiler pass will try to find the layout
60
+ for deepest layer that is has an isomorphic subgraph in the coupling graph. This is
61
+ done by progressively using the algorithm from :class:`~.VF2Layout` on the circuit
62
+ until a mapping is not found. This partial layout is then used to seed the SABRE algorithm
63
+ and then random physical bits are selected for the remaining elements in the mapping.
53
64
54
65
This method exploits the reversibility of quantum circuits, and tries to
55
66
include global circuit information in the choice of initial_layout.
@@ -85,6 +96,10 @@ def __init__(
85
96
swap_trials = None ,
86
97
layout_trials = None ,
87
98
skip_routing = False ,
99
+ vf2_partial_layout = True ,
100
+ vf2_call_limit = None ,
101
+ vf2_time_limit = None ,
102
+ vf2_max_trials = None ,
88
103
):
89
104
"""SabreLayout initializer.
90
105
@@ -121,6 +136,16 @@ def __init__(
121
136
will be returned in the property set. This is a tradeoff to run custom
122
137
routing with multiple layout trials, as using this option will cause
123
138
SabreLayout to run the routing stage internally but not use that result.
139
+ vf2_partial_layout (bool): Run vf2 partial layout
140
+ vf2_call_limit (int): The number of state visits to attempt in each execution of
141
+ VF2 to attempt to find a partial layout.
142
+ vf2_time_limit (float): The total time limit in seconds to run VF2 to find a partial
143
+ layout
144
+ vf2_max_trials (int): The maximum number of trials to run VF2 to find
145
+ a partial layout. If this is not specified the number of trials will be limited
146
+ based on the number of edges in the interaction graph or the coupling graph
147
+ (whichever is larger) if no other limits are set. If set to a value <= 0 no
148
+ limit on the number of trials will be set.
124
149
125
150
Raises:
126
151
TranspilerError: If both ``routing_pass`` and ``swap_trials`` or
@@ -158,6 +183,11 @@ def __init__(
158
183
self .coupling_map = copy .deepcopy (self .coupling_map )
159
184
self .coupling_map .make_symmetric ()
160
185
self ._neighbor_table = NeighborTable (rx .adjacency_matrix (self .coupling_map .graph ))
186
+ self .avg_error_map = None
187
+ self .vf2_partial_layout = vf2_partial_layout
188
+ self .call_limit = vf2_call_limit
189
+ self .time_limit = vf2_time_limit
190
+ self .max_trials = vf2_max_trials
161
191
162
192
def run (self , dag ):
163
193
"""Run the SabreLayout pass on `dag`.
@@ -321,6 +351,13 @@ def _inner_run(self, dag, coupling_map):
321
351
cargs ,
322
352
)
323
353
)
354
+ partial_layout = None
355
+ if self .vf2_partial_layout :
356
+ partial_layout_virtual_bits = self ._vf2_partial_layout (
357
+ dag , coupling_map
358
+ ).get_virtual_bits ()
359
+ partial_layout = [partial_layout_virtual_bits .get (i , None ) for i in dag .qubits ]
360
+
324
361
((initial_layout , final_layout ), swap_map , gate_order ) = sabre_layout_and_routing (
325
362
len (dag .clbits ),
326
363
dag_list ,
@@ -331,6 +368,7 @@ def _inner_run(self, dag, coupling_map):
331
368
self .swap_trials ,
332
369
self .layout_trials ,
333
370
self .seed ,
371
+ partial_layout ,
334
372
)
335
373
# Apply initial layout selected.
336
374
layout_dict = {}
@@ -385,3 +423,173 @@ def _compose_layouts(self, initial_layout, pass_final_layout, qregs):
385
423
qubit_map = Layout .combine_into_edge_map (initial_layout , trivial_layout )
386
424
final_layout = {v : pass_final_layout ._v2p [qubit_map [v ]] for v in initial_layout ._v2p }
387
425
return Layout (final_layout )
426
+
427
+ # TODO: Migrate this to rust as part of sabre_layout.rs after
428
+ # https://github.com/Qiskit/rustworkx/issues/741 is implemented and released
429
+ def _vf2_partial_layout (self , dag , coupling_map ):
430
+ """Find a partial layout using vf2 on the deepest subgraph that is isomorphic to
431
+ the coupling graph."""
432
+ im_graph_node_map = {}
433
+ reverse_im_graph_node_map = {}
434
+ im_graph = rx .PyGraph (multigraph = False )
435
+ logger .debug ("Buidling interaction graphs" )
436
+ largest_im_graph = None
437
+ best_mapping = None
438
+ first_mapping = None
439
+ if self .avg_error_map is None :
440
+ self .avg_error_map = vf2_utils .build_average_error_map (self .target , None , coupling_map )
441
+
442
+ cm_graph , cm_nodes = vf2_utils .shuffle_coupling_graph (coupling_map , self .seed , False )
443
+ # To avoid trying to over optimize the result by default limit the number
444
+ # of trials based on the size of the graphs. For circuits with simple layouts
445
+ # like an all 1q circuit we don't want to sit forever trying every possible
446
+ # mapping in the search space if no other limits are set
447
+ if self .max_trials is None and self .call_limit is None and self .time_limit is None :
448
+ im_graph_edge_count = len (im_graph .edge_list ())
449
+ cm_graph_edge_count = len (coupling_map .graph .edge_list ())
450
+ self .max_trials = max (im_graph_edge_count , cm_graph_edge_count ) + 15
451
+
452
+ start_time = time .time ()
453
+
454
+ # A more efficient search pattern would be to do a binary search
455
+ # and find, but to conserve memory and avoid a large number of
456
+ # unecessary graphs this searchs from the beginning and continues
457
+ # until there is no vf2 match
458
+ def _visit (dag , weight , wire_map ):
459
+ for node in dag .topological_op_nodes ():
460
+ nonlocal largest_im_graph
461
+ largest_im_graph = im_graph .copy ()
462
+ if getattr (node .op , "_directive" , False ):
463
+ continue
464
+ if isinstance (node .op , ControlFlowOp ):
465
+ if isinstance (node .op , ForLoopOp ):
466
+ inner_weight = len (node .op .params [0 ]) * weight
467
+ else :
468
+ inner_weight = weight
469
+ for block in node .op .blocks :
470
+ inner_wire_map = {
471
+ inner : wire_map [outer ] for outer , inner in zip (node .qargs , block .qubits )
472
+ }
473
+ _visit (circuit_to_dag (block ), inner_weight , inner_wire_map )
474
+ continue
475
+ len_args = len (node .qargs )
476
+ qargs = [wire_map [q ] for q in node .qargs ]
477
+ if len_args == 1 :
478
+ if qargs [0 ] not in im_graph_node_map :
479
+ weights = defaultdict (int )
480
+ weights [node .name ] += weight
481
+ im_graph_node_map [qargs [0 ]] = im_graph .add_node (weights )
482
+ reverse_im_graph_node_map [im_graph_node_map [qargs [0 ]]] = qargs [0 ]
483
+ else :
484
+ im_graph [im_graph_node_map [qargs [0 ]]][node .op .name ] += weight
485
+ if len_args == 2 :
486
+ if qargs [0 ] not in im_graph_node_map :
487
+ im_graph_node_map [qargs [0 ]] = im_graph .add_node (defaultdict (int ))
488
+ reverse_im_graph_node_map [im_graph_node_map [qargs [0 ]]] = qargs [0 ]
489
+ if qargs [1 ] not in im_graph_node_map :
490
+ im_graph_node_map [qargs [1 ]] = im_graph .add_node (defaultdict (int ))
491
+ reverse_im_graph_node_map [im_graph_node_map [qargs [1 ]]] = qargs [1 ]
492
+ edge = (im_graph_node_map [qargs [0 ]], im_graph_node_map [qargs [1 ]])
493
+ if im_graph .has_edge (* edge ):
494
+ im_graph .get_edge_data (* edge )[node .name ] += weight
495
+ else :
496
+ weights = defaultdict (int )
497
+ weights [node .name ] += weight
498
+ im_graph .add_edge (* edge , weights )
499
+ if len_args > 2 :
500
+ raise TranspilerError (
501
+ "Encountered an instruction operating on more than 2 qubits, this pass "
502
+ "only functions with 1 or 2 qubit operations."
503
+ )
504
+ vf2_mapping = rx .vf2_mapping (
505
+ cm_graph ,
506
+ im_graph ,
507
+ subgraph = True ,
508
+ id_order = False ,
509
+ induced = False ,
510
+ call_limit = self .call_limit ,
511
+ )
512
+ try :
513
+ nonlocal first_mapping
514
+ first_mapping = next (vf2_mapping )
515
+ except StopIteration :
516
+ break
517
+ nonlocal best_mapping
518
+ best_mapping = vf2_mapping
519
+ elapsed_time = time .time () - start_time
520
+ if (
521
+ self .time_limit is not None
522
+ and best_mapping is not None
523
+ and elapsed_time >= self .time_limit
524
+ ):
525
+ logger .debug (
526
+ "SabreLayout VF2 heuristic has taken %s which exceeds configured max time: %s" ,
527
+ elapsed_time ,
528
+ self .time_limit ,
529
+ )
530
+ break
531
+
532
+ _visit (dag , 1 , {bit : bit for bit in dag .qubits })
533
+ logger .debug ("Finding best mappings of largest partial subgraph" )
534
+ im_graph = largest_im_graph
535
+
536
+ def mapping_to_layout (layout_mapping ):
537
+ return Layout ({reverse_im_graph_node_map [k ]: v for k , v in layout_mapping .items ()})
538
+
539
+ layout_mapping = {im_i : cm_nodes [cm_i ] for cm_i , im_i in first_mapping .items ()}
540
+ chosen_layout = mapping_to_layout (layout_mapping )
541
+ chosen_layout_score = vf2_utils .score_layout (
542
+ self .avg_error_map ,
543
+ layout_mapping ,
544
+ im_graph_node_map ,
545
+ reverse_im_graph_node_map ,
546
+ im_graph ,
547
+ False ,
548
+ )
549
+ trials = 1
550
+ for mapping in best_mapping : # pylint: disable=not-an-iterable
551
+ trials += 1
552
+ logger .debug ("Running trial: %s" , trials )
553
+ layout_mapping = {im_i : cm_nodes [cm_i ] for cm_i , im_i in mapping .items ()}
554
+ # If the graphs have the same number of nodes we don't need to score or do multiple
555
+ # trials as the score heuristic currently doesn't weigh nodes based on gates on a
556
+ # qubit so the scores will always all be the same
557
+ if len (cm_graph ) == len (im_graph ):
558
+ break
559
+ layout_score = vf2_utils .score_layout (
560
+ self .avg_error_map ,
561
+ layout_mapping ,
562
+ im_graph_node_map ,
563
+ reverse_im_graph_node_map ,
564
+ im_graph ,
565
+ False ,
566
+ )
567
+ logger .debug ("Trial %s has score %s" , trials , layout_score )
568
+ if chosen_layout is None :
569
+ chosen_layout = mapping_to_layout (layout_mapping )
570
+ chosen_layout_score = layout_score
571
+ elif layout_score < chosen_layout_score :
572
+ layout = mapping_to_layout (layout_mapping )
573
+ logger .debug (
574
+ "Found layout %s has a lower score (%s) than previous best %s (%s)" ,
575
+ layout ,
576
+ layout_score ,
577
+ chosen_layout ,
578
+ chosen_layout_score ,
579
+ )
580
+ chosen_layout = layout
581
+ chosen_layout_score = layout_score
582
+ if self .max_trials and trials >= self .max_trials :
583
+ logger .debug ("Trial %s is >= configured max trials %s" , trials , self .max_trials )
584
+ break
585
+ elapsed_time = time .time () - start_time
586
+ if self .time_limit is not None and elapsed_time >= self .time_limit :
587
+ logger .debug (
588
+ "VF2Layout has taken %s which exceeds configured max time: %s" ,
589
+ elapsed_time ,
590
+ self .time_limit ,
591
+ )
592
+ break
593
+ for reg in dag .qregs .values ():
594
+ chosen_layout .add_register (reg )
595
+ return chosen_layout
0 commit comments