Skip to content

Commit 8c50ce4

Browse files
authored
Add method to compute estimated duration of scheduled circuit (#13783)
* Add method to compute estimated duration of scheduled circuit This commit adds a new QuantumCircuit method to compute the estimated duration of a scheduled circuit. This is to replace the deprecated duration attribute that the transpiler was potentially setting during the scheduling stage. This method computes the longest duration path in the dag view of the circuit internally. This method should have been included in the 1.2.0 release prior to the deprecation of the `QuantumCircuit.duration` attribute in 1.3.0. But, this was an oversight in the path to deprecation, as it was part of larger deprecation of numerous scheduling pieces in the 1.3.0. We should definitely backport this for the 1.4.0 release for inclusion in that release prior to the Qiskit 2.0.0 release which removes the deprecated attribute * Simplify dag node indexing * Expand docs * Fix handling for StandardInstruction in rust and add first test * Expand test coverage * Fix lint
1 parent d9b8a18 commit 8c50ce4

File tree

8 files changed

+314
-1
lines changed

8 files changed

+314
-1
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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 pyo3::prelude::*;
14+
use pyo3::wrap_pyfunction;
15+
16+
use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType, Wire};
17+
use qiskit_circuit::operations::{DelayUnit, Operation, OperationRef, Param, StandardInstruction};
18+
19+
use crate::nlayout::PhysicalQubit;
20+
use crate::target_transpiler::Target;
21+
use crate::QiskitError;
22+
use rustworkx_core::dag_algo::longest_path;
23+
use rustworkx_core::petgraph::stable_graph::StableDiGraph;
24+
use rustworkx_core::petgraph::visit::{EdgeRef, IntoEdgeReferences};
25+
26+
/// Estimate the duration of a scheduled circuit in seconds
27+
#[pyfunction]
28+
pub(crate) fn compute_estimated_duration(dag: &DAGCircuit, target: &Target) -> PyResult<f64> {
29+
let dt = target.dt;
30+
31+
let get_duration =
32+
|edge: <&StableDiGraph<NodeType, Wire> as IntoEdgeReferences>::EdgeRef| -> PyResult<f64> {
33+
let node_weight = &dag[edge.target()];
34+
match node_weight {
35+
NodeType::Operation(inst) => {
36+
let name = inst.op.name();
37+
let qubits = dag.get_qargs(inst.qubits);
38+
let physical_qubits: Vec<PhysicalQubit> =
39+
qubits.iter().map(|x| PhysicalQubit::new(x.0)).collect();
40+
41+
if let OperationRef::StandardInstruction(op) = inst.op.view() {
42+
if let StandardInstruction::Delay(unit) = op {
43+
let dur = &inst.params.as_ref().unwrap()[0];
44+
return if unit == DelayUnit::DT {
45+
if let Some(dt) = dt {
46+
match dur {
47+
Param::Float(val) =>
48+
{
49+
Ok(val / dt)
50+
51+
},
52+
Param::Obj(val) => {
53+
Python::with_gil(|py| {
54+
let dur_float: f64 = val.extract(py)?;
55+
Ok(dur_float * dt)
56+
})
57+
},
58+
Param::ParameterExpression(_) => Err(QiskitError::new_err(
59+
"Circuit contains parameterized delays, can't compute a duration estimate with this circuit"
60+
)),
61+
}
62+
} else {
63+
Err(QiskitError::new_err(
64+
"Circuit contains delays in dt but the target doesn't specify dt"
65+
))
66+
}
67+
} else if unit == DelayUnit::S {
68+
match dur {
69+
Param::Float(val) => Ok(*val),
70+
_ => Err(QiskitError::new_err(
71+
"Invalid type for parameter value for delay in circuit",
72+
)),
73+
}
74+
} else {
75+
Err(QiskitError::new_err(
76+
"Circuit contains delays in units other then seconds or dt, the circuit is not scheduled."
77+
))
78+
};
79+
} else if let StandardInstruction::Barrier(_) = op {
80+
return Ok(0.);
81+
}
82+
}
83+
match target.get_duration(name, &physical_qubits) {
84+
Some(dur) => Ok(dur),
85+
None => Err(QiskitError::new_err(format!(
86+
"Duration not found for {} on qubits: {:?}",
87+
name, qubits
88+
))),
89+
}
90+
}
91+
NodeType::QubitOut(_) | NodeType::ClbitOut(_) => Ok(0.),
92+
NodeType::ClbitIn(_) | NodeType::QubitIn(_) => {
93+
Err(QiskitError::new_err("Invalid circuit provided"))
94+
}
95+
_ => Err(QiskitError::new_err(
96+
"Circuit contains Vars, duration can't be calculated with classical variables",
97+
)),
98+
}
99+
};
100+
match longest_path(dag.dag(), get_duration)? {
101+
Some((_, weight)) => Ok(weight),
102+
None => Err(QiskitError::new_err("Invalid circuit provided")),
103+
}
104+
}
105+
106+
pub fn compute_duration(m: &Bound<PyModule>) -> PyResult<()> {
107+
m.add_wrapped(wrap_pyfunction!(compute_estimated_duration))?;
108+
Ok(())
109+
}

