Skip to content

Commit ce16e1a

Browse files
authored
Port ASAPScheduleAnalysis to Rust (Qiskit#14833)
* Initial commit: Port ASAPScheduleAnalysis to Rust * Fix lint * Fix lint * Import TimeOps instead of redefining * Implement reviewer suggestions * Fix lint
1 parent 7e90e42 commit ce16e1a

File tree

5 files changed

+232
-59
lines changed

5 files changed

+232
-59
lines changed

crates/pyext/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ where
3030
#[pymodule]
3131
fn _accelerate(m: &Bound<PyModule>) -> PyResult<()> {
3232
add_submodule(m, ::qiskit_transpiler::passes::alap_schedule_analysis_mod, "alap_schedule_analysis")?;
33+
add_submodule(m, ::qiskit_transpiler::passes::asap_schedule_analysis_mod, "asap_schedule_analysis")?;
3334
add_submodule(m, ::qiskit_transpiler::passes::apply_layout_mod, "apply_layout")?;
3435
add_submodule(m, ::qiskit_transpiler::passes::barrier_before_final_measurements_mod, "barrier_before_final_measurement")?;
3536
add_submodule(m, ::qiskit_transpiler::passes::basis_translator_mod, "basis_translator")?;
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
// This code is part of Qiskit.
2+
//
3+
// (C) Copyright IBM 2025
4+
//
5+
// This code is licensed under the Apache License, Version 2.0. You may
6+
// obtain a copy of this license in the LICENSE.txt file in the root directory
7+
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
//
9+
// Any modifications or derivative works of this code must retain this
10+
// copyright notice, and modified files need to carry a notice indicating
11+
// that they have been altered from the originals.
12+
13+
use crate::passes::alap_schedule_analysis::TimeOps;
14+
use crate::TranspilerError;
15+
use hashbrown::HashMap;
16+
use pyo3::prelude::*;
17+
use pyo3::types::PyDict;
18+
use qiskit_circuit::dag_circuit::{DAGCircuit, Wire};
19+
use qiskit_circuit::dag_node::{DAGNode, DAGOpNode};
20+
use qiskit_circuit::operations::{OperationRef, StandardInstruction};
21+
use qiskit_circuit::{Clbit, Qubit};
22+
use rustworkx_core::petgraph::prelude::NodeIndex;
23+
24+
pub fn run_asap_schedule_analysis<T: TimeOps>(
25+
dag: &DAGCircuit,
26+
clbit_write_latency: T,
27+
node_durations: HashMap<NodeIndex, T>,
28+
) -> PyResult<HashMap<NodeIndex, T>> {
29+
if dag.qregs().len() != 1 || !dag.qregs_data().contains_key("q") {
30+
return Err(TranspilerError::new_err(
31+
"ASAP schedule runs on physical circuits only",
32+
));
33+
}
34+
35+
let mut node_start_time: HashMap<NodeIndex, T> = HashMap::new();
36+
let mut idle_after: HashMap<Wire, T> = HashMap::new();
37+
38+
let zero = T::zero();
39+
40+
for index in 0..dag.qubits().len() {
41+
idle_after.insert(Wire::Qubit(Qubit::new(index)), zero);
42+
}
43+
44+
for index in 0..dag.clbits().len() {
45+
idle_after.insert(Wire::Clbit(Clbit::new(index)), zero);
46+
}
47+
48+
for node_index in dag.topological_op_nodes()? {
49+
let op = dag[node_index].unwrap_operation();
50+
51+
let qargs: Vec<Wire> = dag
52+
.qargs_interner()
53+
.get(op.qubits)
54+
.iter()
55+
.map(|&q| Wire::Qubit(q))
56+
.collect();
57+
let cargs: Vec<Wire> = dag
58+
.cargs_interner()
59+
.get(op.clbits)
60+
.iter()
61+
.map(|&c| Wire::Clbit(c))
62+
.collect();
63+
64+
let &op_duration = node_durations.get(&node_index).ok_or_else(|| {
65+
TranspilerError::new_err(format!(
66+
"No duration found for node at index {}",
67+
node_index.index()
68+
))
69+
})?;
70+
let op_view = op.op.view();
71+
let is_gate_or_delay = matches!(
72+
op_view,
73+
OperationRef::Gate(_)
74+
| OperationRef::StandardGate(_)
75+
| OperationRef::StandardInstruction(StandardInstruction::Delay(_))
76+
);
77+
78+
// compute t0, t1: instruction interval, note that
79+
// t0: start time of instruction
80+
// t1: end time of instruction
81+
82+
let (t0, t1) = if is_gate_or_delay {
83+
let t0 = qargs
84+
.iter()
85+
.map(|q| idle_after[q])
86+
.fold(zero, |acc, x| *T::max(&acc, &x));
87+
(t0, t0 + op_duration)
88+
} else if matches!(
89+
op_view,
90+
OperationRef::StandardInstruction(StandardInstruction::Measure)
91+
) {
92+
// Measure instruction handling is bit tricky due to clbit_write_latency
93+
let t0q = qargs
94+
.iter()
95+
.map(|q| idle_after[q])
96+
.fold(zero, |acc, x| *T::max(&acc, &x));
97+
let t0c = cargs
98+
.iter()
99+
.map(|c| idle_after[c])
100+
.fold(zero, |acc, x| *T::max(&acc, &x));
101+
// Assume following case (t0c > t0q)
102+
//
103+
// |t0q
104+
// Q ▒▒▒▒░░░░░░░░░░░░
105+
// C ▒▒▒▒▒▒▒▒░░░░░░░░
106+
// |t0c
107+
//
108+
// In this case, there is no actual clbit access until clbit_write_latency.
109+
// The node t0 can be push backward by this amount.
110+
//
111+
// |t0q' = t0c - clbit_write_latency
112+
// Q ▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒
113+
// C ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
114+
// |t0c' = t0c
115+
//
116+
// rather than naively doing
117+
//
118+
// |t0q' = t0c
119+
// Q ▒▒▒▒░░░░▒▒▒▒▒▒▒▒
120+
// C ▒▒▒▒▒▒▒▒░░░▒▒▒▒▒
121+
// |t0c' = t0c + clbut_write_latency
122+
let t0 = *T::max(&t0q, &(t0c - clbit_write_latency));
123+
let t1 = t0 + op_duration;
124+
for clbit in cargs.iter() {
125+
idle_after.insert(*clbit, t1);
126+
}
127+
(t0, t1)
128+
} else {
129+
// Directives (like Barrier)
130+
let t0 = qargs
131+
.iter()
132+
.chain(cargs.iter())
133+
.map(|bit| idle_after[bit])
134+
.fold(zero, |acc, x| *T::max(&acc, &x));
135+
(t0, t0 + op_duration)
136+
};
137+
138+
for qubit in qargs {
139+
idle_after.insert(qubit, t1);
140+
}
141+
142+
node_start_time.insert(node_index, t0);
143+
}
144+
145+
Ok(node_start_time)
146+
}
147+
148+
#[pyfunction]
149+
/// Runs the ASAPSchedule analysis pass on dag.
150+
///
151+
/// Args:
152+
/// dag (DAGCircuit): DAG to schedule.
153+
/// clbit_write_latency (u64): The latency to write classical bits.
154+
/// node_durations (PyDict): Mapping from node indices to operation durations.
155+
///
156+
/// Returns:
157+
/// PyDict: A dictionary mapping each DAGOpNode to its scheduled start time.
158+
///
159+
#[pyo3(name = "asap_schedule_analysis", signature= (dag, clbit_write_latency, node_durations))]
160+
pub fn py_run_asap_schedule_analysis(
161+
py: Python,
162+
dag: &DAGCircuit,
163+
clbit_write_latency: u64,
164+
node_durations: &Bound<PyDict>,
165+
) -> PyResult<Py<PyDict>> {
166+
// Extract indices and durations from PyDict
167+
// Get the first duration type
168+
let mut iter = node_durations.iter();
169+
let py_dict = PyDict::new(py);
170+
if let Some((_, first_duration)) = iter.next() {
171+
if first_duration.extract::<u64>().is_ok() {
172+
// All durations are of type u64
173+
let mut op_durations = HashMap::new();
174+
for (py_node, py_duration) in node_durations.iter() {
175+
let node_idx = py_node
176+
.downcast_into::<DAGOpNode>()?
177+
.extract::<DAGNode>()?
178+
.node
179+
.expect("Node index not found.");
180+
let val = py_duration.extract::<u64>()?;
181+
op_durations.insert(node_idx, val);
182+
}
183+
let node_start_time =
184+
run_asap_schedule_analysis::<u64>(dag, clbit_write_latency, op_durations)?;
185+
for (node_idx, t1) in node_start_time {
186+
let node = dag.get_node(py, node_idx)?;
187+
py_dict.set_item(node, t1)?;
188+
}
189+
} else if first_duration.extract::<f64>().is_ok() {
190+
// All durations are of type f64
191+
let mut op_durations = HashMap::new();
192+
for (py_node, py_duration) in node_durations.iter() {
193+
let node_idx = py_node
194+
.downcast_into::<DAGOpNode>()?
195+
.extract::<DAGNode>()?
196+
.node
197+
.expect("Node index not found.");
198+
let val = py_duration.extract::<f64>()?;
199+
op_durations.insert(node_idx, val);
200+
}
201+
let node_start_time =
202+
run_asap_schedule_analysis::<f64>(dag, clbit_write_latency as f64, op_durations)?;
203+
for (node_idx, t1) in node_start_time {
204+
let node = dag.get_node(py, node_idx)?;
205+
py_dict.set_item(node, t1)?;
206+
}
207+
} else {
208+
return Err(TranspilerError::new_err("Duration must be int or float"));
209+
}
210+
} else {
211+
return Err(TranspilerError::new_err("No durations provided"));
212+
}
213+
214+
Ok(py_dict.into())
215+
}
216+
217+
pub fn asap_schedule_analysis_mod(m: &Bound<PyModule>) -> PyResult<()> {
218+
m.add_wrapped(wrap_pyfunction!(py_run_asap_schedule_analysis))?;
219+
Ok(())
220+
}

crates/transpiler/src/passes/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
2424
mod alap_schedule_analysis;
2525
mod apply_layout;
26+
mod asap_schedule_analysis;
2627
mod barrier_before_final_measurement;
2728
mod basis_translator;
2829
mod check_map;
@@ -50,6 +51,7 @@ mod vf2;
5051

5152
pub use alap_schedule_analysis::{alap_schedule_analysis_mod, run_alap_schedule_analysis};
5253
pub use apply_layout::{apply_layout, apply_layout_mod, update_layout};
54+
pub use asap_schedule_analysis::{asap_schedule_analysis_mod, run_asap_schedule_analysis};
5355
pub use barrier_before_final_measurement::{
5456
barrier_before_final_measurements_mod, run_barrier_before_final_measurements,
5557
};

qiskit/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
# and not have to rely on attribute access. No action needed for top-level extension packages.
6363

6464
sys.modules["qiskit._accelerate.alap_schedule_analysis"] = _accelerate.alap_schedule_analysis
65+
sys.modules["qiskit._accelerate.asap_schedule_analysis"] = _accelerate.asap_schedule_analysis
6566
sys.modules["qiskit._accelerate.apply_layout"] = _accelerate.apply_layout
6667
sys.modules["qiskit._accelerate.circuit"] = _accelerate.circuit
6768
sys.modules["qiskit._accelerate.circuit.classical"] = _accelerate.circuit.classical

qiskit/transpiler/passes/scheduling/scheduling/asap.py

Lines changed: 8 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@
1111
# that they have been altered from the originals.
1212

1313
"""ASAP Scheduling."""
14-
from qiskit.circuit import Measure
1514
from qiskit.transpiler.exceptions import TranspilerError
16-
1715
from qiskit.transpiler.passes.scheduling.scheduling.base_scheduler import BaseScheduler
16+
from qiskit._accelerate.asap_schedule_analysis import asap_schedule_analysis
1817

1918

2019
class ASAPScheduleAnalysis(BaseScheduler):
@@ -37,64 +36,14 @@ def run(self, dag):
3736
TranspilerError: if the circuit is not mapped on physical qubits.
3837
TranspilerError: if conditional bit is added to non-supported instruction.
3938
"""
40-
if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None:
41-
raise TranspilerError("ASAP schedule runs on physical circuits only")
39+
4240
if self.property_set["time_unit"] == "stretch":
4341
raise TranspilerError("Scheduling cannot run on circuits with stretch durations.")
4442

43+
node_durations = {
44+
node: self._get_node_duration(node, dag) for node in dag.topological_op_nodes()
45+
}
4546
clbit_write_latency = self.property_set.get("clbit_write_latency", 0)
46-
47-
node_start_time = {}
48-
idle_after = {q: 0 for q in dag.qubits + dag.clbits}
49-
for node in dag.topological_op_nodes():
50-
op_duration = self._get_node_duration(node, dag)
51-
52-
# compute t0, t1: instruction interval, note that
53-
# t0: start time of instruction
54-
# t1: end time of instruction
55-
if isinstance(node.op, self.CONDITIONAL_SUPPORTED):
56-
t0q = max(idle_after[q] for q in node.qargs)
57-
t0 = t0q
58-
t1 = t0 + op_duration
59-
else:
60-
if isinstance(node.op, Measure):
61-
# measure instruction handling is bit tricky due to clbit_write_latency
62-
t0q = max(idle_after[q] for q in node.qargs)
63-
t0c = max(idle_after[c] for c in node.cargs)
64-
# Assume following case (t0c > t0q)
65-
#
66-
# |t0q
67-
# Q ▒▒▒▒░░░░░░░░░░░░
68-
# C ▒▒▒▒▒▒▒▒░░░░░░░░
69-
# |t0c
70-
#
71-
# In this case, there is no actual clbit access until clbit_write_latency.
72-
# The node t0 can be push backward by this amount.
73-
#
74-
# |t0q' = t0c - clbit_write_latency
75-
# Q ▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒
76-
# C ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
77-
# |t0c' = t0c
78-
#
79-
# rather than naively doing
80-
#
81-
# |t0q' = t0c
82-
# Q ▒▒▒▒░░░░▒▒▒▒▒▒▒▒
83-
# C ▒▒▒▒▒▒▒▒░░░▒▒▒▒▒
84-
# |t0c' = t0c + clbit_write_latency
85-
#
86-
t0 = max(t0q, t0c - clbit_write_latency)
87-
t1 = t0 + op_duration
88-
for clbit in node.cargs:
89-
idle_after[clbit] = t1
90-
else:
91-
# It happens to be directives such as barrier
92-
t0 = max(idle_after[bit] for bit in node.qargs + node.cargs)
93-
t1 = t0 + op_duration
94-
95-
for bit in node.qargs:
96-
idle_after[bit] = t1
97-
98-
node_start_time[node] = t0
99-
100-
self.property_set["node_start_time"] = node_start_time
47+
self.property_set["node_start_time"] = asap_schedule_analysis(
48+
dag, clbit_write_latency, node_durations
49+
)

0 commit comments

Comments
 (0)