Skip to content

Commit ad07847

Browse files
authored
Fix transpile() for control flow operations with a Target/BackendV2 (#8852)
* Fix transpile() for control flow operations with a Target/BackendV2 This commit fixes an issue when compiling with circuits that have control flow operations and are running transpile() with a BackendV2 instance or a custom Target. In these cases the transpile() operation would fail to run because the Target didn't have a provision to recognize a global variable width operation as part of the target. Previously all operations in the target needed to have an instance of an Operation so that the parameters and number of qubits could be verified. Each of these operation instances would be assigned a unique name. But, for control flow operations they're defined over a variable number of qubits and all have the same name (this is a similar problem for gates like mcx too). Not being able to fully represent the control flow operations in a target was preventing running transpile() on a circuit with control flow. This commit fixes this by adding support to the target to represent globally defined operations by passing the class into Target.add_instruction instead of an instance of that class. When the class is received the Target class will treat that operation name and class as always being valid for the target for all qubits and parameters. This can then be used to represent control flow operations in the target and run transpile() with control flow operations. Fixes #8824 * Simplify test slightly * Add release note * Add coupling map target test * Raise if instruction class and properties are both set * Only return global gates if they exist * Change UserWarning to a comment * Revert change to instructions() getter * Add release note about coupling map api change * Fix lint and logic bug * Add back nested while_loop to transpile() test
1 parent b3cf64f commit ad07847

File tree

5 files changed

+633
-20
lines changed

5 files changed

+633
-20
lines changed

qiskit/transpiler/target.py

+105-18
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@
2222
import datetime
2323
import io
2424
import logging
25+
import inspect
2526

2627
import retworkx as rx
2728

2829
from qiskit.circuit.parameter import Parameter
2930
from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap
3031
from qiskit.transpiler.coupling import CouplingMap
32+
from qiskit.transpiler.exceptions import TranspilerError
3133
from qiskit.transpiler.instruction_durations import InstructionDurations
3234
from qiskit.transpiler.timing_constraints import TimingConstraints
3335

@@ -295,7 +297,11 @@ def add_instruction(self, instruction, properties=None, name=None):
295297
296298
Args:
297299
instruction (qiskit.circuit.Instruction): The operation object to add to the map. If it's
298-
paramerterized any value of the parameter can be set
300+
paramerterized any value of the parameter can be set. Optionally for variable width
301+
instructions (such as control flow operations such as :class:`~.ForLoop` or
302+
:class:`~MCXGate`) you can specify the class. If the class is specified than the
303+
``name`` argument must be specified. When a class is used the gate is treated as global
304+
and not having any properties set.
299305
properties (dict): A dictionary of qarg entries to an
300306
:class:`~qiskit.transpiler.InstructionProperties` object for that
301307
instruction implementation on the backend. Properties are optional
@@ -319,19 +325,37 @@ def add_instruction(self, instruction, properties=None, name=None):
319325
documentation for the :class:`~qiskit.transpiler.Target` class).
320326
Raises:
321327
AttributeError: If gate is already in map
328+
TranspilerError: If an operation class is passed in for ``instruction`` and no name
329+
is specified or ``properties`` is set.
322330
"""
331+
is_class = inspect.isclass(instruction)
332+
if not is_class:
333+
instruction_name = name or instruction.name
334+
else:
335+
# Invalid to have class input without a name with characters set "" is not a valid name
336+
if not name:
337+
raise TranspilerError(
338+
"A name must be specified when defining a supported global operation by class"
339+
)
340+
if properties is not None:
341+
raise TranspilerError(
342+
"An instruction added globally by class can't have properties set."
343+
)
344+
instruction_name = name
323345
if properties is None:
324346
properties = {None: None}
325-
instruction_name = name or instruction.name
326347
if instruction_name in self._gate_map:
327348
raise AttributeError("Instruction %s is already in the target" % instruction_name)
328349
self._gate_name_map[instruction_name] = instruction
329-
qargs_val = {}
330-
for qarg in properties:
331-
if qarg is not None:
332-
self.num_qubits = max(self.num_qubits, max(qarg) + 1)
333-
qargs_val[qarg] = properties[qarg]
334-
self._qarg_gate_map[qarg].add(instruction_name)
350+
if is_class:
351+
qargs_val = {None: None}
352+
else:
353+
qargs_val = {}
354+
for qarg in properties:
355+
if qarg is not None:
356+
self.num_qubits = max(self.num_qubits, max(qarg) + 1)
357+
qargs_val[qarg] = properties[qarg]
358+
self._qarg_gate_map[qarg].add(instruction_name)
335359
self._gate_map[instruction_name] = qargs_val
336360
self._coupling_graph = None
337361
self._instruction_durations = None
@@ -494,7 +518,9 @@ def operation_from_name(self, instruction):
494518
instruction (str): The instruction name to get the
495519
:class:`~qiskit.circuit.Instruction` instance for
496520
Returns:
497-
qiskit.circuit.Instruction: The Instruction instance corresponding to the name
521+
qiskit.circuit.Instruction: The Instruction instance corresponding to the
522+
name. This also can also be the class for globally defined variable with
523+
operations.
498524
"""
499525
return self._gate_name_map[instruction]
500526

