Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions release-notes/unreleased/3077.bug.rst
Original file line number Diff line number Diff line change
@@ -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).
86 changes: 85 additions & 1 deletion test/unit/transpiler/passes/scheduling/test_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down