From 6d032e0e9de1acd71102d4e44af4b2b001444f80 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Fri, 21 Apr 2023 12:28:43 +0900 Subject: [PATCH 01/12] Add and update tests --- .../legacy_scheduling/test_scheduling_pass.py | 23 ++++++++++++ .../transpiler/test_dynamical_decoupling.py | 35 +++++++++++++++++++ .../test_scheduling_padding_pass.py | 17 +++++++++ 3 files changed, 75 insertions(+) diff --git a/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py b/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py index f195143d32d4..24f2baa900e2 100644 --- a/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py +++ b/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py @@ -17,9 +17,11 @@ from ddt import ddt, data, unpack from qiskit import QuantumCircuit from qiskit.test import QiskitTestCase +from qiskit.circuit.library.standard_gates import XGate from qiskit.transpiler.instruction_durations import InstructionDurations from qiskit.transpiler.passes import ASAPSchedule, ALAPSchedule from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.target import Target, InstructionProperties @ddt @@ -718,6 +720,27 @@ def test_dag_introduces_extra_dependency_between_conditionals(self): self.assertEqual(expected, scheduled) + @data(ALAPSchedule, ASAPSchedule) + def test_respect_target_instruction_constraints(self, schedule_pass): + """Test if ALAP/ASAP does not pad delays for qubits that do not support delay instructions. + See: https://github.com/Qiskit/qiskit-terra/issues/9993 + """ + target = Target(dt=1) + target.add_instruction(XGate(), {(1,): InstructionProperties(duration=200)}) + # delays are not supported + + qc = QuantumCircuit(2) + qc.x(1) + + pm = PassManager(schedule_pass(target=target)) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2) + expected.x(1) + # no delay on qubit 0 + + self.assertEqual(expected, scheduled) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_dynamical_decoupling.py b/test/python/transpiler/test_dynamical_decoupling.py index ca57a82239ed..9bb4b805bb77 100644 --- a/test/python/transpiler/test_dynamical_decoupling.py +++ b/test/python/transpiler/test_dynamical_decoupling.py @@ -195,6 +195,7 @@ def test_insert_dd_ghz_with_target(self): target.add_instruction( Reset(), {(x,): InstructionProperties(duration=1500) for x in range(4)} ) + target.add_instruction(Delay(Parameter("t")), {(x,): None for x in range(4)}) dd_sequence = [XGate(), XGate()] pm = PassManager( [ @@ -822,6 +823,40 @@ def test_dd_can_sequentially_called(self): self.assertEqual(circ1, circ2) + def test_respect_target_instruction_constraints(self): + """Test if DD pass does not pad delays for qubits that do not support delay instructions. + See: https://github.com/Qiskit/qiskit-terra/issues/9993 + """ + qc = QuantumCircuit(3) + qc.cx(1, 2) + + target = Target(dt=1) + target.add_instruction( + XGate(), {(q,): InstructionProperties(duration=100) for q in range(3)} + ) + target.add_instruction(CXGate(), {(1, 2): InstructionProperties(duration=1000)}) + # delays and Y are not supported + + pm = PassManager( + [ + ALAPScheduleAnalysis(target=target), + PadDynamicalDecoupling(dd_sequence=[XGate(), XGate()], target=target), + ] + ) + scheduled = pm.run(qc) + self.assertEqual(qc, scheduled) + + pm2 = PassManager( + [ + ALAPScheduleAnalysis(target=target), + PadDynamicalDecoupling( + dd_sequence=[XGate(), YGate(), XGate(), YGate()], target=target + ), + ] + ) + with self.assertRaises(TranspilerError): + pm2.run(qc) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_scheduling_padding_pass.py b/test/python/transpiler/test_scheduling_padding_pass.py index 29d086a44163..c981f98649b6 100644 --- a/test/python/transpiler/test_scheduling_padding_pass.py +++ b/test/python/transpiler/test_scheduling_padding_pass.py @@ -851,6 +851,23 @@ def test_no_pad_very_end_of_circuit(self): self.assertEqual(scheduled, qc) + @data(ALAPScheduleAnalysis, ASAPScheduleAnalysis) + def test_respect_target_instruction_constraints(self, schedule_pass): + """Test if DD pass does not pad delays for qubits that do not support delay instructions. + See: https://github.com/Qiskit/qiskit-terra/issues/9993 + """ + qc = QuantumCircuit(3) + qc.cx(1, 2) + + target = Target(dt=1) + target.add_instruction(CXGate(), {(1, 2): InstructionProperties(duration=1000)}) + # delays are not supported + + pm = PassManager([schedule_pass(target=target), PadDelay(target=target)]) + scheduled = pm.run(qc) + + self.assertEqual(qc, scheduled) + if __name__ == "__main__": unittest.main() From da38b13ff65068235720deb19f70be2682e359d8 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Fri, 21 Apr 2023 12:30:47 +0900 Subject: [PATCH 02/12] Fix padding passes to respect target's constraints --- qiskit/transpiler/passes/scheduling/alap.py | 6 +- qiskit/transpiler/passes/scheduling/asap.py | 6 +- .../passes/scheduling/base_scheduler.py | 9 +-- .../passes/scheduling/padding/base_padding.py | 60 ++++++++++++------- .../padding/dynamical_decoupling.py | 2 +- .../passes/scheduling/padding/pad_delay.py | 8 ++- 6 files changed, 60 insertions(+), 31 deletions(-) diff --git a/qiskit/transpiler/passes/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/alap.py index 935df96a063f..f1036afc35b1 100644 --- a/qiskit/transpiler/passes/scheduling/alap.py +++ b/qiskit/transpiler/passes/scheduling/alap.py @@ -138,7 +138,8 @@ def run(self, dag): for bit in node.qargs: delta = t0 - idle_before[bit] if delta > 0: - new_dag.apply_operation_front(Delay(delta, time_unit), [bit], []) + if self.target is None or (bit_indices[bit],) in self.target.get("delay", []): + new_dag.apply_operation_front(Delay(delta, time_unit), [bit], []) idle_before[bit] = t1 new_dag.apply_operation_front(node.op, node.qargs, node.cargs) @@ -148,7 +149,8 @@ def run(self, dag): delta = circuit_duration - before if not (delta > 0 and isinstance(bit, Qubit)): continue - new_dag.apply_operation_front(Delay(delta, time_unit), [bit], []) + if self.target is None or (bit_indices[bit],) in self.target.get("delay", []): + new_dag.apply_operation_front(Delay(delta, time_unit), [bit], []) new_dag.name = dag.name new_dag.metadata = dag.metadata diff --git a/qiskit/transpiler/passes/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/asap.py index 331a0fcd42f7..a300f3c45c2a 100644 --- a/qiskit/transpiler/passes/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/asap.py @@ -151,7 +151,8 @@ def run(self, dag): for bit in node.qargs: delta = t0 - idle_after[bit] if delta > 0 and isinstance(bit, Qubit): - new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) + if self.target is None or (bit_indices[bit],) in self.target.get("delay", []): + new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) idle_after[bit] = t1 new_dag.apply_operation_back(node.op, node.qargs, node.cargs) @@ -161,7 +162,8 @@ def run(self, dag): delta = circuit_duration - after if not (delta > 0 and isinstance(bit, Qubit)): continue - new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) + if self.target is None or (bit_indices[bit],) in self.target.get("delay", []): + new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) new_dag.name = dag.name new_dag.metadata = dag.metadata diff --git a/qiskit/transpiler/passes/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/base_scheduler.py index bc96aacc2d57..cfd73de53e93 100644 --- a/qiskit/transpiler/passes/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/base_scheduler.py @@ -245,15 +245,16 @@ def __init__( """ super().__init__() self.durations = durations + # Ensure op node durations are attached and in consistent unit + if target is not None: + self.durations = target.durations() + self.requires.append(TimeUnitConversion(self.durations)) # Control flow constraints. self.clbit_write_latency = clbit_write_latency self.conditional_latency = conditional_latency - # Ensure op node durations are attached and in consistent unit - self.requires.append(TimeUnitConversion(durations)) - if target is not None: - self.durations = target.durations() + self.target = target @staticmethod def _get_node_duration( diff --git a/qiskit/transpiler/passes/scheduling/padding/base_padding.py b/qiskit/transpiler/passes/scheduling/padding/base_padding.py index 0e3126d74f50..272355404508 100644 --- a/qiskit/transpiler/passes/scheduling/padding/base_padding.py +++ b/qiskit/transpiler/passes/scheduling/padding/base_padding.py @@ -19,6 +19,7 @@ from qiskit.dagcircuit import DAGCircuit, DAGNode from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.target import Target class BasePadding(TransformationPass): @@ -49,6 +50,20 @@ class BasePadding(TransformationPass): which may result in violation of hardware alignment constraints. """ + def __init__( + self, + target: Target = None, + ): + """BasePadding initializer. + + Args: + target: The :class:`~.Target` representing the target backend. + If it supplied and it does not support delay instruction on a qubit, + padding passes do not pad any idle time of the qubit. + """ + super().__init__() + self.target = target + def run(self, dag: DAGCircuit): """Run the padding pass on ``dag``. @@ -83,6 +98,7 @@ def run(self, dag: DAGCircuit): new_dag.calibrations = dag.calibrations new_dag.global_phase = dag.global_phase + bit_indices = {q: index for index, q in enumerate(dag.qubits)} idle_after = {bit: 0 for bit in dag.qubits} # Compute fresh circuit duration from the node start time dictionary and op duration. @@ -107,16 +123,19 @@ def run(self, dag: DAGCircuit): # Fill idle time with some sequence if t0 - idle_after[bit] > 0: - # Find previous node on the wire, i.e. always the latest node on the wire - prev_node = next(new_dag.predecessors(new_dag.output_map[bit])) - self._pad( - dag=new_dag, - qubit=bit, - t_start=idle_after[bit], - t_end=t0, - next_node=node, - prev_node=prev_node, - ) + if self.target is None or (bit_indices[bit],) in self.target.get( + "delay", [] + ): + # Find previous node on the wire, i.e. always the latest node on the wire + prev_node = next(new_dag.predecessors(new_dag.output_map[bit])) + self._pad( + dag=new_dag, + qubit=bit, + t_start=idle_after[bit], + t_end=t0, + next_node=node, + prev_node=prev_node, + ) idle_after[bit] = t1 @@ -130,16 +149,17 @@ def run(self, dag: DAGCircuit): # Add delays until the end of circuit. for bit in new_dag.qubits: if circuit_duration - idle_after[bit] > 0: - node = new_dag.output_map[bit] - prev_node = next(new_dag.predecessors(node)) - self._pad( - dag=new_dag, - qubit=bit, - t_start=idle_after[bit], - t_end=circuit_duration, - next_node=node, - prev_node=prev_node, - ) + if self.target is None or (bit_indices[bit],) in self.target.get("delay", []): + node = new_dag.output_map[bit] + prev_node = next(new_dag.predecessors(node)) + self._pad( + dag=new_dag, + qubit=bit, + t_start=idle_after[bit], + t_end=circuit_duration, + next_node=node, + prev_node=prev_node, + ) new_dag.duration = circuit_duration diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index f0a24ea810a9..60e069423989 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -152,7 +152,7 @@ def __init__( non-multiple of the alignment constraint value is found. TypeError: If ``dd_sequence`` is not specified """ - super().__init__() + super().__init__(target=target) self._durations = durations if dd_sequence is None: raise TypeError("required argument 'dd_sequence' is not specified") diff --git a/qiskit/transpiler/passes/scheduling/padding/pad_delay.py b/qiskit/transpiler/passes/scheduling/padding/pad_delay.py index c0a12267211a..f1517900874f 100644 --- a/qiskit/transpiler/passes/scheduling/padding/pad_delay.py +++ b/qiskit/transpiler/passes/scheduling/padding/pad_delay.py @@ -15,6 +15,7 @@ from qiskit.circuit import Qubit from qiskit.circuit.delay import Delay from qiskit.dagcircuit import DAGCircuit, DAGNode, DAGOutNode +from qiskit.transpiler.target import Target from .base_padding import BasePadding @@ -50,13 +51,16 @@ class PadDelay(BasePadding): See :class:`BasePadding` pass for details. """ - def __init__(self, fill_very_end: bool = True): + def __init__(self, fill_very_end: bool = True, target: Target = None): """Create new padding delay pass. Args: fill_very_end: Set ``True`` to fill the end of circuit with delay. + target: The :class:`~.Target` representing the target backend. + If it supplied and it does not support delay instruction on a qubit, + padding passes do not pad any idle time of the qubit. """ - super().__init__() + super().__init__(target=target) self.fill_very_end = fill_very_end def _pad( From fb1daba9a78c914078c3778970886e3a9348b1fa Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Fri, 21 Apr 2023 13:10:21 +0900 Subject: [PATCH 03/12] Fix transpile with scheduling to respect target's constraints --- .../transpiler/preset_passmanagers/common.py | 2 +- test/python/compiler/test_transpiler.py | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 7cd3221c84ae..d8d3bc400e50 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -534,7 +534,7 @@ def _require_alignment(property_set): ) if scheduling_method: # Call padding pass if circuit is scheduled - scheduling.append(PadDelay()) + scheduling.append(PadDelay(target=target)) return scheduling diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 238a77ea699e..2f03fd5112b1 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -2910,3 +2910,51 @@ def test_transpile_does_not_affect_backend_coupling(self, opt_level): original_map = copy.deepcopy(backend.coupling_map) transpile(qc, backend, optimization_level=opt_level) self.assertEqual(original_map, backend.coupling_map) + + @combine( + optimization_level=[0, 1, 2, 3], + scheduling_method=["asap", "alap"], + ) + def test_transpile_target_with_qubits_without_delays_with_scheduling( + self, optimization_level, scheduling_method + ): + """Test qubits without operations aren't ever used.""" + no_delay_qubits = [1, 3, 4] + target = Target(num_qubits=5, dt=1) + target.add_instruction( + XGate(), {(i,): InstructionProperties(duration=160) for i in range(4)} + ) + target.add_instruction( + HGate(), {(i,): InstructionProperties(duration=160) for i in range(4)} + ) + target.add_instruction( + CXGate(), + { + edge: InstructionProperties(duration=800) + for edge in [(0, 1), (1, 2), (2, 0), (2, 3)] + }, + ) + target.add_instruction( + Delay(Parameter("t")), {(i,): None for i in range(4) if i not in no_delay_qubits} + ) + qc = QuantumCircuit(4) + qc.x(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(1, 3) + qc.cx(0, 3) + tqc = transpile( + qc, + target=target, + optimization_level=optimization_level, + scheduling_method=scheduling_method, + ) + invalid_qubits = { + 4, + } + self.assertEqual(tqc.num_qubits, 5) + for inst in tqc.data: + for bit in inst.qubits: + self.assertNotIn(tqc.find_bit(bit).index, invalid_qubits) + if isinstance(inst.operation, Delay): + self.assertNotIn(tqc.find_bit(bit).index, no_delay_qubits) From fbc78514dfee2cefd38c03c0034571ea1e7c91aa Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Fri, 21 Apr 2023 18:35:45 +0900 Subject: [PATCH 04/12] Add release note --- .../notes/fix-delay-padding-75937bda37ebc3fd.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 releasenotes/notes/fix-delay-padding-75937bda37ebc3fd.yaml diff --git a/releasenotes/notes/fix-delay-padding-75937bda37ebc3fd.yaml b/releasenotes/notes/fix-delay-padding-75937bda37ebc3fd.yaml new file mode 100644 index 000000000000..c3438508987a --- /dev/null +++ b/releasenotes/notes/fix-delay-padding-75937bda37ebc3fd.yaml @@ -0,0 +1,13 @@ +--- +fixes: + - | + Fixed an issue with tranpiler passes for padding delays, which did not respect target's + constraints and inserted delays even for qubits not supporting :class:`~.Delay` instruction. + :class:`~.PadDelay` and :class:`~.PadDynamicalDecoupling` are fixed + so that they do not pad any idle time of qubits such that the target does not support + ``Delay`` instructions for the qubits. + Also legacy scheduling passes ``ASAPSchedule`` and ``ALAPSchedule``, + which pad delays internally, are fixed in the same way. + In addition, :func:`transpile` is fixed to call ``PadDelay`` with a ``target`` object + so that it works correctly when called with ``scheduling_method`` option. + Fixed `#9993 `__ From b3e4cb4958969cc6a4c45bd03d8a88197d130cf9 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Fri, 21 Apr 2023 19:21:53 +0900 Subject: [PATCH 05/12] fix reno --- releasenotes/notes/fix-delay-padding-75937bda37ebc3fd.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/releasenotes/notes/fix-delay-padding-75937bda37ebc3fd.yaml b/releasenotes/notes/fix-delay-padding-75937bda37ebc3fd.yaml index c3438508987a..34c42ab5baf8 100644 --- a/releasenotes/notes/fix-delay-padding-75937bda37ebc3fd.yaml +++ b/releasenotes/notes/fix-delay-padding-75937bda37ebc3fd.yaml @@ -1,8 +1,8 @@ --- fixes: - | - Fixed an issue with tranpiler passes for padding delays, which did not respect target's - constraints and inserted delays even for qubits not supporting :class:`~.Delay` instruction. + Fixed an issue in tranpiler passes for padding delays, which did not respect target's constraints + and inserted delays even for qubits not supporting :class:`~.circuit.Delay` instruction. :class:`~.PadDelay` and :class:`~.PadDynamicalDecoupling` are fixed so that they do not pad any idle time of qubits such that the target does not support ``Delay`` instructions for the qubits. From 1c3f585858dfee4716ab0dbcbd1ce7dd7bd26f10 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Fri, 21 Apr 2023 19:37:44 +0900 Subject: [PATCH 06/12] use target.instruction_supported --- qiskit/transpiler/passes/scheduling/alap.py | 8 ++++++-- qiskit/transpiler/passes/scheduling/asap.py | 8 ++++++-- .../transpiler/passes/scheduling/padding/base_padding.py | 8 +++++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/qiskit/transpiler/passes/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/alap.py index f1036afc35b1..3c475bf28942 100644 --- a/qiskit/transpiler/passes/scheduling/alap.py +++ b/qiskit/transpiler/passes/scheduling/alap.py @@ -138,7 +138,9 @@ def run(self, dag): for bit in node.qargs: delta = t0 - idle_before[bit] if delta > 0: - if self.target is None or (bit_indices[bit],) in self.target.get("delay", []): + if self.target is None or self.target.instruction_supported( + "delay", qargs=(bit_indices[bit],) + ): new_dag.apply_operation_front(Delay(delta, time_unit), [bit], []) idle_before[bit] = t1 @@ -149,7 +151,9 @@ def run(self, dag): delta = circuit_duration - before if not (delta > 0 and isinstance(bit, Qubit)): continue - if self.target is None or (bit_indices[bit],) in self.target.get("delay", []): + if self.target is None or self.target.instruction_supported( + "delay", qargs=(bit_indices[bit],) + ): new_dag.apply_operation_front(Delay(delta, time_unit), [bit], []) new_dag.name = dag.name diff --git a/qiskit/transpiler/passes/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/asap.py index a300f3c45c2a..0199a93fe1d7 100644 --- a/qiskit/transpiler/passes/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/asap.py @@ -151,7 +151,9 @@ def run(self, dag): for bit in node.qargs: delta = t0 - idle_after[bit] if delta > 0 and isinstance(bit, Qubit): - if self.target is None or (bit_indices[bit],) in self.target.get("delay", []): + if self.target is None or self.target.instruction_supported( + "delay", qargs=(bit_indices[bit],) + ): new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) idle_after[bit] = t1 @@ -162,7 +164,9 @@ def run(self, dag): delta = circuit_duration - after if not (delta > 0 and isinstance(bit, Qubit)): continue - if self.target is None or (bit_indices[bit],) in self.target.get("delay", []): + if self.target is None or self.target.instruction_supported( + "delay", qargs=(bit_indices[bit],) + ): new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) new_dag.name = dag.name diff --git a/qiskit/transpiler/passes/scheduling/padding/base_padding.py b/qiskit/transpiler/passes/scheduling/padding/base_padding.py index 272355404508..2d4e6df10485 100644 --- a/qiskit/transpiler/passes/scheduling/padding/base_padding.py +++ b/qiskit/transpiler/passes/scheduling/padding/base_padding.py @@ -123,8 +123,8 @@ def run(self, dag: DAGCircuit): # Fill idle time with some sequence if t0 - idle_after[bit] > 0: - if self.target is None or (bit_indices[bit],) in self.target.get( - "delay", [] + if self.target is None or self.target.instruction_supported( + "delay", qargs=(bit_indices[bit],) ): # Find previous node on the wire, i.e. always the latest node on the wire prev_node = next(new_dag.predecessors(new_dag.output_map[bit])) @@ -149,7 +149,9 @@ def run(self, dag: DAGCircuit): # Add delays until the end of circuit. for bit in new_dag.qubits: if circuit_duration - idle_after[bit] > 0: - if self.target is None or (bit_indices[bit],) in self.target.get("delay", []): + if self.target is None or self.target.instruction_supported( + "delay", qargs=(bit_indices[bit],) + ): node = new_dag.output_map[bit] prev_node = next(new_dag.predecessors(node)) self._pad( From eee5bae991118606db7e9f5bf3c28187d5b5fd0a Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Sat, 22 Apr 2023 00:14:36 +0900 Subject: [PATCH 07/12] simplify --- qiskit/transpiler/passes/scheduling/alap.py | 11 +--- qiskit/transpiler/passes/scheduling/asap.py | 11 +--- .../passes/scheduling/base_scheduler.py | 6 ++ .../passes/scheduling/padding/base_padding.py | 57 +++++++++---------- .../padding/dynamical_decoupling.py | 8 ++- 5 files changed, 46 insertions(+), 47 deletions(-) diff --git a/qiskit/transpiler/passes/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/alap.py index 3c475bf28942..5059656058f3 100644 --- a/qiskit/transpiler/passes/scheduling/alap.py +++ b/qiskit/transpiler/passes/scheduling/alap.py @@ -137,11 +137,8 @@ def run(self, dag): for bit in node.qargs: delta = t0 - idle_before[bit] - if delta > 0: - if self.target is None or self.target.instruction_supported( - "delay", qargs=(bit_indices[bit],) - ): - new_dag.apply_operation_front(Delay(delta, time_unit), [bit], []) + if delta > 0 and self._delay_supported(bit_indices[bit]): + new_dag.apply_operation_front(Delay(delta, time_unit), [bit], []) idle_before[bit] = t1 new_dag.apply_operation_front(node.op, node.qargs, node.cargs) @@ -151,9 +148,7 @@ def run(self, dag): delta = circuit_duration - before if not (delta > 0 and isinstance(bit, Qubit)): continue - if self.target is None or self.target.instruction_supported( - "delay", qargs=(bit_indices[bit],) - ): + if self._delay_supported(bit_indices[bit]): new_dag.apply_operation_front(Delay(delta, time_unit), [bit], []) new_dag.name = dag.name diff --git a/qiskit/transpiler/passes/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/asap.py index 0199a93fe1d7..b404e73d00a7 100644 --- a/qiskit/transpiler/passes/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/asap.py @@ -150,11 +150,8 @@ def run(self, dag): # Add delay to qubit wire for bit in node.qargs: delta = t0 - idle_after[bit] - if delta > 0 and isinstance(bit, Qubit): - if self.target is None or self.target.instruction_supported( - "delay", qargs=(bit_indices[bit],) - ): - new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) + if delta > 0 and isinstance(bit, Qubit) and self._delay_supported(bit_indices[bit]): + new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) idle_after[bit] = t1 new_dag.apply_operation_back(node.op, node.qargs, node.cargs) @@ -164,9 +161,7 @@ def run(self, dag): delta = circuit_duration - after if not (delta > 0 and isinstance(bit, Qubit)): continue - if self.target is None or self.target.instruction_supported( - "delay", qargs=(bit_indices[bit],) - ): + if self._delay_supported(bit_indices[bit]): new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) new_dag.name = dag.name diff --git a/qiskit/transpiler/passes/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/base_scheduler.py index cfd73de53e93..9f851d56b7e0 100644 --- a/qiskit/transpiler/passes/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/base_scheduler.py @@ -282,5 +282,11 @@ def _get_node_duration( return duration + def _delay_supported(self, qarg: int) -> bool: + """Delay operation is supported on the qubit (qarg) or not.""" + if self.target is None or self.target.instruction_supported("delay", qargs=(qarg,)): + return True + return False + def run(self, dag: DAGCircuit): raise NotImplementedError diff --git a/qiskit/transpiler/passes/scheduling/padding/base_padding.py b/qiskit/transpiler/passes/scheduling/padding/base_padding.py index 2d4e6df10485..9786f23e6336 100644 --- a/qiskit/transpiler/passes/scheduling/padding/base_padding.py +++ b/qiskit/transpiler/passes/scheduling/padding/base_padding.py @@ -120,22 +120,18 @@ def run(self, dag: DAGCircuit): continue for bit in node.qargs: - # Fill idle time with some sequence - if t0 - idle_after[bit] > 0: - if self.target is None or self.target.instruction_supported( - "delay", qargs=(bit_indices[bit],) - ): - # Find previous node on the wire, i.e. always the latest node on the wire - prev_node = next(new_dag.predecessors(new_dag.output_map[bit])) - self._pad( - dag=new_dag, - qubit=bit, - t_start=idle_after[bit], - t_end=t0, - next_node=node, - prev_node=prev_node, - ) + if t0 - idle_after[bit] > 0 and self.__delay_supported(bit_indices[bit]): + # Find previous node on the wire, i.e. always the latest node on the wire + prev_node = next(new_dag.predecessors(new_dag.output_map[bit])) + self._pad( + dag=new_dag, + qubit=bit, + t_start=idle_after[bit], + t_end=t0, + next_node=node, + prev_node=prev_node, + ) idle_after[bit] = t1 @@ -148,25 +144,28 @@ def run(self, dag: DAGCircuit): # Add delays until the end of circuit. for bit in new_dag.qubits: - if circuit_duration - idle_after[bit] > 0: - if self.target is None or self.target.instruction_supported( - "delay", qargs=(bit_indices[bit],) - ): - node = new_dag.output_map[bit] - prev_node = next(new_dag.predecessors(node)) - self._pad( - dag=new_dag, - qubit=bit, - t_start=idle_after[bit], - t_end=circuit_duration, - next_node=node, - prev_node=prev_node, - ) + if circuit_duration - idle_after[bit] > 0 and self.__delay_supported(bit_indices[bit]): + node = new_dag.output_map[bit] + prev_node = next(new_dag.predecessors(node)) + self._pad( + dag=new_dag, + qubit=bit, + t_start=idle_after[bit], + t_end=circuit_duration, + next_node=node, + prev_node=prev_node, + ) new_dag.duration = circuit_duration return new_dag + def __delay_supported(self, qarg: int) -> bool: + """Delay operation is supported on the qubit (qarg) or not.""" + if self.target is None or self.target.instruction_supported("delay", qargs=(qarg,)): + return True + return False + def _pre_runhook(self, dag: DAGCircuit): """Extra routine inserted before running the padding pass. diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index 60e069423989..1634de75c4b1 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -167,6 +167,11 @@ def __init__( self._sequence_phase = 0 if target is not None: self._durations = target.durations() + for gate in dd_sequence: + if gate.name not in self.target.operation_names: + raise TranspilerError( + f"{gate.name} in dd_sequence is not supported in the target" + ) def _pre_runhook(self, dag: DAGCircuit): super()._pre_runhook(dag) @@ -201,8 +206,7 @@ def _pre_runhook(self, dag: DAGCircuit): self._sequence_phase = np.angle(noop[0][0]) # Precompute qubit-wise DD sequence length for performance - for qubit in dag.qubits: - physical_index = dag.qubits.index(qubit) + for physical_index, qubit in enumerate(dag.qubits): if self._qubits and physical_index not in self._qubits: continue From df8231222f51d71c435e8c3531ae99b3d5f010e1 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Sat, 22 Apr 2023 01:09:41 +0900 Subject: [PATCH 08/12] Add check if all DD gates are supported on each qubit --- .../padding/dynamical_decoupling.py | 10 ++++++ .../transpiler/test_dynamical_decoupling.py | 36 +++++++++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index 1634de75c4b1..5874109752c7 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -212,6 +212,10 @@ def _pre_runhook(self, dag: DAGCircuit): sequence_lengths = [] for gate in self._dd_sequence: + if not self.__gate_supported(gate, physical_index): + raise TranspilerError( + f"Gate {gate.name} in dd_sequence is not supported on qubit {physical_index}" + ) try: # Check calibration. gate_length = dag.calibrations[gate.name][(physical_index, gate.params)] @@ -235,6 +239,12 @@ def _pre_runhook(self, dag: DAGCircuit): gate.duration = gate_length self._dd_sequence_lengths[qubit] = sequence_lengths + def __gate_supported(self, gate: Gate, qarg: int) -> bool: + """A gate is supported on the qubit (qarg) or not.""" + if self.target is None or self.target.instruction_supported(gate.name, qargs=(qarg,)): + return True + return False + def _pad( self, dag: DAGCircuit, diff --git a/test/python/transpiler/test_dynamical_decoupling.py b/test/python/transpiler/test_dynamical_decoupling.py index 9bb4b805bb77..81de8cd41356 100644 --- a/test/python/transpiler/test_dynamical_decoupling.py +++ b/test/python/transpiler/test_dynamical_decoupling.py @@ -828,15 +828,27 @@ def test_respect_target_instruction_constraints(self): See: https://github.com/Qiskit/qiskit-terra/issues/9993 """ qc = QuantumCircuit(3) + qc.cx(0, 1) qc.cx(1, 2) target = Target(dt=1) target.add_instruction( XGate(), {(q,): InstructionProperties(duration=100) for q in range(3)} ) - target.add_instruction(CXGate(), {(1, 2): InstructionProperties(duration=1000)}) - # delays and Y are not supported + # Y is partially supported (not supported on qubit 2) + target.add_instruction( + YGate(), {(q,): InstructionProperties(duration=100) for q in range(2)} + ) + target.add_instruction( + CXGate(), + { + (0, 1): InstructionProperties(duration=1000), + (1, 2): InstructionProperties(duration=1000), + }, + ) + # delays are not supported + # No DD instructions are padded (since no delays are in target) pm = PassManager( [ ALAPScheduleAnalysis(target=target), @@ -846,7 +858,20 @@ def test_respect_target_instruction_constraints(self): scheduled = pm.run(qc) self.assertEqual(qc, scheduled) + # No error because Y is supported on qubit 0 but no DD instructions are padded pm2 = PassManager( + [ + ALAPScheduleAnalysis(target=target), + PadDynamicalDecoupling( + dd_sequence=[XGate(), YGate(), XGate(), YGate()], qubits=[0], target=target + ), + ] + ) + scheduled = pm2.run(qc) + self.assertEqual(qc, scheduled) + + # Failed since Y is not supported on qubit 2 + pm3 = PassManager( [ ALAPScheduleAnalysis(target=target), PadDynamicalDecoupling( @@ -855,7 +880,12 @@ def test_respect_target_instruction_constraints(self): ] ) with self.assertRaises(TranspilerError): - pm2.run(qc) + pm3.run(qc) + + # Still fails even if delays are supported since Y is not supported on qubit 2 + target.add_instruction(Delay(Parameter("t")), {(q,): None for q in range(3)}) + with self.assertRaises(TranspilerError): + pm3.run(qc) if __name__ == "__main__": From d8f7de03c76c5a057947ed0d769c993aafd03bd4 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Sat, 22 Apr 2023 01:36:08 +0900 Subject: [PATCH 09/12] Add logging --- .../passes/scheduling/padding/base_padding.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/qiskit/transpiler/passes/scheduling/padding/base_padding.py b/qiskit/transpiler/passes/scheduling/padding/base_padding.py index 9786f23e6336..8234a3eeb590 100644 --- a/qiskit/transpiler/passes/scheduling/padding/base_padding.py +++ b/qiskit/transpiler/passes/scheduling/padding/base_padding.py @@ -12,6 +12,7 @@ """Padding pass to fill empty timeslot.""" +import logging from typing import List, Optional, Union from qiskit.circuit import Qubit, Clbit, Instruction @@ -21,6 +22,8 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.target import Target +logger = logging.getLogger(__name__) + class BasePadding(TransformationPass): """The base class of padding pass. @@ -180,6 +183,13 @@ def _pre_runhook(self, dag: DAGCircuit): f"The input circuit {dag.name} is not scheduled. Call one of scheduling passes " f"before running the {self.__class__.__name__} pass." ) + for qarg, _ in enumerate(dag.qubits): + if not self.__delay_supported(qarg): + logger.debug( + "Idle time may not be padded for qubit %d as the target does not support " + "delay instruction on the qubit", + qarg, + ) def _apply_scheduled_op( self, From d864b1d08d9f36242826836b0ebab64e5ca32502 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Sat, 22 Apr 2023 15:57:02 +0900 Subject: [PATCH 10/12] Update DD tests --- .../transpiler/test_dynamical_decoupling.py | 59 +++++++++---------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/test/python/transpiler/test_dynamical_decoupling.py b/test/python/transpiler/test_dynamical_decoupling.py index 81de8cd41356..6d3b42ab3395 100644 --- a/test/python/transpiler/test_dynamical_decoupling.py +++ b/test/python/transpiler/test_dynamical_decoupling.py @@ -832,12 +832,9 @@ def test_respect_target_instruction_constraints(self): qc.cx(1, 2) target = Target(dt=1) - target.add_instruction( - XGate(), {(q,): InstructionProperties(duration=100) for q in range(3)} - ) # Y is partially supported (not supported on qubit 2) target.add_instruction( - YGate(), {(q,): InstructionProperties(duration=100) for q in range(2)} + XGate(), {(q,): InstructionProperties(duration=100) for q in range(2)} ) target.add_instruction( CXGate(), @@ -848,44 +845,42 @@ def test_respect_target_instruction_constraints(self): ) # delays are not supported - # No DD instructions are padded (since no delays are in target) - pm = PassManager( + # No DD instructions nor delays are padded due to no delay support in the target + pm_xx = PassManager( [ ALAPScheduleAnalysis(target=target), PadDynamicalDecoupling(dd_sequence=[XGate(), XGate()], target=target), ] ) - scheduled = pm.run(qc) - self.assertEqual(qc, scheduled) - - # No error because Y is supported on qubit 0 but no DD instructions are padded - pm2 = PassManager( - [ - ALAPScheduleAnalysis(target=target), - PadDynamicalDecoupling( - dd_sequence=[XGate(), YGate(), XGate(), YGate()], qubits=[0], target=target - ), - ] - ) - scheduled = pm2.run(qc) + scheduled = pm_xx.run(qc) self.assertEqual(qc, scheduled) - # Failed since Y is not supported on qubit 2 - pm3 = PassManager( - [ - ALAPScheduleAnalysis(target=target), - PadDynamicalDecoupling( - dd_sequence=[XGate(), YGate(), XGate(), YGate()], target=target - ), - ] - ) + # Fails since Y is not supported in the target with self.assertRaises(TranspilerError): - pm3.run(qc) + PassManager( + [ + ALAPScheduleAnalysis(target=target), + PadDynamicalDecoupling( + dd_sequence=[XGate(), YGate(), XGate(), YGate()], target=target + ), + ] + ) - # Still fails even if delays are supported since Y is not supported on qubit 2 + # Add delay support to the target target.add_instruction(Delay(Parameter("t")), {(q,): None for q in range(3)}) - with self.assertRaises(TranspilerError): - pm3.run(qc) + # No error but no DD on qubit 2 (just delay is padded) since X is not supported on it + scheduled = pm_xx.run(qc) + + expected = QuantumCircuit(3) + expected.delay(1000, [2]) + expected.cx(0, 1) + expected.cx(1, 2) + expected.delay(200, [0]) + expected.x([0]) + expected.delay(400, [0]) + expected.x([0]) + expected.delay(200, [0]) + self.assertEqual(expected, scheduled) if __name__ == "__main__": From ae972fd6925e590ca88a83c91437a7a063c4192f Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Sat, 22 Apr 2023 16:14:05 +0900 Subject: [PATCH 11/12] Make DD gates check target-aware --- .../passes/scheduling/padding/base_padding.py | 3 +- .../padding/dynamical_decoupling.py | 31 ++++++++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/qiskit/transpiler/passes/scheduling/padding/base_padding.py b/qiskit/transpiler/passes/scheduling/padding/base_padding.py index 8234a3eeb590..5fb25790cfbb 100644 --- a/qiskit/transpiler/passes/scheduling/padding/base_padding.py +++ b/qiskit/transpiler/passes/scheduling/padding/base_padding.py @@ -186,8 +186,7 @@ def _pre_runhook(self, dag: DAGCircuit): for qarg, _ in enumerate(dag.qubits): if not self.__delay_supported(qarg): logger.debug( - "Idle time may not be padded for qubit %d as the target does not support " - "delay instruction on the qubit", + "No padding on qubit %d as delay is not supported on it", qarg, ) diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index 5874109752c7..1e6a2b79f1b3 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -12,6 +12,7 @@ """Dynamical Decoupling insertion pass.""" +import logging from typing import List, Optional import numpy as np @@ -29,6 +30,8 @@ from .base_padding import BasePadding +logger = logging.getLogger(__name__) + class PadDynamicalDecoupling(BasePadding): """Dynamical decoupling insertion pass. @@ -163,6 +166,7 @@ def __init__( self._spacing = spacing self._extra_slack_distribution = extra_slack_distribution + self._no_dd_qubits = set() self._dd_sequence_lengths = {} self._sequence_phase = 0 if target is not None: @@ -205,17 +209,22 @@ def _pre_runhook(self, dag: DAGCircuit): raise TranspilerError("The DD sequence does not make an identity operation.") self._sequence_phase = np.angle(noop[0][0]) + # Compute no DD qubits on which any gate in dd_sequence is not supported in the target + for qarg, _ in enumerate(dag.qubits): + for gate in self._dd_sequence: + if not self.__gate_supported(gate, qarg): + self._no_dd_qubits.add(qarg) + logger.debug( + "No DD on qubit %d as gate %s is not supported on it", qarg, gate.name + ) + break # Precompute qubit-wise DD sequence length for performance for physical_index, qubit in enumerate(dag.qubits): - if self._qubits and physical_index not in self._qubits: + if not self.__is_dd_qubit(physical_index): continue sequence_lengths = [] for gate in self._dd_sequence: - if not self.__gate_supported(gate, physical_index): - raise TranspilerError( - f"Gate {gate.name} in dd_sequence is not supported on qubit {physical_index}" - ) try: # Check calibration. gate_length = dag.calibrations[gate.name][(physical_index, gate.params)] @@ -245,6 +254,14 @@ def __gate_supported(self, gate: Gate, qarg: int) -> bool: return True return False + def __is_dd_qubit(self, qubit_index: int) -> bool: + """DD can be inserted in the qubit or not.""" + if (qubit_index in self._no_dd_qubits) or ( + self._qubits and qubit_index not in self._qubits + ): + return False + return True + def _pad( self, dag: DAGCircuit, @@ -282,7 +299,7 @@ def _pad( # Y3: 288 dt + 160 dt + 80 dt = 528 dt (33 x 16 dt) # Y4: 368 dt + 160 dt + 80 dt = 768 dt (48 x 16 dt) # - # As you can see, constraints on t0 are all satified without explicit scheduling. + # As you can see, constraints on t0 are all satisfied without explicit scheduling. time_interval = t_end - t_start if time_interval % self._alignment != 0: raise TranspilerError( @@ -291,7 +308,7 @@ def _pad( f"on qargs {next_node.qargs}." ) - if self._qubits and dag.qubits.index(qubit) not in self._qubits: + if not self.__is_dd_qubit(dag.qubits.index(qubit)): # Target physical qubit is not the target of this DD sequence. self._apply_scheduled_op(dag, t_start, Delay(time_interval, dag.unit), qubit) return From deb9117d86dfb5c696bf07e6876ae9cd424c2f61 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Sat, 22 Apr 2023 18:33:35 +0900 Subject: [PATCH 12/12] Fix legacy DD pass --- .../passes/scheduling/dynamical_decoupling.py | 27 ++++++-- .../padding/dynamical_decoupling.py | 2 +- .../legacy_scheduling/test_scheduling_pass.py | 69 ++++++++++++++++++- .../transpiler/test_dynamical_decoupling.py | 3 +- 4 files changed, 91 insertions(+), 10 deletions(-) diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py index 9dfeb8454161..fa0b2b29187e 100644 --- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py @@ -16,8 +16,7 @@ import warnings import numpy as np -from qiskit.circuit.delay import Delay -from qiskit.circuit.reset import Reset +from qiskit.circuit import Gate, Delay, Reset from qiskit.circuit.library.standard_gates import IGate, UGate, U3Gate from qiskit.dagcircuit import DAGOpNode, DAGInNode from qiskit.quantum_info.operators.predicates import matrix_equal @@ -128,8 +127,14 @@ def __init__( self._qubits = qubits self._spacing = spacing self._skip_reset_qubits = skip_reset_qubits + self._target = target if target is not None: self._durations = target.durations() + for gate in dd_sequence: + if gate.name not in target.operation_names: + raise TranspilerError( + f"{gate.name} in dd_sequence is not supported in the target" + ) def run(self, dag): """Run the DynamicalDecoupling pass on dag. @@ -178,18 +183,22 @@ def run(self, dag): end = mid / 2 self._spacing = [end] + [mid] * (num_pulses - 1) + [end] - new_dag = dag.copy_empty_like() + for qarg in list(self._qubits): + for gate in self._dd_sequence: + if not self.__gate_supported(gate, qarg): + self._qubits.discard(qarg) + break - qubit_index_map = {qubit: index for index, qubit in enumerate(new_dag.qubits)} index_sequence_duration_map = {} - for qubit in new_dag.qubits: - physical_qubit = qubit_index_map[qubit] + for physical_qubit in self._qubits: dd_sequence_duration = 0 for gate in self._dd_sequence: gate.duration = self._durations.get(gate, physical_qubit) dd_sequence_duration += gate.duration index_sequence_duration_map[physical_qubit] = dd_sequence_duration + new_dag = dag.copy_empty_like() + qubit_index_map = {qubit: index for index, qubit in enumerate(new_dag.qubits)} for nd in dag.topological_op_nodes(): if not isinstance(nd.op, Delay): new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs) @@ -252,6 +261,12 @@ def run(self, dag): return new_dag + def __gate_supported(self, gate: Gate, qarg: int) -> bool: + """A gate is supported on the qubit (qarg) or not.""" + if self._target is None or self._target.instruction_supported(gate.name, qargs=(qarg,)): + return True + return False + def _mod_2pi(angle: float, atol: float = 0): """Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π""" diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index 1e6a2b79f1b3..e3923ed8b26e 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -172,7 +172,7 @@ def __init__( if target is not None: self._durations = target.durations() for gate in dd_sequence: - if gate.name not in self.target.operation_names: + if gate.name not in target.operation_names: raise TranspilerError( f"{gate.name} in dd_sequence is not supported in the target" ) diff --git a/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py b/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py index 24f2baa900e2..2f375c46f67b 100644 --- a/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py +++ b/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py @@ -15,11 +15,14 @@ import unittest from ddt import ddt, data, unpack + from qiskit import QuantumCircuit +from qiskit.circuit import Delay, Parameter +from qiskit.circuit.library.standard_gates import XGate, YGate, CXGate from qiskit.test import QiskitTestCase -from qiskit.circuit.library.standard_gates import XGate +from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.instruction_durations import InstructionDurations -from qiskit.transpiler.passes import ASAPSchedule, ALAPSchedule +from qiskit.transpiler.passes import ASAPSchedule, ALAPSchedule, DynamicalDecoupling from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.target import Target, InstructionProperties @@ -741,6 +744,68 @@ def test_respect_target_instruction_constraints(self, schedule_pass): self.assertEqual(expected, scheduled) + def test_dd_respect_target_instruction_constraints(self): + """Test if DD pass does not pad delays for qubits that do not support delay instructions + and does not insert DD gates for qubits that do not support necessary gates. + See: https://github.com/Qiskit/qiskit-terra/issues/9993 + """ + qc = QuantumCircuit(3) + qc.cx(0, 1) + qc.cx(1, 2) + + target = Target(dt=1) + # Y is partially supported (not supported on qubit 2) + target.add_instruction( + XGate(), {(q,): InstructionProperties(duration=100) for q in range(2)} + ) + target.add_instruction( + CXGate(), + { + (0, 1): InstructionProperties(duration=1000), + (1, 2): InstructionProperties(duration=1000), + }, + ) + # delays are not supported + + # No DD instructions nor delays are padded due to no delay support in the target + pm_xx = PassManager( + [ + ALAPSchedule(target=target), + DynamicalDecoupling(durations=None, dd_sequence=[XGate(), XGate()], target=target), + ] + ) + scheduled = pm_xx.run(qc) + self.assertEqual(qc, scheduled) + + # Fails since Y is not supported in the target + with self.assertRaises(TranspilerError): + PassManager( + [ + ALAPSchedule(target=target), + DynamicalDecoupling( + durations=None, + dd_sequence=[XGate(), YGate(), XGate(), YGate()], + target=target, + ), + ] + ) + + # Add delay support to the target + target.add_instruction(Delay(Parameter("t")), {(q,): None for q in range(3)}) + # No error but no DD on qubit 2 (just delay is padded) since X is not supported on it + scheduled = pm_xx.run(qc) + + expected = QuantumCircuit(3) + expected.delay(1000, [2]) + expected.cx(0, 1) + expected.cx(1, 2) + expected.delay(200, [0]) + expected.x([0]) + expected.delay(400, [0]) + expected.x([0]) + expected.delay(200, [0]) + self.assertEqual(expected, scheduled) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_dynamical_decoupling.py b/test/python/transpiler/test_dynamical_decoupling.py index 6d3b42ab3395..bd8c44ab0973 100644 --- a/test/python/transpiler/test_dynamical_decoupling.py +++ b/test/python/transpiler/test_dynamical_decoupling.py @@ -824,7 +824,8 @@ def test_dd_can_sequentially_called(self): self.assertEqual(circ1, circ2) def test_respect_target_instruction_constraints(self): - """Test if DD pass does not pad delays for qubits that do not support delay instructions. + """Test if DD pass does not pad delays for qubits that do not support delay instructions + and does not insert DD gates for qubits that do not support necessary gates. See: https://github.com/Qiskit/qiskit-terra/issues/9993 """ qc = QuantumCircuit(3)