@@ -507,14 +533,18 @@ def operations_for_qargs(self, qargs):
507533
instructions that apply to qubit 0.
508534
Returns:
509535
list: The list of :class:`~qiskit.circuit.Instruction` instances
510-
that apply to the specified qarg.
536+
that apply to the specified qarg. This may also be a class if
537+
a variable width operation is globally defined.
511538
512539
Raises:
513540
KeyError: If qargs is not in target
514541
"""
515542
if qargs not in self._qarg_gate_map:
516543
raise KeyError(f"{qargs} not in target.")
517-
return [self._gate_name_map[x] for x in self._qarg_gate_map[qargs]]
544+
res = [self._gate_name_map[x] for x in self._qarg_gate_map[qargs]]
545+
if None in self._qarg_gate_map:
546+
res += [self._gate_name_map[x] for x in self._qarg_gate_map[None]]
547+
return res
518548

519549
def operation_names_for_qargs(self, qargs):
520550
"""Get the operation names for a specified qargs tuple
@@ -609,6 +639,20 @@ def check_obj_params(parameters, obj):
609639
qargs = tuple(qargs)
610640
if operation_class is not None:
611641
for op_name, obj in self._gate_name_map.items():
642+
if inspect.isclass(obj):
643+
if obj != operation_class:
644+
continue
645+
# If no qargs a operation class is supported
646+
if qargs is None:
647+
return True
648+
# If qargs set then validate no duplicates and all indices are valid on device
649+
elif all(qarg <= self.num_qubits for qarg in qargs) and len(set(qargs)) == len(
650+
qargs
651+
):
652+
return True
653+
else:
654+
return False
655+
612656
if isinstance(obj, operation_class):
613657
if parameters is not None:
614658
if len(parameters) != len(obj.params):
@@ -627,6 +671,23 @@ def check_obj_params(parameters, obj):
627671
if operation_name in self._gate_map:
628672
if parameters is not None:
629673
obj = self._gate_name_map[operation_name]
674+
if inspect.isclass(obj):
675+
# The parameters argument was set and the operation_name specified is
676+
# defined as a globally supported class in the target. This means
677+
# there is no available validation (including whether the specified
678+
# operation supports parameters), the returned value will not factor
679+
# in the argument `parameters`,
680+
681+
# If no qargs a operation class is supported
682+
if qargs is None:
683+
return True
684+
# If qargs set then validate no duplicates and all indices are valid on device
685+
elif all(qarg <= self.num_qubits for qarg in qargs) and len(set(qargs)) == len(
686+
qargs
687+
):
688+
return True
689+
else:
690+
return False
630691
if len(parameters) != len(obj.params):
631692
return False
632693
for index, param in enumerate(parameters):
@@ -643,9 +704,21 @@ def check_obj_params(parameters, obj):
643704
if qargs in self._gate_map[operation_name]:
644705
return True
645706
if self._gate_map[operation_name] is None or None in self._gate_map[operation_name]:
646-
return self._gate_name_map[operation_name].num_qubits == len(qargs) and all(
647-
x < self.num_qubits for x in qargs
648-
)
707+
obj = self._gate_name_map[operation_name]
708+
if inspect.isclass(obj):
709+
if qargs is None:
710+
return True
711+
# If qargs set then validate no duplicates and all indices are valid on device
712+
elif all(qarg <= self.num_qubits for qarg in qargs) and len(set(qargs)) == len(
713+
qargs
714+
):
715+
return True
716+
else:
717+
return False
718+
else:
719+
return self._gate_name_map[operation_name].num_qubits == len(qargs) and all(
720+
x < self.num_qubits for x in qargs
721+
)
649722
return False
650723

