diff --git a/qiskit_ibm_runtime/transpiler/passes/scheduling/block_base_padder.py b/qiskit_ibm_runtime/transpiler/passes/scheduling/block_base_padder.py index e065b0606a..ea5bed527c 100644 --- a/qiskit_ibm_runtime/transpiler/passes/scheduling/block_base_padder.py +++ b/qiskit_ibm_runtime/transpiler/passes/scheduling/block_base_padder.py @@ -363,6 +363,8 @@ def _visit_block( prev_node = self._prev_node self._prev_node = None prev_wire_map, self._wire_map = self._wire_map, wire_map + prev_idle_after = self._idle_after + prev_current_block_idx = self._current_block_idx prev_block_dag = self._block_dag self._block_dag = new_block_dag = self._empty_dag_like( @@ -394,13 +396,15 @@ def _visit_block( # Edge-case: Add a barrier if the final node is a fast-path if self._prev_node in self._fast_path_nodes: self._add_block_terminating_barrier( - prev_block_duration, prev_block_idx, self._prev_node, force=True + prev_block_idx, prev_block_duration, self._prev_node, force=True ) # Pop the previous block dag off the stack restoring it self._block_dag = prev_block_dag self._prev_node = prev_node self._wire_map = prev_wire_map + self._idle_after = prev_idle_after + self._current_block_idx = prev_current_block_idx return new_block_dag @@ -463,9 +467,9 @@ def _will_use_fast_path(self, node: DAGNode) -> bool: def _visit_control_flow_op(self, node: DAGNode) -> None: """Visit a control-flow node to pad.""" # Control-flow terminator ends scheduling of block currently - block_idx, t0 = self._node_start_time[node] - self._terminate_block(t0, block_idx) - self._add_block_terminating_barrier(block_idx, t0, node) + scheduled_block_idx, t0 = self._node_start_time[node] + self._terminate_block(t0, scheduled_block_idx) + self._add_block_terminating_barrier(scheduled_block_idx, t0, node) # Only pad non-fast path nodes fast_path_node = node in self._fast_path_nodes @@ -480,8 +484,8 @@ def _visit_control_flow_op(self, node: DAGNode) -> None: # We resolve this here by extracting the cached dag blocks that were # stored by the scheduling pass. new_node_block_dags = [] - for block_idx, _ in enumerate(node.op.blocks): - block_dag = self._node_block_dags[node][block_idx] + for body_idx, _ in enumerate(node.op.blocks): + block_dag = self._node_block_dags[node][body_idx] inner_wire_map = { inner: outer for outer, inner in zip( @@ -513,7 +517,7 @@ def _visit_control_flow_op(self, node: DAGNode) -> None: else: padded_qubits = self._block_dag.qubits self._apply_scheduled_op( - block_idx, + scheduled_block_idx, t0, new_control_flow_op, padded_qubits, diff --git a/release-notes/unreleased/3077.bug.rst b/release-notes/unreleased/3077.bug.rst new file mode 100644 index 0000000000..114938f006 --- /dev/null +++ b/release-notes/unreleased/3077.bug.rst @@ -0,0 +1,6 @@ +Fixed control-flow padding in :class:`.BlockBasePadder` so nested block traversal no +longer leaks timing state into the parent block. Rebuilt control-flow operations now +retain their scheduled block index, and barriers after final fast-path operations use +the correct block index and start time. This prevents incorrect scheduling metadata and +padding when dynamic circuits contain nested or multi-branch control flow +(:issue:`3077`; idle-qubit detection in :issue:`3005` remains separate). diff --git a/test/unit/transpiler/passes/scheduling/test_scheduler.py b/test/unit/transpiler/passes/scheduling/test_scheduler.py index 0b175676f7..de99d46b14 100644 --- a/test/unit/transpiler/passes/scheduling/test_scheduler.py +++ b/test/unit/transpiler/passes/scheduling/test_scheduler.py @@ -14,9 +14,10 @@ from ddt import data, ddt from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, transpile -from qiskit.circuit import Delay, Parameter +from qiskit.circuit import ControlFlowOp, Delay, Parameter from qiskit.circuit.library import CXGate, Measure, Reset, RZGate, XGate from qiskit.converters import circuit_to_dag +from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.target import InstructionProperties, Target @@ -1178,6 +1179,89 @@ def test_back_to_back_if_test(self, use_target): self.assertEqual(expected, scheduled) + def test_padding_preserves_control_flow_block_indices(self): + """Test that rebuilding control-flow ops preserves their scheduled block indices.""" + target = Target(num_qubits=3, dt=1) + target.add_instruction( + XGate(), + {(qubit,): InstructionProperties(duration=200) for qubit in range(3)}, + ) + target.add_instruction( + Measure(), + {(qubit,): InstructionProperties(duration=1000) for qubit in range(3)}, + ) + + circuit = QuantumCircuit(3, 1) + circuit.measure(0, 0) + with circuit.if_test((0, True)) as else_: + circuit.x(1) + with else_: + circuit.x(2) + with circuit.if_test((0, True)) as else_: + circuit.x(2) + with else_: + circuit.x(1) + + scheduler = PassManager([ASAPScheduleAnalysis(target=target)]) + scheduler.run(circuit) + expected = [ + start_time + for node, start_time in scheduler.property_set["node_start_time"].items() + if isinstance(node.op, ControlFlowOp) + ] + + scheduler_and_padder = PassManager( + [ + ASAPScheduleAnalysis(target=target), + PadDelay(target=target, schedule_idle_qubits=True), + ] + ) + scheduler_and_padder.run(circuit) + actual = [ + start_time + for node, start_time in scheduler_and_padder.property_set["node_start_time"].items() + if isinstance(node.op, ControlFlowOp) + ] + + self.assertEqual(expected, actual) + + def test_nested_block_visit_restores_parent_timing_state(self): + """Test nested timing state restoration and final fast-path barrier placement.""" + padder = PadDelay(target=Target(num_qubits=1, dt=1)) + parent_qubit = QuantumCircuit(1).qubits[0] + parent_idle_after = {parent_qubit: 10} + padder._idle_after = parent_idle_after + padder._current_block_idx = 7 + padder._prev_node = None + padder._wire_map = {} + padder._block_dag = DAGCircuit() + padder._block_duration = 0 + padder._block_ordering_callable = lambda _: [] + + nested_dag = DAGCircuit() + padder._empty_dag_like = lambda *args, **kwargs: nested_dag + fast_path_node = object() + padder._fast_path_nodes = {fast_path_node} + terminating_barrier_args = [] + + def mutate_nested_timing_state(block_duration, block_idx): + del block_duration, block_idx + padder._idle_after = {parent_qubit: 20} + padder._current_block_idx = 11 + padder._prev_node = fast_path_node + + def record_terminating_barrier(block_idx, time, current_node, force=False): + terminating_barrier_args.append((block_idx, time, current_node, force)) + + padder._terminate_block = mutate_nested_timing_state + padder._add_block_terminating_barrier = record_terminating_barrier + + padder._visit_block(nested_dag, {}) + + self.assertEqual(terminating_barrier_args, [(7, 0, fast_path_node, True)]) + self.assertIs(padder._idle_after, parent_idle_after) + self.assertEqual(padder._current_block_idx, 7) + @data(True, False) def test_nested_control_scheduling(self, use_target): """Test scheduling of nested control-flow."""