From dca953c972f53787786b37ea5a401d6d11229013 Mon Sep 17 00:00:00 2001 From: Sankalp Thakur Date: Tue, 21 Jul 2026 18:01:07 +0530 Subject: [PATCH 1/4] Fix nested control-flow padding state --- .../passes/scheduling/block_base_padder.py | 20 +++-- release-notes/unreleased/3005.bug.rst | 5 ++ .../passes/scheduling/test_scheduler.py | 87 ++++++++++++++++++- 3 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 release-notes/unreleased/3005.bug.rst 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..34669acb30 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,9 @@ def _visit_block( prev_node = self._prev_node self._prev_node = None prev_wire_map, self._wire_map = self._wire_map, wire_map + # This nested-state preservation was prepared with Codex (GPT-5) and reviewed. + 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 +397,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 +468,10 @@ 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) + # This index-preservation fix was prepared with Codex (GPT-5) and reviewed. + 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 +486,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 +519,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/3005.bug.rst b/release-notes/unreleased/3005.bug.rst new file mode 100644 index 0000000000..8d864b8983 --- /dev/null +++ b/release-notes/unreleased/3005.bug.rst @@ -0,0 +1,5 @@ +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. diff --git a/test/unit/transpiler/passes/scheduling/test_scheduler.py b/test/unit/transpiler/passes/scheduling/test_scheduler.py index 0b175676f7..e5cd3dd55f 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,90 @@ def test_back_to_back_if_test(self, use_target): self.assertEqual(expected, scheduled) + # These control-flow regressions were prepared with Codex (GPT-5) and reviewed. + 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.""" From de10add9c74bdb07838d9ae4483ddba7244b4192 Mon Sep 17 00:00:00 2001 From: Sankalp Thakur Date: Wed, 22 Jul 2026 12:22:49 +0530 Subject: [PATCH 2/4] Relink release note to focused bug report --- release-notes/unreleased/{3005.bug.rst => 3077.bug.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename release-notes/unreleased/{3005.bug.rst => 3077.bug.rst} (100%) diff --git a/release-notes/unreleased/3005.bug.rst b/release-notes/unreleased/3077.bug.rst similarity index 100% rename from release-notes/unreleased/3005.bug.rst rename to release-notes/unreleased/3077.bug.rst From efaee5e37771244f6cf6218fbf2388b626aac6d6 Mon Sep 17 00:00:00 2001 From: Sankalp Thakur Date: Thu, 23 Jul 2026 01:04:24 +0530 Subject: [PATCH 3/4] docs: cite #3077 and scope #3005 in release note --- release-notes/unreleased/3077.bug.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/release-notes/unreleased/3077.bug.rst b/release-notes/unreleased/3077.bug.rst index 8d864b8983..114938f006 100644 --- a/release-notes/unreleased/3077.bug.rst +++ b/release-notes/unreleased/3077.bug.rst @@ -2,4 +2,5 @@ Fixed control-flow padding in :class:`.BlockBasePadder` so nested block traversa 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. +padding when dynamic circuits contain nested or multi-branch control flow +(:issue:`3077`; idle-qubit detection in :issue:`3005` remains separate). From 147b5cf8a770188ed40e6c955e5f677e9012457e Mon Sep 17 00:00:00 2001 From: Sankalp Thakur Date: Thu, 30 Jul 2026 14:45:58 +0530 Subject: [PATCH 4/4] Remove AI-provenance comments from source Tool attribution belongs in the PR description and commit trailers, not as inline comments in library and test code. The AI/LLM disclosure checkboxes in the PR body are unchanged. --- .../transpiler/passes/scheduling/block_base_padder.py | 2 -- test/unit/transpiler/passes/scheduling/test_scheduler.py | 1 - 2 files changed, 3 deletions(-) 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 34669acb30..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,7 +363,6 @@ def _visit_block( prev_node = self._prev_node self._prev_node = None prev_wire_map, self._wire_map = self._wire_map, wire_map - # This nested-state preservation was prepared with Codex (GPT-5) and reviewed. prev_idle_after = self._idle_after prev_current_block_idx = self._current_block_idx @@ -468,7 +467,6 @@ 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 - # This index-preservation fix was prepared with Codex (GPT-5) and reviewed. 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) diff --git a/test/unit/transpiler/passes/scheduling/test_scheduler.py b/test/unit/transpiler/passes/scheduling/test_scheduler.py index e5cd3dd55f..de99d46b14 100644 --- a/test/unit/transpiler/passes/scheduling/test_scheduler.py +++ b/test/unit/transpiler/passes/scheduling/test_scheduler.py @@ -1179,7 +1179,6 @@ def test_back_to_back_if_test(self, use_target): self.assertEqual(expected, scheduled) - # These control-flow regressions were prepared with Codex (GPT-5) and reviewed. 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)