651724
@property
@@ -661,7 +734,12 @@ def operations(self):
661734
@property
662735
def instructions(self):
663736
"""Get the list of tuples ``(:class:`~qiskit.circuit.Instruction`, (qargs))``
664-
for the target"""
737+
for the target
738+
739+
For globally defined variable width operations the tuple will be of the form
740+
``(class, None)`` where class is the actual operation class that
741+
is globally defined.
742+
"""
665743
return [
666744
(self._gate_name_map[op], qarg) for op in self._gate_map for qarg in self._gate_map[op]
667745
]
@@ -711,6 +789,8 @@ def _build_coupling_graph(self):
711789
self._coupling_graph.add_nodes_from([{} for _ in range(self.num_qubits)])
712790
for gate, qarg_map in self._gate_map.items():
713791
for qarg, properties in qarg_map.items():
792+
if qarg is None:
793+
continue
714794
if len(qarg) == 1:
715795
self._coupling_graph[qarg[0]] = properties
716796
elif len(qarg) == 2:
@@ -719,6 +799,8 @@ def _build_coupling_graph(self):
719799
edge_data[gate] = properties
720800
except rx.NoEdgeBetweenNodes:
721801
self._coupling_graph.add_edge(*qarg, {gate: properties})
802+
if self._coupling_graph.num_edges() == 0 and any(x is None for x in self._qarg_gate_map):
803+
self._coupling_graph = None
722804

723805
def build_coupling_map(self, two_q_gate=None):
724806
"""Get a :class:`~qiskit.transpiler.CouplingMap` from this target.
@@ -761,9 +843,14 @@ def build_coupling_map(self, two_q_gate=None):
761843

762844
if self._coupling_graph is None:
763845
self._build_coupling_graph()
764-
cmap = CouplingMap()
765-
cmap.graph = self._coupling_graph
766-
return cmap
846+
# if there is no connectivity constraints in the coupling graph treat it as not
847+
# existing and return
848+
if self._coupling_graph is not None:
849+
cmap = CouplingMap()
850+
cmap.graph = self._coupling_graph
851+
return cmap
852+
else:
853+
return None
767854