crates/accelerate/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use pyo3::import_exception;
1717
pub mod barrier_before_final_measurement;
1818
pub mod basis;
1919
pub mod check_map;
20+
pub mod circuit_duration;
2021
pub mod circuit_library;
2122
pub mod commutation_analysis;
2223
pub mod commutation_cancellation;

crates/accelerate/src/target_transpiler/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,17 @@ impl Target {
977977
})
978978
}
979979

980+
/// Get the duration of a given instruction in the target
981+
pub fn get_duration(&self, name: &str, qargs: &[PhysicalQubit]) -> Option<f64> {
982+
self.gate_map.get(name).and_then(|gate_props| {
983+
let qargs_key: Qargs = qargs.iter().cloned().collect();
984+
match gate_props.get(Some(&qargs_key)) {
985+
Some(props) => props.as_ref().and_then(|inst_props| inst_props.duration),
986+
None => None,
987+
}
988+
})
989+
}
990+
980991
/// Get an iterator over the indices of all physical qubits of the target
981992
pub fn physical_qubits(&self) -> impl ExactSizeIterator<Item = usize> {
982993
0..self.num_qubits.unwrap_or_default()

crates/pyext/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ fn _accelerate(m: &Bound<PyModule>) -> PyResult<()> {
3131
add_submodule(m, ::qiskit_accelerate::barrier_before_final_measurement::barrier_before_final_measurements_mod, "barrier_before_final_measurement")?;
3232
add_submodule(m, ::qiskit_accelerate::basis::basis, "basis")?;
3333
add_submodule(m, ::qiskit_accelerate::check_map::check_map_mod, "check_map")?;
34+
add_submodule(m, ::qiskit_accelerate::circuit_duration::compute_duration, "circuit_duration")?;
3435
add_submodule(m, ::qiskit_accelerate::circuit_library::circuit_library, "circuit_library")?;
3536
add_submodule(m, ::qiskit_accelerate::commutation_analysis::commutation_analysis, "commutation_analysis")?;
3637
add_submodule(m, ::qiskit_accelerate::commutation_cancellation::commutation_cancellation, "commutation_cancellation")?;

qiskit/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
sys.modules["qiskit._accelerate.twirling"] = _accelerate.twirling
111111
sys.modules["qiskit._accelerate.high_level_synthesis"] = _accelerate.high_level_synthesis
112112
sys.modules["qiskit._accelerate.remove_identity_equiv"] = _accelerate.remove_identity_equiv
113+
sys.modules["qiskit._accelerate.circuit_duration"] = _accelerate.circuit_duration
113114

114115
from qiskit.exceptions import QiskitError, MissingOptionalLibraryError
115116

qiskit/circuit/quantumcircuit.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import numpy as np
4242
from qiskit._accelerate.circuit import CircuitData
4343
from qiskit._accelerate.circuit import StandardGate
44+
from qiskit._accelerate.circuit_duration import compute_estimated_duration
4445
from qiskit.exceptions import QiskitError
4546
from qiskit.utils.multiprocessing import is_main_process
4647
from qiskit.circuit.instruction import Instruction
@@ -157,6 +158,8 @@ class QuantumCircuit:
157158
:attr:`data` List of individual :class:`CircuitInstruction`\\ s that make up the
158159
circuit.
159160
:attr:`duration` Total duration of the circuit, added by scheduling transpiler passes.
161+
This attribute is deprecated and :meth:`.estimate_duration` should
162+
be used instead.
160163
161164
:attr:`layout` Hardware layout and routing information added by the transpiler.
162165
:attr:`num_ancillas` The number of ancilla qubits in the circuit.
@@ -909,8 +912,9 @@ class QuantumCircuit:
909912
910913
If a :class:`QuantumCircuit` has been scheduled as part of a transpilation pipeline, the timing
911914
information for individual qubits can be accessed. The whole-circuit timing information is
912-
available through the :attr:`duration`, :attr:`unit` and :attr:`op_start_times` attributes.
915+
available through the :meth:`estimate_duration` method and :attr:`op_start_times` attribute.
913916
917+
.. automethod:: estimate_duration
914918
.. automethod:: qubit_duration
915919
.. automethod:: qubit_start_time
916920
.. automethod:: qubit_stop_time
@@ -6919,6 +6923,65 @@ def qubit_stop_time(self, *qubits: Union[Qubit, int]) -> float:
69196923
else:
69206924
return 0 # If there are no instructions over bits
69216925

6926+
def estimate_duration(self, target, unit: str = "s") -> int | float:
6927+
"""Estimate the duration of a scheduled circuit
6928+
6929+
This method computes the estimate of the circuit duration by finding
6930+
the longest duration path in the circuit based on the durations
6931+
provided by a given target. This method only works for simple circuits
6932+
that have no control flow or other classical feed-forward operations.
6933+
6934+
Args:
6935+
target (Target): The :class:`.Target` instance that contains durations for
6936+
the instructions if the target is missing duration data for any of the
6937+
instructions in the circuit an :class:`.QiskitError` will be raised. This
6938+
should be the same target object used as the target for transpilation.
6939+
unit: The unit to return the duration in. This defaults to "s" for seconds
6940+
but this can be a supported SI prefix for seconds returns. For example
6941+
setting this to "n" will return in unit of nanoseconds. Supported values
6942+
of this type are "f", "p", "n", "u", "µ", "m", "k", "M", "G", "T", and
6943+
"P". Additionally, a value of "dt" is also accepted to output an integer
6944+
in units of "dt". For this to function "dt" must be specified in the
6945+
``target``.
6946+
6947+
Returns:
6948+
The estimated duration for the execution of a single shot of the circuit in
6949+
the specified unit.
6950+
6951+
Raises:
6952+
QiskitError: If the circuit is not scheduled or contains other
6953+
details that prevent computing an estimated duration from
6954+
(such as parameterized delay).
6955+
"""
6956+
from qiskit.converters import circuit_to_dag
6957+
6958+
dur = compute_estimated_duration(circuit_to_dag(self), target)
6959+
if unit == "s":
6960+
return dur
6961+
if unit == "dt":
6962+
from qiskit.circuit.duration import duration_in_dt # pylint: disable=cyclic-import
6963+
6964+
return duration_in_dt(dur, target.dt)
6965+
6966+
prefix_dict = {
6967+
"f": 1e-15,
6968+
"p": 1e-12,
6969+
"n": 1e-9,
6970+
"u": 1e-6,
6971+
"µ": 1e-6,
6972+
"m": 1e-3,
6973+
"k": 1e3,
6974+
"M": 1e6,
6975+
"G": 1e9,
6976+
"T": 1e12,
6977+
"P": 1e15,
6978+
}
6979+
if unit not in prefix_dict:
6980+
raise QiskitError(
6981+
f"Specified unit: {unit} is not a valid/supported SI prefix, 's', or 'dt'"
6982+
)
6983+
return dur / prefix_dict[unit]
6984+
69226985

69236986
class _OuterCircuitScopeInterface(CircuitScopeInterface):
69246987
# This is an explicit interface-fulfilling object friend of QuantumCircuit that acts as its
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
features_circuits:
3+
- |
4+
Added a new method, :meth:`.QuantumCircuit.estimate_duration`, to compute
5+
the estimated duration of a scheduled circuit output from the :mod:`.transpiler`.
6+
This should be used if you need an estimate of the full circuit duration instead
7+
of the deprecated :attr:`.QuantumCircuit.duration` attribute.

test/python/circuit/test_scheduled_circuit.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,126 @@ def test_convert_duration_to_dt(self):
469469
ref_unit,
470470
)
471471

472+
@data("s", "dt", "f", "p", "n", "u", "µ", "m", "k", "M", "G", "T", "P")
473+
def test_estimate_duration(self, unit):
474+
"""Test the circuit duration is computed correctly."""
475+
backend = GenericBackendV2(num_qubits=3, seed=42)
476+
477+
circ = QuantumCircuit(2)
478+
circ.cx(0, 1)
479+
circ.measure_all()
480+
481+
circuit_dt = transpile(circ, backend, scheduling_method="asap")
482+
duration = circuit_dt.estimate_duration(backend.target, unit=unit)
483+
expected_in_sec = 1.815516e-06
484+
expected_val = {
485+
"s": expected_in_sec,
486+
"dt": int(expected_in_sec / backend.target.dt),
487+
"f": expected_in_sec / 1e-15,
488+
"p": expected_in_sec / 1e-12,
489+
"n": expected_in_sec / 1e-9,
490+
"u": expected_in_sec / 1e-6,
491+
"µ": expected_in_sec / 1e-6,
492+
"m": expected_in_sec / 1e-3,
493+
"k": expected_in_sec / 1e3,
494+
"M": expected_in_sec / 1e6,
495+
"G": expected_in_sec / 1e9,
496+
"T": expected_in_sec / 1e12,
497+
"P": expected_in_sec / 1e15,
498+
}
499+
self.assertEqual(duration, expected_val[unit])
500+
501+
@data("s", "dt", "f", "p", "n", "u", "µ", "m", "k", "M", "G", "T", "P")
502+
def test_estimate_duration_with_long_delay(self, unit):
503+
"""Test the circuit duration is computed correctly."""
504+
backend = GenericBackendV2(num_qubits=3, seed=42)
505+
506+
circ = QuantumCircuit(3)
507+
circ.cx(0, 1)
508+
circ.measure_all()
509+
circ.delay(1e15, 2)
510+
511+
circuit_dt = transpile(circ, backend, scheduling_method="asap")
512+
duration = circuit_dt.estimate_duration(backend.target, unit=unit)
513+
expected_in_sec = 222000.00000139928
514+
expected_val = {
515+
"s": expected_in_sec,
516+
"dt": int(expected_in_sec / backend.target.dt),
517+
"f": expected_in_sec / 1e-15,
518+
"p": expected_in_sec / 1e-12,
519+
"n": expected_in_sec / 1e-9,
520+
"u": expected_in_sec / 1e-6,
521+
"µ": expected_in_sec / 1e-6,
522+
"m": expected_in_sec / 1e-3,
523+
"k": expected_in_sec / 1e3,
524+
"M": expected_in_sec / 1e6,
525+
"G": expected_in_sec / 1e9,
526+
"T": expected_in_sec / 1e12,
527+
"P": expected_in_sec / 1e15,
528+
}
529+
self.assertEqual(duration, expected_val[unit])
530+
531+
def test_estimate_duration_invalid_unit(self):
532+
backend = GenericBackendV2(num_qubits=3, seed=42)
533+
534+
circ = QuantumCircuit(2)
535+
circ.cx(0, 1)
536+
circ.measure_all()
537+
538+
circuit_dt = transpile(circ, backend, scheduling_method="asap")
539+
with self.assertRaises(QiskitError):
540+
circuit_dt.estimate_duration(backend.target, unit="jiffy")
541+
542+
def test_delay_circ(self):
543+
backend = GenericBackendV2(num_qubits=3, seed=42)
544+
545+
circ = QuantumCircuit(2)
546+
circ.delay(100, 0, unit="dt")
547+
548+
circuit_dt = transpile(circ, backend, scheduling_method="asap")
549+
res = circuit_dt.estimate_duration(backend.target, unit="dt")
550+
self.assertIsInstance(res, int)
551+
self.assertEqual(res, 100)
552+
553+
def test_estimate_duration_control_flow(self):
554+
backend = GenericBackendV2(num_qubits=3, seed=42, control_flow=True)
555+
556+
circ = QuantumCircuit(2)
557+
circ.cx(0, 1)
558+
circ.measure_all()
559+
with circ.if_test((0, True)):
560+
circ.x(0)
561+
with self.assertRaises(QiskitError):
562+
circ.estimate_duration(backend.target)
563+
564+
def test_estimate_duration_with_var(self):
565+
backend = GenericBackendV2(num_qubits=3, seed=42, control_flow=True)
566+
567+
circ = QuantumCircuit(2)
568+
circ.cx(0, 1)
569+
circ.measure_all()
570+
circ.add_var("a", False)
571+
with self.assertRaises(QiskitError):
572+
circ.estimate_duration(backend.target)
573+
574+
def test_estimate_duration_parameterized_delay(self):
575+
backend = GenericBackendV2(num_qubits=3, seed=42, control_flow=True)
576+
577+
circ = QuantumCircuit(2)
578+
circ.cx(0, 1)
579+
circ.measure_all()
580+
circ.delay(Parameter("t"), 0)
581+
with self.assertRaises(QiskitError):
582+
circ.estimate_duration(backend.target)
583+
584+
def test_estimate_duration_dt_delay_no_dt(self):
585+
backend = GenericBackendV2(num_qubits=3, seed=42)
586+
circ = QuantumCircuit(1)
587+
circ.delay(100, 0)
588+
backend.target.dt = None
589+
with self.assertRaises(QiskitError):
590+
circ.estimate_duration(backend.target)
591+
472592
def test_change_dt_in_transpile(self):
473593
qc = QuantumCircuit(1, 1)
474594
qc.x(0)

0 commit comments

Comments
 (0)