768855
@property
769856
def physical_qubits(self):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
---
2+
features:
3+
- |
4+
Add support for representing an operation that has a variable width
5+
to the :class:`~.Target` class. Previously, a :class:`~.Target` object
6+
needed to have an instance of :class:`~Operation` defined for each
7+
operation supported in the target. This was used for both validation
8+
of arguments and parameters of the operation. However, for operations
9+
that have a variable width this wasn't possible because each instance
10+
of an :class:`~Operation` class can only have a fixed number of qubits.
11+
For cases where a backend supports variable width operations the
12+
instruction can be added with the class of the operation instead of an
13+
instance. In such cases the operation will be treated as globally
14+
supported on all qubits. For example, if building a target like::
15+
16+
from qiskit.transpiler import Target
17+
18+
ibm_target = Target()
19+
i_props = {
20+
(0,): InstructionProperties(duration=35.5e-9, error=0.000413),
21+
(1,): InstructionProperties(duration=35.5e-9, error=0.000502),
22+
(2,): InstructionProperties(duration=35.5e-9, error=0.0004003),
23+
(3,): InstructionProperties(duration=35.5e-9, error=0.000614),
24+
(4,): InstructionProperties(duration=35.5e-9, error=0.006149),
25+
}
26+
ibm_target.add_instruction(IGate(), i_props)
27+
rz_props = {
28+
(0,): InstructionProperties(duration=0, error=0),
29+
(1,): InstructionProperties(duration=0, error=0),
30+
(2,): InstructionProperties(duration=0, error=0),
31+
(3,): InstructionProperties(duration=0, error=0),
32+
(4,): InstructionProperties(duration=0, error=0),
33+
}
34+
ibm_target.add_instruction(RZGate(theta), rz_props)
35+
sx_props = {
36+
(0,): InstructionProperties(duration=35.5e-9, error=0.000413),
37+
(1,): InstructionProperties(duration=35.5e-9, error=0.000502),
38+
(2,): InstructionProperties(duration=35.5e-9, error=0.0004003),
39+
(3,): InstructionProperties(duration=35.5e-9, error=0.000614),
40+
(4,): InstructionProperties(duration=35.5e-9, error=0.006149),
41+
}
42+
ibm_target.add_instruction(SXGate(), sx_props)
43+
x_props = {
44+
(0,): InstructionProperties(duration=35.5e-9, error=0.000413),
45+
(1,): InstructionProperties(duration=35.5e-9, error=0.000502),
46+
(2,): InstructionProperties(duration=35.5e-9, error=0.0004003),
47+
(3,): InstructionProperties(duration=35.5e-9, error=0.000614),
48+
(4,): InstructionProperties(duration=35.5e-9, error=0.006149),
49+
}
50+
ibm_target.add_instruction(XGate(), x_props)
51+
cx_props = {
52+
(3, 4): InstructionProperties(duration=270.22e-9, error=0.00713),
53+
(4, 3): InstructionProperties(duration=305.77e-9, error=0.00713),
54+
(3, 1): InstructionProperties(duration=462.22e-9, error=0.00929),
55+
(1, 3): InstructionProperties(duration=497.77e-9, error=0.00929),
56+
(1, 2): InstructionProperties(duration=227.55e-9, error=0.00659),
57+
(2, 1): InstructionProperties(duration=263.11e-9, error=0.00659),
58+
(0, 1): InstructionProperties(duration=519.11e-9, error=0.01201),
59+
(1, 0): InstructionProperties(duration=554.66e-9, error=0.01201),
60+
}
61+
ibm_target.add_instruction(CXGate(), cx_props)
62+
measure_props = {
63+
(0,): InstructionProperties(duration=5.813e-6, error=0.0751),
64+
(1,): InstructionProperties(duration=5.813e-6, error=0.0225),
65+
(2,): InstructionProperties(duration=5.813e-6, error=0.0146),
66+
(3,): InstructionProperties(duration=5.813e-6, error=0.0215),
67+
(4,): InstructionProperties(duration=5.813e-6, error=0.0333),
68+
}
69+
ibm_target.add_instruction(Measure(), measure_props)
70+
ibm_target.add_instruction(IfElseOp, name="if_else")
71+
ibm_target.add_instruction(ForLoopOp, name="for_loop")
72+
ibm_target.add_instruction(WhileLoopOp, name="while_loop")
73+
74+
The :class:`~.IfElseOp`, :class:`~.ForLoopOp`, and :class:`~.WhileLoopOp`
75+
operations are globally supported for any number of qubits. This is then
76+
reflected by other calls in the :class:`~.Target` API such as
77+
:meth:`~.Target.instruction_supported`::
78+
79+
ibm_target.instruction_supported(operation_class=WhileLoopOp, qargs=(0, 2, 3, 4))
80+
ibm_target.instruction_supported('if_else', qargs=(0, 1))
81+
82+
both return ``True``.
83+
upgrade:
84+
- |
85+
For :class:`~.Target` objects that only contain globally defined 2 qubit
86+
operations without any connectivity constaints the return from the
87+
:meth:`.Target.build_coupling_map` method will now return ``None`` instead
88+
of a :class:`~.CouplingMap` object that contains ``num_qubits`` nodes
89+
and no edges. This change was made to better reflect the actual
90+
connectivity constraints of the :class:`~.Target` because in this case
91+
there are no connectivity constraints on the backend being modeled by
92+
the :class:`~.Target`, not a lack of connecitvity. If you desire the
93+
previous behavior for any reason you can reproduce it by checking for a
94+
``None`` and manually building a coupling map, for example::
95+
96+
from qiskit.transpiler import Target
97+
from qiskit.circuit.library import CXGate
98+
99+
target = Target(num_qubits=3)
100+
target.add_instruction(CXGate())
101+
cmap = target.build_coupling_map()
102+
if cmap is None:
103+
cmap = CouplingMap()
104+
for i in range(target.num_qubits):
105+
cmap.add_physical_qubit(i)

0 commit comments

Comments
 (0)